Błędy API /v1¶
Każda odpowiedź błędu z /v1/* ma ten sam kształt (api.contracts.ErrorResponse, #46, ADR 0015):
{"error": {"code": "unknown_rule", "message": "Nieznane reguły: NOPE",
"details": {"rules": ["NOPE"]}, "request_id": "6cb511c42fb241ec9e04334766829427"}}
code— stały, maszynowy identyfikator (poniżej). Klient rozgałęzia się pocode, nie pomessage.message— opis dla człowieka (po polsku), maks. 300 znaków; może się zmieniać bez zmiany wersji.details— opcjonalne dane strukturalne (np. lista błędów walidacji),nullgdy brak.request_id— ten sam co w nagłówkuX-Request-ID; podaj go przy zgłaszaniu problemu.
Każda odpowiedź /v1 (także sukces i błąd 500) ma nagłówki X-Request-ID i X-API-Version: v1. Klient może wysłać
własny X-Request-ID ([A-Za-z0-9._-]{1,128}) — zostanie użyty zamiast wygenerowanego.
Katalog kodów¶
| HTTP | code |
Kiedy |
|---|---|---|
| 400 | unknown_rule |
rules zawiera id spoza katalogu; details.rules = nieznane id |
| 400 | unknown_profile |
nieznany profile (tylko nadpisanie superadmina; profile od innych jest ignorowany, #314) |
| 400 | rule_disabled |
rules zawiera reguły wyłączone w katalogu; details.rules = wyłączone id |
| 400 | invalid_webhook_url |
webhook_url nie https, literał IP, localhost/host wewnętrzny albo z danymi logowania (#51) |
| 400 | bad_request |
inny błąd żądania |
| 400 | invalid_url |
url strony odrzucony przed analizą (#111): literał IP, localhost, .local/.internal, dane logowania, schemat inny niż http(s), port inny niż 80/443; details = {url} |
| 401 | unauthorized |
brak logowania (JWT Authorization: Bearer, klucz API Bearer gsj_... albo cookie sesji, #48/#50) |
| 400 | invalid_key_expiry |
expires_at w przeszłości albo dalej niż rok, albo never_expires dla klucza innego niż klucz organizacji / przez kogoś spoza owner/admin (#125) |
| 401 | invalid_api_key |
klucz gsj_... nieznany, błędny sekret lub suma kontrolna, odwołany (także po okresie łaski rotacji albo po zgłoszeniu wycieku), jego projekt usunięty albo klucz użytkownika, który opuścił organizację (#50, #125) |
| 401 | api_key_expired |
klucz po expires_at; utwórz albo zrotuj klucz (#125) |
| 403 | csrf_failed |
żądanie z cookie sesji bez zgodnego Origin/Referer |
| 403 | forbidden |
rola za niska (require_role), brak uprawnień w projekcie albo operacja tylko dla użytkownika (np. zarządzanie kluczami) |
| 403 | ip_not_allowed |
klucz ma allowlistę IP, a adres klienta jest spoza niej (#125) |
| 403 | insufficient_scope |
klucz API bez wymaganego zakresu (judge, reports:read, jobs, projects:read, #50) |
| 403 | org_suspended |
bieżąca organizacja (z X-Org-ID, jedyny org konta albo org projektu klucza API) jest zawieszona (#124) |
| 404 | job_not_found |
GET /v1/jobs/{id} — brak zadania w projektach wywołującego (#51) |
| 404 | audit_not_found |
GET /v1/audits/{id} — brak audytu w projektach wywołującego (#87) |
| 404 | intent_not_found |
GET /v1/intent/{id} i eksporty - brak raportu intencji lub wsadu w projektach wywołującego (#194) |
| 422 | tier_unavailable |
POST /v1/intent* - poziom Intent jeszcze niedostępny (#194) |
| 422 | invalid_csv |
POST /v1/intent/batch/csv - CSV bez fraz albo wiersz z błędnym rynkiem (#194) |
| 403 | private_project |
POST /v1/intent/{id}/shares - raportu intencji w projekcie prywatnym nie można udostępnić publicznie (#194) |
| 503 | pdf_unavailable |
GET /v1/intent/{id}/export.pdf - PDF niedostępny na tym serwerze (#194) |
| 404 | share_not_found |
DELETE /v1/share-links/{id} (także per raport/audyt) — brak aktywnego linku o tym id w organizacji wywołującego (#126) |
| 404 | org_not_found |
X-Org-ID wskazuje nieznaną organizację, org bez członkostwa wywołującego albo inny org niż projekt klucza API (#124) |
| 409 | upgrade_unavailable |
POST /v1/reports/{id}/upgrade, /v1/audits/{id}/upgrade (#192): nie ma czego dodać; details.reason = nothing_to_add | busy | private_mode | no_pages, details.estimate |
| 413 | audit_too_large |
POST /v1/audits/site — pages powyżej GSPAMJUDGE_AUDITS_MAX_PAGES (#87) |
| 422 | sitemap_not_found |
POST /v1/audits/site — brak URL-i w sitemap witryny (#87) |
| 422 | invalid_scope |
/v1/audits/site* — błędna reguła include/exclude (tylko glob) (#119) |
| 422 | empty_selection |
POST /v1/audits/site — po include/exclude nie zostały żadne URL-e (#119) |
| 422 | preview_expired |
/v1/audits/site* — preview_id wygasł, nieznany albo innego użytkownika/projektu, brak źródła (#119) |
| 404 | report_not_found |
GET /v1/reports/{id} — brak raportu; także id spoza formatu publicznego (rep_…), np. stare liczbowe (#142) |
| 404 | not_found |
nieistniejąca ścieżka |
| 405 | method_not_allowed |
zła metoda HTTP |
| 409 | key_not_active |
POST /v1/keys/{id}/rotate dla klucza odwołanego, wygasłego albo już zrotowanego (#125) |
| 409 | conflict |
zasób już istnieje, np. POST /v1/invites dla e-maila z kontem albo aktywnym zaproszeniem (#89) |
| 413 | batch_too_large |
POST /v1/judge/batch ponad GSPAMJUDGE_BATCH_MAX stron — użyj /v1/jobs |
| 413 | job_too_large |
POST /v1/jobs ponad GSPAMJUDGE_JOBS_MAX pozycji (#51) |
| 413 | item_too_large |
POST /v1/jobs: html/text ponad GSPAMJUDGE_JOBS_ITEM_MAX_CHARS na pozycję albo GSPAMJUDGE_JOBS_MAX_CHARS na zadanie |
| 413 | payload_too_large |
inne zbyt duże żądanie |
| 422 | validation_error |
ciało/parametry niezgodne ze schematem; details[] = {loc, msg, type} |
| 400 | registration_failed |
odmowa rejestracji (#387); od #400 POST /v1/auth/register już go nie zwraca |
POST /v1/auth/register (#400) zawsze odpowiada 202 {status: "pending", message} z tą samą treścią, niezależnie od tego, czy e-mail ma konto - status nie zdradza istniejących kont. Konto staje się aktywne dopiero po linku z e-maila (POST /v1/auth/confirm {token} albo strona panelu /confirm/{token}); istniejące konto dostaje zamiast tego mail informacyjny. Jedynie hasło niezgodne z zasadami daje 400 invalid_password (sprawdzane tak samo dla każdego e-maila). Niepotwierdzone konto nie zaloguje się (invalid_credentials).
| HTTP | code |
Kiedy |
|---|---|---|
| 400 | email_unavailable |
PATCH /v1/users/me: nie można użyć nowego e-maila (#387) |
| 400 | invalid_password |
hasło nie spełnia zasad: min. 8 znaków, bez nazwy z e-maila (#387) |
| 400 | invalid_credentials |
nieudane logowanie: zły e-mail lub hasło (#387) |
| 400 | account_not_verified |
logowanie wymaga potwierdzonego e-maila (#387) |
| 400 | invalid_token |
link resetu hasła lub potwierdzenia e-maila nieprawidłowy albo wygasł (#387, POST /v1/auth/confirm #400) |
| 429 | rate_limited |
przekroczony limit zapytań klucza API (#50); nagłówki Retry-After, RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset |
| 429 | too_many_active_jobs |
projekt ma już GSPAMJUDGE_JOBS_MAX_ACTIVE zadań queued/running; Retry-After |
| 503 | spend_cap_reached |
osiągnięty globalny dzienny limit kosztów (#94, GSPAMJUDGE_DAILY_SPEND_CAP_USD, doba UTC); details = {day, spent_usd, cap_usd}, Retry-After: 3600; reset o 00:00 UTC |
| 429 | quota_exceeded |
osiągnięty limit wydatków (#89, #117, #221): użytkownika, projektu albo organizacji (pula miesięczna, potem saldo PAYG), dzienny albo miesięczny (UTC); kwoty w USD, komunikat np. „Miesięczny limit $20.00 dla tej organizacji wyczerpany…”; details = {subject, period, unit: "usd", limit_usd, used_usd, requested_usd, remaining_usd} (+ balance_usd dla org); wsad musi zmieścić się w całości. Zużycie: GET /v1/usage, zob. Rozliczenia |
| 500 | internal_error |
nieoczekiwany błąd serwera; message ogólny (patrz niżej) |
| 502 | fetch_failed |
nie udało się pobrać strony z url (błąd HTTP, timeout, DNS; host rozwiązywany na adres niepubliczny, przekierowanie na taki adres, ciało ponad 10 MB) |
| 502 | inference_failed |
żadna reguła nie została oceniona (dostawca inferencji zawiódł dla każdej reguły); strona nie jest naliczana |
| 503 | unavailable |
usługa chwilowo niedostępna |
| 503 | rate_limiter_unavailable |
limit kluczy w Redisie niedostępny (fail-closed; GSPAMJUDGE_APIKEY_RATE_FAIL_OPEN=true przepuszcza) |
| 503 | queue_unavailable |
POST /v1/jobs: kolejka (Redis) nieskonfigurowana albo niedostępna (#51) |
| 503 | webhooks_unavailable |
POST /v1/jobs z webhook_url, a serwer nie ma sekretu podpisu webhooków (#51) |
message dla 500/502 jest domyślnie ogólny; treść wyjątku (typ + komunikat, maks. 300 znaków) pojawia
się tylko przy GSPAMJUDGE_EXPOSE_ERRORS=true (dev). Szczegóły zawsze w logu serwera z request_id.
Nowy kod to zmiana niełamiąca (wpis w CHANGELOG); zmiana znaczenia lub usunięcie kodu — tylko w nowej wersji.
Błędy pojedynczych stron w batchu nie są błędami HTTP: trafiają do results[].error (odpowiedź 200).
Ścieżki spoza /v1 (panel, /health) zachowują domyślne odpowiedzi FastAPI.