rest-graphql-debug
Debug REST/GraphQL APIs: status codes, auth, schemas, repro.
Debug REST/GraphQL APIs: status codes, auth, schemas, repro.
Real data. Real impact.
Emerging
Developers
Per week
Excellent
Skills give you superpowers. Install in 30 seconds.
Drive REST and GraphQL diagnosis through Hermes tools —
terminal for curl, execute_code for Python requests, web_extract for vendor docs. Isolate the failing layer before guessing at the fix.
Skip for UI rendering, DB query tuning, or DNS/firewall infra (escalate).
Isolate the layer, then fix. A 200 OK can hide broken data. A 500 can mask a one-character auth typo. Walk the chain in order; never skip a step.
1. Connectivity → can we reach the host at all? 1.5 Timeouts → connect-slow vs read-slow? 2. TLS/SSL → cert valid and trusted? 3. Auth → credentials correct and unexpired? 4. Request format → payload shape match server expectations? 5. Response parse → does our code accept what came back? 6. Semantics → does the data mean what we assume?
# Verbose request/response exchange terminal('curl -v https://api.example.com/users/1') # POST with JSON terminal("""curl -X POST https://api.example.com/users \\ -H 'Content-Type: application/json' \\ -H "Authorization: Bearer $TOKEN" \\ -d '{"name":"test","email":"test@example.com"}'""") # Headers only terminal('curl -sI https://api.example.com/health') # Pretty-print JSON terminal('curl -s https://api.example.com/users | python3 -m json.tool')
terminal("""curl -X POST https://api.example.com/graphql \\ -H 'Content-Type: application/json' \\ -H "Authorization: Bearer $TOKEN" \\ -d '{"query":"{ user(id: 1) { name email } }"}'""")
GraphQL gotcha: servers often return HTTP 200 even when the query failed. Always inspect the
errors field regardless of status code:
execute_code(''' import os, requests resp = requests.post( "https://api.example.com/graphql", json={"query": "{ user(id: 1) { name email } }"}, headers={"Authorization": f"Bearer {os.environ['TOKEN']}"}, timeout=10, ) data = resp.json() if data.get("errors"): for err in data["errors"]: print(f"GraphQL error: {err['message']} (path: {err.get('path')})") print(data.get("data")) ''')
execute_code(''' import requests resp = requests.get( "https://api.example.com/users/1", headers={"Authorization": "Bearer <TOKEN>"}, timeout=(3.05, 30), # (connect, read) ) print(resp.status_code, dict(resp.headers)) print(resp.text[:500]) ''')
terminal('nslookup api.example.com') terminal('curl -v --connect-timeout 5 https://api.example.com/health')
Failures: DNS not resolving, firewall, VPN required, proxy missing.
Distinguish can't reach from reaches but slow:
terminal('''curl -w "dns:%{time_namelookup}s connect:%{time_connect}s tls:%{time_appconnect}s ttfb:%{time_starttransfer}s total:%{time_total}s\\n" \\ -o /dev/null -s https://api.example.com/endpoint''')
In Python, always pass a tuple timeout —
requests has no default and will hang forever:
execute_code(''' import requests from requests.exceptions import ConnectTimeout, ReadTimeout try: requests.get(url, timeout=(3.05, 30)) except ConnectTimeout: print("Cannot reach host — DNS, firewall, VPN") except ReadTimeout: print("Connected but server is slow") ''')
Diagnosis: high
time_connect is network/firewall; high time_starttransfer with low time_connect is a slow server.
terminal('curl -vI https://api.example.com 2>&1 | grep -E "SSL|subject|expire|issuer"')
Failures: expired cert, self-signed, hostname mismatch, missing CA bundle. Use
-k only for ad-hoc debug, never in code.
# Token validity check terminal('curl -s -o /dev/null -w "%{http_code}\\n" -H "Authorization: Bearer $TOKEN" https://api.example.com/me') # Decode JWT exp claim — handles base64url padding correctly execute_code(''' import json, base64, os tok = os.environ["TOKEN"] payload = tok.split(".")[1] payload += "=" * (-len(payload) % 4) print(json.dumps(json.loads(base64.urlsafe_b64decode(payload)), indent=2)) ''')
Checklist:
exp claim in JWT)X-Api-Key?api_key=…)?terminal("""curl -v -X POST https://api.example.com/endpoint \\ -H 'Content-Type: application/json' \\ -d '{"key":"value"}' 2>&1""")
Content-Type / body mismatch — the silent 415/400:
# WRONG — data= sends form-encoded, header lies requests.post(url, data='{"k":"v"}', headers={"Content-Type": "application/json"}) # RIGHT — json= auto-sets header AND serializes requests.post(url, json={"k": "v"}) # WRONG — Accept says XML, code calls .json() requests.get(url, headers={"Accept": "text/xml"}) # RIGHT — let requests build multipart with boundary requests.post(url, files={"file": open("doc.pdf", "rb")})
Common: form-encoded vs JSON, missing required fields, wrong HTTP method, unencoded query params.
Always inspect content-type before calling
.json():
execute_code(''' import requests resp = requests.post(url, json=payload, timeout=10) print(f"status={resp.status_code}") print(f"headers={dict(resp.headers)}") ct = resp.headers.get("Content-Type", "") if "application/json" in ct: print(resp.json()) else: print(f"unexpected content-type {ct!r}, body={resp.text[:500]!r}") ''')
Failures: HTML error page where JSON expected, empty body, wrong charset.
Parsed cleanly — but is the data correct?
"status": "active" mean what your code thinks?Authorization header actually present? (curl -v to confirm)Bearer vs Basic vs Token)?api_key=…) instead of header.Access-Control-Allow-Origin)/v1/ vs /v2/)?ETag / If-Match?The error body usually names the bad fields. Check:
Check
Retry-After and X-RateLimit-* headers. Exponential backoff:
execute_code(''' import time, requests def with_backoff(method, url, **kwargs): for attempt in range(5): resp = requests.request(method, url, **kwargs) if resp.status_code != 429: return resp wait = int(resp.headers.get("Retry-After", 2 ** attempt)) time.sleep(wait) return resp ''')
For all 5xx: backoff with jitter, alert on persistence.
Pagination. Verify you're getting all results. Look for
next_cursor, next_page, total_count. Two patterns:
?limit=100&offset=200) — simple, can skip items if data shifts.?cursor=abc123) — preferred for live or large datasets.Idempotency. For non-idempotent operations (POST), send
Idempotency-Key: <uuid> so retries don't double-charge / double-create. Mandatory for payments and orders.
Catch schema drift before it hits production:
execute_code(''' import requests def validate_user(data: dict) -> list[str]: errors = [] required = {"id": int, "email": str, "created_at": str} for field, expected in required.items(): if field not in data: errors.append(f"missing field: {field}") elif not isinstance(data[field], expected): errors.append(f"{field}: want {expected.__name__}, got {type(data[field]).__name__}") return errors resp = requests.get(f"{BASE}/users/1", headers=HEADERS, timeout=10) issues = validate_user(resp.json()) if issues: print(f"contract violations: {issues}") ''')
Run after API upgrades, when integrating new third parties, or in CI smoke tests.
Always capture the provider's request ID — fastest path to vendor support:
execute_code(''' import requests resp = requests.post(url, json=payload, headers=headers, timeout=10) request_id = ( resp.headers.get("X-Request-Id") or resp.headers.get("X-Trace-Id") or resp.headers.get("CF-Ray") # Cloudflare ) if resp.status_code >= 400: print(f"failed status={resp.status_code} req_id={request_id} ts={resp.headers.get('Date')}") ''')
Vendor bug-report template:
Endpoint: POST /api/v1/orders Request ID: req_abc123xyz Timestamp: 2026-03-17T14:30:00Z Status: 500 Expected: 201 with order object Actual: 500 {"error":"internal server error"} Repro: curl -X POST … (auth: <REDACTED>)
Drop this into
tests/ and run via terminal('pytest tests/test_api_smoke.py -v'):
import os, requests, pytest BASE_URL = os.environ.get("API_BASE_URL", "https://api.example.com") TOKEN = os.environ.get("API_TOKEN", "") HEADERS = {"Authorization": f"Bearer {TOKEN}"} class TestAPISmoke: def test_health(self): resp = requests.get(f"{BASE_URL}/health", timeout=5) assert resp.status_code == 200 def test_list_users_returns_array(self): resp = requests.get(f"{BASE_URL}/users", headers=HEADERS, timeout=10) assert resp.status_code == 200 data = resp.json() assert isinstance(data.get("data", data), list) def test_get_user_required_fields(self): resp = requests.get(f"{BASE_URL}/users/1", headers=HEADERS, timeout=10) assert resp.status_code in (200, 404) if resp.status_code == 200: user = resp.json() assert "id" in user and "email" in user def test_invalid_auth_returns_401(self): resp = requests.get( f"{BASE_URL}/users", headers={"Authorization": "Bearer invalid-token"}, timeout=10, ) assert resp.status_code == 401
Bearer <REDACTED>.os.environ["API_TOKEN"]) or ~/.hermes/.env.def redact_auth(headers: dict) -> dict: sensitive = {"authorization", "x-api-key", "cookie", "set-cookie"} return {k: ("<REDACTED>" if k.lower() in sensitive else v) for k, v in headers.items()}
404 on /users/123 shouldn't reveal whether the user exists (enumeration).10.x.x.x, internal-api.corp.local in error bodies.Server / X-Powered-By. Stack-info leaks. Note for security review.terminal('curl -sI https://api.example.com') terminal('openssl s_client -connect api.example.com:443 -servername api.example.com </dev/null 2>/dev/null | openssl x509 -noout -dates')
When debugging spans auth → fetch → paginate → validate, use
execute_code. Variables persist for the script, results print to stdout, no risk of token spam in your context:
execute_code(''' import os, requests token = os.environ["API_TOKEN"] base = "https://api.example.com" H = {"Authorization": f"Bearer {token}"} # 1. auth me = requests.get(f"{base}/me", headers=H, timeout=10) print(f"auth {me.status_code}") # 2. paginate all_users, cursor = [], None while True: params = {"cursor": cursor} if cursor else {} r = requests.get(f"{base}/users", headers=H, params=params, timeout=10) body = r.json() all_users.extend(body["data"]) cursor = body.get("next_cursor") if not cursor: break print(f"users={len(all_users)}") ''')
Pull the spec for the endpoint you're debugging instead of guessing:
web_extract(urls=["https://docs.example.com/api/v1/users"])
delegate_task( goal="Test all CRUD endpoints for /api/v1/users", context=""" Follow the rest-graphql-debug skill (optional-skills/software-development/rest-graphql-debug). Base URL: https://api.example.com Auth: Bearer token from API_TOKEN env var. For each verb (POST, GET, PATCH, DELETE): - happy path: assert status + response schema - error cases: 400, 404, 422 - log a repro curl for any failure (redact tokens) Output: pass/fail per endpoint + correlation IDs for failures. """, toolsets=["terminal", "file"], )
When reporting findings:
## Finding Endpoint: POST /api/v1/users Status: 422 Unprocessable Entity Req ID: req_abc123xyz ## Repro curl -X POST https://api.example.com/api/v1/users \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer <REDACTED>' \ -d '{"name":"test"}' ## Root Cause Missing required field `email`. Server validation rejects before processing. ## Fix -d '{"name":"test","email":"test@example.com"}'
systematic-debugging — once the failing API layer is isolated, root-cause your codetest-driven-development — write the regression test before shipping the fixMIT
mkdir -p ~/.hermes/skills/software-development/rest-graphql-debug && curl -o ~/.hermes/skills/software-development/rest-graphql-debug/SKILL.md https://raw.githubusercontent.com/NousResearch/hermes-agent/main/optional-skills/software-development/rest-graphql-debug/SKILL.md1,500+ AI skills, agents & workflows. Install in 30 seconds. Part of the Torly.ai family.
© 2026 Torly.ai. All rights reserved.