#!/bin/bash
perform_update_request() {
local access_id="$1"
local access_code="$2"
local updates="$3"
local timeout=30
# === Input Validation ===
if [[ -z "$access_id" ]]; then
echo "Error: access_id must be a non-empty string" >&2
return 1
fi
if [[ -z "$access_code" ]]; then
echo "Error: access_code must be a non-empty string" >&2
return 1
fi
# Validate updates: must be integer between 1 and 50
if ! [[ "$updates" =~ ^[0-9]+$ ]] || (( updates < 1 || updates > 50 )); then
echo "Error: updates must be an integer between 1 and 50 (hard limit)" >&2
return 1
fi
local url="https://www.serversnug.com/api/public/v1/update/"
# === Build JSON payload ===
local payload
payload=$(jq -n \
--arg access_id "$access_id" \
--arg access_code "$access_code" \
--argjson updates "$updates" \
'{access_id: $access_id, access_code: $access_code, updates: $updates}') || {
echo "Error: Failed to construct JSON payload" >&2
return 1
}
# === Make the HTTP request ===
local tmpfile
tmpfile=$(mktemp) || {
echo "Error: Failed to create temporary file" >&2
return 1
}
local http_code
http_code=$(curl -sS --max-time "$timeout" \
-w "%{http_code}" \
-o "$tmpfile" \
-X POST \
-H "Content-Type: application/json" \
-d "$payload" \
"$url")
local curl_exit=$?
local response_body
response_body=$(<"$tmpfile")
rm -f "$tmpfile"
# Handle curl failures (timeout or connection issues)
if [[ $curl_exit -ne 0 ]]; then
if [[ $curl_exit -eq 28 ]]; then
echo "Error: Request timed out while contacting ServerSnug API" >&2
else
echo "Error: Failed to connect to ServerSnug API (network issue)" >&2
fi
return 1
fi
# === Validate JSON ===
if ! echo "$response_body" | jq empty > /dev/null 2>&1; then
echo "Error: Invalid JSON response from server. Status code: $http_code" >&2
return 1
fi
# === Handle HTTP status codes ===
case "$http_code" in
403)
echo "Error: 403 Forbidden - Credentials not recognized" >&2
return 1
;;
500)
echo "Error: 500 Internal Server Error - ServerSnug service issue" >&2
return 1
;;
502)
echo "Error: 502 Bad Gateway - ServerSnug service issue" >&2
return 1
;;
503)
echo "Error: 503 Service Unavailable - ServerSnug under maintenance" >&2
return 1
;;
422)
local err
err=$(echo "$response_body" | jq -r '.error // "Unknown error"')
echo "Error: 422 Processing Error: $err" >&2
return 1
;;
esac
if [[ "$http_code" != "200" ]]; then
echo "Error: Unexpected HTTP status: $http_code" >&2
return 1
fi
# === Check for API-level errors (can occur even on HTTP 200) ===
local api_error
api_error=$(echo "$response_body" | jq -r '.error // empty')
if [[ -n "$api_error" ]]; then
case "$api_error" in
"Allowance exceeded")
echo "Error: Allowance exceeded - fewer results returned than requested" >&2
;;
"Malformed request")
echo "Error: Malformed request - check JSON format" >&2
;;
*)
echo "Error: API error: $api_error" >&2
;;
esac
return 1
fi
# === Validate response structure ===
if ! echo "$response_body" | jq -e 'has("count") and has("response")' > /dev/null; then
echo "Error: Response missing required fields ('count' or 'response')" >&2
return 1
fi
# Success → print JSON to stdout
echo "$response_body"
return 0
}
import requests
import json
from typing import Dict, Any
def perform_update_request(
access_id: str,
access_code: str,
updates: int,
timeout: int = 30
) -> Dict[str, Any]:
"""
Performs a ServerSnug 'update' request with full error handling.
Returns the parsed JSON response on success.
Raises detailed exceptions for any failure.
"""
# Input validation
if not access_id or not isinstance(access_id, str):
raise ValueError("access_id must be a non-empty string")
if not access_code or not isinstance(access_code, str):
raise ValueError("access_code must be a non-empty string")
if not isinstance(updates, int) or updates < 1 or updates > 50:
raise ValueError("updates must be an integer between 1 and 50 (hard limit)")
url = "https://www.serversnug.com/api/public/v1/update/"
payload = {
"access_id": access_id,
"access_code": access_code,
"updates": updates
}
try:
response = requests.post(
url,
json=payload,
timeout=timeout,
allow_redirects=False
)
except requests.exceptions.Timeout:
raise ConnectionError("Request timed out while contacting ServerSnug API")
except requests.exceptions.ConnectionError:
raise ConnectionError("Failed to connect to ServerSnug API (network issue)")
except requests.exceptions.RequestException as e:
raise ConnectionError(f"Request failed: {e}")
# Always attempt to parse JSON
try:
data: Dict[str, Any] = response.json()
except json.JSONDecodeError:
raise ValueError(f"Invalid JSON response from server. Status code: {response.status_code}")
except Exception as e:
raise ValueError(f"Failed to parse response: {e}")
# Handle HTTP-level errors
if response.status_code == 403:
raise PermissionError("403 Forbidden - Credentials not recognized")
if response.status_code == 500:
raise RuntimeError("500 Internal Server Error - ServerSnug service issue")
if response.status_code == 502:
raise RuntimeError("502 Internal Server Error - ServerSnug service issue")
if response.status_code == 503:
raise RuntimeError("503 Service Unavailable - ServerSnug under maintenance")
if response.status_code == 422:
error_msg = data.get("error") # Error is Missing updates
raise RuntimeError("422 Processing Error: {error_msg}")
if response.status_code != 200:
raise RuntimeError(f"Unexpected HTTP status: {response.status_code}")
# Check for API-level error message (returned with 200)
error_msg = data.get("error")
if error_msg:
if error_msg == "Allowance exceeded":
raise RuntimeError("Allowance exceeded - fewer results returned than requested")
if error_msg == "Malformed request":
raise ValueError("Malformed request - check JSON format")
raise RuntimeError(f"API error: {error_msg}")
# Validate expected success structure
if "count" not in data or "response" not in data:
raise ValueError("Response missing required fields ('count' or 'response')")
return data
<?php
/**
* Performs a ServerSnug 'update' request with full error handling (PHP equivalent).
* Returns the parsed JSON response on success.
* Throws detailed exceptions for any failure.
*/
function perform_update_request(
string $access_id,
string $access_code,
int $updates,
int $timeout = 30
): array {
// Input validation
if (empty($access_id) || !is_string($access_id)) {
throw new InvalidArgumentException("access_id must be a non-empty string");
}
if (empty($access_code) || !is_string($access_code)) {
throw new InvalidArgumentException("access_code must be a non-empty string");
}
if ($updates < 1 || $updates > 50) {
throw new InvalidArgumentException("updates must be an integer between 1 and 50 (hard limit)");
}
$url = "https://www.serversnug.com/api/public/v1/update/";
$payload = [
"access_id" => $access_id,
"access_code" => $access_code,
"updates" => $updates
];
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false); // Defensive: same as Python version
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
$responseBody = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlError = curl_error($ch);
curl_close($ch);
// Network / cURL errors
if ($responseBody === false) {
throw new RuntimeException("cURL request failed: " . $curlError);
}
if ($httpCode === 0) {
throw new RuntimeException("Failed to connect to ServerSnug API (network issue)");
}
// Always attempt to parse JSON even on error status codes
$data = json_decode($responseBody, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new RuntimeException("Invalid JSON response from server. Status code: $httpCode");
}
// Handle HTTP-level errors
if ($httpCode === 403) {
throw new RuntimeException("403 Forbidden - Credentials not recognized");
}
if ($httpCode === 500) {
throw new RuntimeException("500 Internal Server Error - ServerSnug service issue");
}
if ($httpCode !== 200) {
throw new RuntimeException("Unexpected HTTP status: $httpCode");
}
// Check for API-level error message (returned with 200)
if (isset($data['error'])) {
$errorMsg = $data['error'];
if ($errorMsg === "Allowance exceeded") {
throw new RuntimeException("Allowance exceeded - fewer results returned than requested");
}
if ($errorMsg === "Malformed request") {
throw new InvalidArgumentException("Malformed request - check JSON format");
}
if ($errorMsg === "Missing updates") {
throw new InvalidArgumentException("Missing 'updates' field in request");
}
throw new RuntimeException("API error: $errorMsg");
}
// Validate expected success structure
if (!isset($data['count']) || !isset($data['response'])) {
throw new RuntimeException("Response missing required fields ('count' or 'response')");
}
return $data;
}
?>
/**
* Performs a ServerSnug 'update' request with full error handling (Node.js equivalent).
* Requires Node.js v18+ (native fetch). Returns the parsed JSON response on success.
* Throws detailed errors for any failure — identical logic to the Python/PHP versions.
*/
async function performUpdateRequest(accessId, accessCode, updates, timeout = 30) {
// === Input validation ===
if (!accessId || typeof accessId !== 'string') {
throw new TypeError("accessId must be a non-empty string");
}
if (!accessCode || typeof accessCode !== 'string') {
throw new TypeError("accessCode must be a non-empty string");
}
if (typeof updates !== 'number' || !Number.isInteger(updates) || updates < 1 || updates > 50) {
throw new RangeError("updates must be an integer between 1 and 50 (hard limit)");
}
const url = "https://www.serversnug.com/api/public/v1/update/";
const payload = {
access_id: accessId,
access_code: accessCode,
updates: updates
};
try {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify(payload),
signal: AbortSignal.timeout(timeout * 1000) // Clean timeout handling
});
let data;
try {
data = await response.json();
} catch (jsonError) {
throw new Error(`Invalid JSON response from server. Status code: ${response.status}`);
}
// === HTTP status code handling ===
if (response.status === 403) {
throw new Error("403 Forbidden - Credentials not recognized");
}
if ([500, 502].includes(response.status)) {
throw new Error(`${response.status} Internal Server Error - ServerSnug service issue`);
}
if (response.status === 503) {
throw new Error("503 Service Unavailable - ServerSnug under maintenance");
}
if (response.status === 422) {
const errorMsg = data?.error || "Unknown error";
throw new Error(`422 Processing Error: ${errorMsg}`);
}
if (response.status !== 200) {
throw new Error(`Unexpected HTTP status: ${response.status}`);
}
// === API-level error messages (even with HTTP 200) ===
if (data?.error) {
const errorMsg = data.error;
if (errorMsg === "Allowance exceeded") {
throw new Error("Allowance exceeded - fewer results returned than requested");
}
if (errorMsg === "Malformed request") {
throw new TypeError("Malformed request - check JSON format");
}
throw new Error(`API error: ${errorMsg}`);
}
// === Validate expected response structure ===
if (typeof data?.count === 'undefined' || typeof data?.response === 'undefined') {
throw new Error("Response missing required fields ('count' or 'response')");
}
return data;
} catch (error) {
// Handle timeout specifically
if (error.name === 'TimeoutError' || error.name === 'AbortError') {
throw new Error("Request timed out while contacting ServerSnug API");
}
// Handle network/connection errors
if (error instanceof TypeError && error.message.includes('fetch')) {
throw new Error("Failed to connect to ServerSnug API (network issue)");
}
// Re-throw all other errors (our custom ones)
throw error;
}
}
export { performUpdateRequest };