跳到主要內容

Rest Graphql Debug

調試 REST/GraphQL API:狀態碼、身份驗證、架構、復現。

技能元數據

來源可選 — 使用 hermes skills install official/software-development/rest-graphql-debug 安裝
路徑optional-skills/software-development/rest-graphql-debug
版本1.2.0
作者eren-karakus0
許可證MIT
標籤api, rest, graphql, http, debugging, testing, curl, integration
相關技能systematic-debugging, test-driven-development

參考:完整 SKILL.md

信息

以下是 Hermes 在觸發此技能時加載的完整技能定義。這是技能激活時代理看到的指令。

API 測試與調試

通過 Hermes 工具驅動 REST 和 GraphQL 診斷 — 使用 terminal 執行 curl,使用 execute_code 執行 Python requests,使用 web_extract 獲取供應商文檔。在猜測修復方案之前,先隔離出故障層。

何時使用

  • API 返回意外的狀態碼或響應體
  • 身份驗證失敗(刷新令牌後出現 401/403、OAuth、API 密鑰)
  • 在 Postman 中有效但在代碼中失敗
  • Webhook / 回調集成調試
  • 構建或審查 API 集成測試
  • 速率限制或分頁問題

如果是 UI 渲染、數據庫查詢調優或 DNS/防火牆基礎設施問題,請跳過(升級處理)。

核心原則

隔離層級,然後修復。 200 OK 可能隱藏了損壞的數據。500 錯誤可能掩蓋了一個字符的身份驗證拼寫錯誤。按順序檢查整個鏈路;切勿跳過任何步驟。

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?

5 分鐘快速入門

通過終端使用 REST

# 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')

通過終端使用 GraphQL

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 陷阱: 即使查詢失敗,服務器通常也會返回 HTTP 200。無論狀態碼如何,務必檢查 errors 字段:

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 使用 Python (requests)

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])
''')

分層調試流程

第 1 步 — 連通性

terminal('nslookup api.example.com')
terminal('curl -v --connect-timeout 5 https://api.example.com/health')

故障原因:DNS 無法解析、防火牆、需要 VPN、缺少代理。

第 1.5 步 — 超時

區分無法到達能到達但緩慢

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''')

在 Python 中,始終傳遞元組形式的超時參數 — requests 沒有默認值,否則會無限掛起:

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")
''')

診斷:高 time_connect 表示網絡/防火牆問題;低 time_connect 但高 time_starttransfer 表示服務器響應緩慢。

第 2 步 — TLS/SSL

terminal('curl -vI https://api.example.com 2>&1 | grep -E "SSL|subject|expire|issuer"')

故障原因:證書過期、自簽名證書、主機名不匹配、缺少 CA bundle。僅在臨時調試時使用 -k,切勿在生產代碼中使用。

第 3 步 — 身份驗證

# 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))
''')

檢查清單:

  • 令牌是否過期?(JWT 中的 exp 聲明)
  • 方案是否正確?Bearer vs Basic vs Token vs X-Api-Key
  • 環境是否正確?在生產環境使用 staging 密鑰是常見錯誤
  • API 密鑰是在請求頭中還是查詢參數中(?api_key=…)?

第 4 步 — 請求格式

terminal("""curl -v -X POST https://api.example.com/endpoint \\
-H 'Content-Type: application/json' \\
-d '{"key":"value"}' 2>&1""")

Content-Type / 請求體不匹配 — 靜默的 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")})

常見問題:表單編碼與 JSON 混淆、缺少必填字段、HTTP 方法錯誤、查詢參數未編碼。

第 5 步 — 響應解析

