Przejdź do treści

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ę po code, nie po message.
  • 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), null gdy brak.
  • request_id — ten sam co w nagłówku X-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.