Перейти к основному содержимому

Rest Graphql Debug

Отладка REST/GraphQL API: коды статусов, аутентификация, схемы, воспроизведение.

Метаданные навыка​

ИсточникОпционально — установка с помощью vibeos 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​

к сведению

Ниже приведено полное описание навыка, которое VibeOS загружает при его активации. Это те инструкции, которые видит агент, когда навык активен.

Тестирование и отладка API

Управляйте диагностикой REST и GraphQL с помощью инструментов VibeOS — terminal для curl, execute_code для Python requests, web_extract для документации вендора. Изолируйте проблемный слой, прежде чем гадать, как исправить.

Когда использовать​

  • API возвращает неожиданный статус или тело ответа
  • Ошибка аутентификации (401/403 после обновления токена, OAuth, API-ключ)
  • Работает в Postman, но не работает в коде
  • Отладка вебхуков / обратных вызовов
  • Создание или проверка интеграционных тестов API
  • Проблемы с ограничением скорости или пагинацией

Пропустите для рендеринга UI, настройки запросов к БД или инфраструктурных проблем (DNS/файрвол) — передайте выше.

Основной принцип​

Изолируйте слой, затем исправляйте. Статус 200 OK может скрывать неверные данные. Статус 500 может маскировать опечатку в одном символе аутентификации. Проходите цепочку по порядку; никогда не пропускайте шаги.

1. Связность       → можем ли мы вообще достичь хоста?
1.5 Таймауты → медленное соединение или медленное чтение?
2. TLS/SSL → сертификат действителен и доверенный?
3. Аутентификация → учетные данные верны и не истекли?
4. Формат запроса → соответствует ли форма полезной нагрузки ожиданиям сервера?
5. Разбор ответа → принимает ли наш код то, что пришло?
6. Семантика → означают ли данные то, что мы предполагаем?

Быстрый старт на 5 минут​

REST через terminal​

# Подробный обмен запросом/ответом
terminal('curl -v https://api.example.com/users/1')

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

# Только заголовки
terminal('curl -sI https://api.example.com/health')

# Красивый вывод JSON
terminal('curl -s https://api.example.com/users | python3 -m json.tool')

GraphQL через terminal​

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

Python (requests) через execute_code​

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_starttransfer при низком time_connect — медленный сервер.

Шаг 2 — TLS/SSL​

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

Неудачи: истекший сертификат, самоподписанный, несоответствие имени хоста, отсутствие CA-связки. Используйте -k только для разовой отладки, никогда в коде.

Шаг 3 — Аутентификация​

# Проверка действительности токена
terminal('curl -s -o /dev/null -w "%{http_code}\\n" -H "Authorization: Bearer $TOKEN" https://api.example.com/me')

# Декодирование утверждения exp JWT — корректно обрабатывает отступы base64url
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))
''')

Контрольный список:

  • Срок действия токена истек? (утверждение exp в JWT)
  • Правильная схема? 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:

# НЕПРАВИЛЬНО — data= отправляет form-encoded, заголовок лжет
requests.post(url, data='{"k":"v"}', headers={"Content-Type": "application/json"})

# ПРАВИЛЬНО — json= автоматически устанавливает заголовок И сериализует
requests.post(url, json={"k": "v"})

# НЕПРАВИЛЬНО — Accept говорит XML, код вызывает .json()
requests.get(url, headers={"Accept": "text/xml"})

# ПРАВИЛЬНО — позвольте requests построить multipart с границей
requests.post(url, files={"file": open("doc.pdf", "rb")})

Распространенные ошибки: form-encoded вместо JSON, отсутствие обязательных полей, неверный HTTP-метод, незакодированные параметры запроса.

Шаг 5 — Разбор ответа​

Всегда проверяйте content-type перед вызовом .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}")
''')

Неудачи: HTML-страница ошибки вместо ожидаемого JSON, пустое тело, неверная кодировка.

Шаг 6 — Семантическая проверка​

Распарсено чисто — но корректны ли данные?

  • Означает ли "status": "active" то, что думает ваш код?
  • ID в ответе соответствует запрошенному?
  • Метки времени в ожидаемом часовом поясе?
  • Пагинация возвращает все результаты или только первую страницу?

Справочник по 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-After и X-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 — баг сервера. Захватите correlation ID, сообщите провайдеру.
  • 502 — вышестоящий сервис недоступен. Задержка + повтор.
  • 503 — перегрузка / обслуживание. Проверьте страницу статуса.
  • 504 — таймаут вышестоящего сервиса. Уменьшите полезную нагрузку или увеличьте таймаут.

Для всех 5xx: задержка с джиттером, оповещение при сохранении.

Пагинация и идемпотентность​

Пагинация. Убедитесь, что получаете все результаты. Ищите next_cursor, next_page, total_count. Два паттерна:

  • Смещение (?limit=100&offset=200) — просто, может пропустить элементы при сдвиге данных.
  • Курсор (?cursor=abc123) — предпочтительно для живых или больших наборов данных.

Идемпотентность. Для неидемпотентных операций (POST) отправляйте Idempotency-Key: &lt;uuid&gt;, чтобы повторные попытки не привели к двойному списанию / двойному созданию. Обязательно для платежей и заказов.

Проверка контракта​

Ловите расхождение схем до того, как оно попадет в продакшн:

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-дымовых тестах.

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

Шаблон баг-репорта вендору:

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

Безопасность​

Обработка токенов​

  • Никогда не логируйте полные токены. Маскируйте: Bearer &lt;REDACTED&gt;.
  • Никогда не жестко кодируйте токены в скриптах. Читайте из окружения (os.environ["API_TOKEN"]) или ${VIBEOS_HOME:-~/.vibeos}/.env.
  • Немедленно ротируйте, если токен появился в логах, сообщениях об ошибках или истории 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-ключи в строке запроса попадают в серверные логи, историю браузера, заголовки referrer — используйте заголовки.
  • PII в ответах об ошибках. 404 on /users/123 не должен раскрывать, существует ли пользователь (перебор).
  • Стек-трейсы в проде. 500-е ошибки не должны раскрывать пути к файлам, версии фреймворков.
  • Внутренние имена хостов/IP. 10.x.x.x, internal-api.corp.local в телах ошибок.
  • Токены в ответе. Некоторые API включают токен аутентификации в детали ошибки. Убедитесь, что нет.
  • Подробные Server / X-Powered-By. Утечка информации о стеке. Отметьте для проверки безопасности.

Паттерны инструментов VibeOS​

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-потоков​

Когда отладка включает аутентификацию → получение → пагинацию → проверку, используйте execute_code. Переменные сохраняются для скрипта, результаты выводятся в stdout, нет риска спама токенами в вашем контексте:

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 — напишите регрессионный тест перед отправкой исправления