在調用 .json() 之前始終檢查 content-type:

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}")
''')

故障原因:預期為 JSON 卻收到 HTML 錯誤頁面、空響應體、字符集錯誤。

第 6 步 — 語義驗證

解析成功 — 但數據正確嗎?

  • "status": "active" 的含義是否與你的代碼預期一致?
  • 響應中的 ID 是否與請求的 ID 匹配?
  • 時間戳是否在預期的時區?
  • 分頁是否返回了所有結果,還是僅返回了第 1 頁?

HTTP 狀態碼速查表

401 Unauthorized — 憑據缺失或無效

  1. Authorization 請求頭確實存在嗎?(使用 curl -v 確認)
  2. 令牌是否正確且未過期?
  3. 身份驗證方案是否正確?(Bearer vs Basic vs Token
  4. 某些 API 使用查詢參數(?api_key=…)而非請求頭。

403 Forbidden — 已認證但未授權

  1. 令牌是否具有所需的作用域/權限?
  2. 資源是否屬於其他賬戶?
  3. IP 白名單是否阻止了你?
  4. 瀏覽器中的 CORS 問題?(檢查 Access-Control-Allow-Origin

404 Not Found — 資源不存在或 URL 錯誤

  1. 路徑是否正確?(尾部斜槓、拼寫錯誤、版本前綴)
  2. 資源 ID 是否存在?
  3. API 版本是否正確(/v1/ vs /v2/)?
  4. 基礎 URL 是否正確(staging vs prod)?

409 Conflict — 狀態衝突

  1. 資源已存在(重複創建)?
  2. ETag / If-Match 過時?
  3. 另一個進程併發修改?

422 Unprocessable Entity — JSON 有效,數據無效

錯誤響應體通常會指出有問題的字段。檢查:

  • 字段類型(字符串 vs 整數、日期格式)
  • 必填 vs 選填
  • 枚舉值是否在允許集合內

429 Too Many Requests — 速率限制

檢查 Retry-AfterX-RateLimit-* 請求頭。指數退避:

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
''')

5xx — 服務器端錯誤,通常不是你的錯

  • 500 — 服務器端 bug。捕獲關聯 ID,並向服務提供商提交工單。
  • 502 — 上游服務不可用。實施退避 + 重試。
  • 503 — 過載 / 維護中。檢查狀態頁面。
  • 504 — 上游超時。減少負載或增加超時時間。

對於所有 5xx 錯誤:使用帶抖動的退避策略,若問題持續則發出警報。

分頁與冪等性

分頁。 驗證是否獲取了所有結果。查找 next_cursornext_pagetotal_count。兩種模式:

  • 偏移量(?limit=100&offset=200)— 簡單,但如果數據發生變動可能會跳過項目。
  • 遊標(?cursor=abc123)— 對於實時或大型數據集首選此方式。

冪等性。 對於非冪等操作(POST),發送 Idempotency-Key: <uuid>,以確保重試不會導致重複扣費或重複創建。對於支付和訂單操作,這是強制要求的。

契約驗證

在生產環境受到影響之前捕獲架構漂移:

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}")
''')

在 API 升級後、集成新的第三方服務時,或在 CI 冒煙測試中運行。

關聯 ID

始終捕獲服務提供商的請求 ID — 這是獲得廠商支持的最快途徑:

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')}")
''')

廠商 bug 報告模板:

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>)

迴歸測試模板

將此文件放入 tests/ 目錄,並通過 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

安全性

Token 處理

  • 切勿記錄完整的 token。進行脫敏處理:Bearer <REDACTED>
  • 切勿在腳本中硬編碼 token。從環境變量(os.environ["API_TOKEN"])或 ~/.hermes/.env 文件中讀取。
  • 如果 token 出現在日誌、錯誤消息或 git 歷史記錄中,請立即輪換。

安全日誌記錄

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()}

洩露檢查清單

  • URL 中的憑證。 查詢字符串中的 API 密鑰會出現在服務器日誌、瀏覽器歷史記錄、Referer 頭中 — 請使用請求頭。
  • 錯誤響應中的個人身份信息 (PII)。 /users/123 上的 404 不應揭示用戶是否存在(枚舉攻擊)。
  • 生產環境中的堆棧跟蹤。 500 錯誤不應洩露文件路徑、框架版本。
  • 內部主機名/IP。 錯誤主體中包含 10.x.x.xinternal-api.corp.local
  • Token 回顯。 某些 API 會在錯誤詳情中包含認證 token。請驗證它們沒有這樣做。
  • 詳細的 Server / X-Powered-By 頭。 這會洩露技術棧信息。需在安全審查中註明。

Hermes 工具模式

terminal — 用於 curl, dig, openssl

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')

execute_code — 用於多步驟 Python 流程

當調試跨越 auth → fetch → paginate → validate 的流程時,使用 execute_code。變量在腳本執行期間持久存在,結果打印到 stdout,且上下文中不會出現 token 垃圾信息的風險:

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)}")
''')

web_extract — 用於廠商 API 文檔

提取正在調試的端點的規範,而不是猜測:

web_extract(urls=["https://docs.example.com/api/v1/users"])

delegate_task — 用於完整的 CRUD 測試掃描

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"],
)

輸出格式

報告發現時:

## 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 — 一旦隔離出失敗的 API 層,即可對代碼進行根本原因分析
  • test-driven-development — 在發佈修復程序之前編寫回歸測試