Skip to content

Fehler ​

Alle Fehler der API kommen im selben JSON-Format, egal ob ein Datensatz fehlt, das Token nicht passt oder auf dem Server etwas schiefgeht. Das gilt für jede Anfrage unter /api/…, auch wenn der Client kein Accept: application/json schickt (ein Browser bekommt also bei einem unbekannten Pfad ebenfalls JSON und keine HTML-Fehlerseite).

Eine Ausnahme gibt es: Ruft man einen geschützten Endpunkt ohne Token und ohne Accept: application/json auf, will Laravel zur Login-Seite umleiten. Weil es keine Route login gibt, kann daraus ein 500 server_error statt 401 unauthenticated werden. Schicken Sie deshalb immer Accept: application/json mit.

Umgesetzt ist das in admin/app/Support/ApiErrorResponse.php, eingebunden in admin/bootstrap/app.php. Die Tests stehen in admin/tests/Feature/Api/ApiErrorFormatTest.php.

Erfolgreiche Antworten bleiben unverändert. Sie haben kein error-Feld.

Aufbau ​

json
{
  "message": "This endpoint or resource does not exist.",
  "error": {
    "code": "not_found",
    "message": "This endpoint or resource does not exist.",
    "status": 404,
    "request_id": "9b1c2f4e-3a77-4a52-9d0e-6f1f2b8c7a10"
  }
}
FeldTypBedeutung
messageStringKurzer Fehlertext. Steht aus Kompatibilitätsgründen auch auf oberster Ebene, so wie Laravel es von Haus aus sendet.
error.codeStringMaschinenlesbarer Fehlercode, siehe Tabelle unten. Darauf sollte ein Client reagieren.
error.messageStringDerselbe Text wie message. Englisch, für Menschen gedacht. Kann sich ändern.
error.statusIntegerDer HTTP-Status, noch einmal im Body.
error.request_idStringKennung dieser Anfrage, siehe Request-ID.
error.detailsObjektNur wenn es Zusatzinformationen gibt, zum Beispiel die fehlerhaften Felder bei einer Validierung. Fehlt sonst ganz.
errorsObjektNur bei Validierungsfehlern. Gleicher Inhalt wie error.details.fields, auf oberster Ebene für ältere Clients.

Felder ohne Wert werden weggelassen, nicht mit null gesendet. Ein Client muss also damit rechnen, dass error.details fehlt.

Validierungsfehler ​

Bei einem Validierungsfehler (HTTP 422) stehen die betroffenen Felder zweimal im Body: unter error.details.fields und, wie bei Laravel üblich, unter errors.

json
{
  "message": "The given data was invalid.",
  "error": {
    "code": "validation_failed",
    "message": "The given data was invalid.",
    "status": 422,
    "request_id": "kirby-7f3a9c21",
    "details": {
      "fields": {
        "per_page": ["The per page field must be an integer."]
      }
    }
  },
  "errors": {
    "per_page": ["The per page field must be an integer."]
  }
}

Derzeit ohne Anwendungsfall

Die heutigen Endpunkte prüfen ihre Parameter nicht mit Laravel-Validierung, sondern ignorieren ungültige Werte. Ein validation_failed kommt deshalb im Moment praktisch nicht vor. Das Format ist trotzdem festgelegt, damit Clients es schon behandeln können.

Serverfehler im Debug-Modus ​

Bei Fehlern ab 500 enthält der Body nie technische Details, außer der Server läuft mit APP_DEBUG=true (nur lokal). Dann kommt zusätzlich:

json
"error": {
  "code": "server_error",
  "…": "…",
  "debug": { "exception": "RuntimeException", "message": "…" }
}

Auf Produktion ist APP_DEBUG aus. Dort steht nur die Request-ID im Body, mit der sich der Fehler im Server-Log finden lässt.

Fehlercodes ​

codeHTTPWannTypischer Text
not_found404Endpunkt gibt es nicht, oder der Datensatz fehlt bzw. ist nicht freigegeben„This endpoint or resource does not exist.“
unauthenticated401Kein Token, oder das Token ist ungültig oder widerrufen„Authentication required: send a valid Bearer token.“
forbidden403Token gültig, aber ohne die nötige Ability„Invalid ability provided.“
validation_failed422Parameter ungültig (siehe oben)„The given data was invalid.“
method_not_allowed405Falsche HTTP-Methode, z. B. POST auf einen Lese-Endpunkt. Der Header Allow nennt die erlaubten Methoden.„This HTTP method is not allowed here.“
too_many_requests429Zu viele Anfragen. Der Header Retry-After nennt die Wartezeit in Sekunden. Grenzen siehe Rate Limit.„Too many requests. Retry after the time given in Retry-After.“
server_error500 (und andere 5xx)Unerwarteter Fehler auf dem Server„An unexpected error occurred. Quote the request id when reporting it.“
bad_request400Anfrage nicht lesbar–
conflict409Konflikt mit dem Zustand auf dem Server–
gone410Ressource dauerhaft entfernt–
unsupported_media_type415Format der Anfrage wird nicht unterstützt–
unprocessable422Anfrage verstanden, aber nicht verarbeitbar (ohne Feldliste)–
service_unavailable503Server vorübergehend nicht verfügbar, z. B. Wartungsmodus–
client_errorandere 4xxSonstiger Fehler des Clients–
application_errormeist 422Fachlicher Fehler aus der Anwendung (App\Exceptions\AppException). Code, Status und details setzt die Anwendung selbst.je nach Fall

Die Codes ab bad_request werden aus dem HTTP-Status abgeleitet. Sie entstehen, wenn irgendwo im Code ein HTTP-Fehler mit diesem Status ausgelöst wird. Die heutigen Endpunkte tun das nicht gezielt, die Codes sind aber Teil des Formats.

Die Texte sind Englisch und können sich ändern. Werten Sie im Client nur error.code und den HTTP-Status aus, nie den Text.

Hinweis für Entwickler: warum „not_found“ keine IDs mitliefert

ApiErrorResponse hat einen eigenen Zweig für ModelNotFoundException mit Text „The requested … does not exist.“ und details.ids. Laravel wandelt diese Exception aber vor den Render-Callbacks in eine NotFoundHttpException um. In der Praxis kommt deshalb bei fehlenden Datensätzen derselbe allgemeine Text wie bei unbekannten Pfaden, ohne details. Gleiches gilt für 403: Sanctum meldet eine fehlende Ability als AuthorizationException, Laravel macht daraus eine AccessDeniedHttpException, und der Text ist der von Sanctum. Der Code (not_found, forbidden) stimmt in beiden Fällen.

Request-ID ​

Jede Fehlerantwort hat den Header X-Request-Id. Derselbe Wert steht in error.request_id.

  • Schickt der Client selbst einen Header X-Request-Id, übernimmt der Server ihn, wenn er gültig ist: 8 bis 64 Zeichen, nur Buchstaben, Ziffern, Punkt, Unterstrich und Bindestrich (^[A-Za-z0-9._-]{8,64}$).
  • Ist er ungültig oder fehlt er, erzeugt der Server eine UUID.
bash
curl -i https://admin.matev.eu/api/v1/products/gibt-es-nicht \
  -H 'Accept: application/json' \
  -H 'X-Request-Id: kirby-abc12345'
http
HTTP/2 404
content-type: application/json
x-request-id: kirby-abc12345
json
{
  "message": "This endpoint or resource does not exist.",
  "error": {
    "code": "not_found",
    "message": "This endpoint or resource does not exist.",
    "status": 404,
    "request_id": "kirby-abc12345"
  }
}

Erzeugen Sie die ID im Client und schreiben Sie sie in Ihr eigenes Log. Wenn Sie einen Fehler melden, nennen Sie die Request-ID: damit findet man den Vorgang auf beiden Seiten.

Nur bei Fehlern

Erfolgreiche Antworten tragen derzeit keinen X-Request-Id-Header. Die ID gibt es nur bei Fehlern.

Browser-Code auf einer anderen Domain kann den Header nicht lesen (CORS gibt ihn nicht frei). Lesen Sie die ID dort aus error.request_id.

So behandeln Clients Fehler ​

Empfohlenes Vorgehen:

  1. HTTP-Status prüfen. Alles außer 2xx ist ein Fehler.
  2. Body als JSON lesen und error.code auswerten.
  3. Je nach Code reagieren:
    • not_found: Inhalt ausblenden oder 404-Seite zeigen. Nicht wiederholen.
    • unauthenticated oder forbidden: Konfigurationsfehler. Token prüfen, Betrieb informieren. Nicht wiederholen.
    • too_many_requests: Retry-After abwarten, dann einmal wiederholen.
    • server_error, service_unavailable und Netzwerkfehler: mit kurzer Pause wenige Male wiederholen (z. B. nach 1 s, 2 s, 4 s), danach aufgeben.
    • Alles andere: nicht wiederholen, protokollieren.
  4. Immer error.request_id protokollieren.
  5. Ist der Body kein JSON (etwa bei einem Proxy-Fehler 502 vor dem PIM), mit dem HTTP-Status allein arbeiten.

JavaScript (fetch) ​

js
const BASE = 'https://admin.matev.eu/api/v1'

export class ApiError extends Error {
  constructor(status, code, message, requestId, details) {
    super(message)
    this.status = status
    this.code = code
    this.requestId = requestId
    this.details = details
  }
}

export async function matevApi(path, { token, retries = 2, ...init } = {}) {
  const requestId = `web-${crypto.randomUUID()}`

  for (let attempt = 0; ; attempt++) {
    const res = await fetch(`${BASE}${path}`, {
      ...init,
      headers: {
        Accept: 'application/json',
        'X-Request-Id': requestId,
        ...(token ? { Authorization: `Bearer ${token}` } : {}),
        ...init.headers,
      },
    })

    if (res.ok) return res.json()

    const body = await res.json().catch(() => null)
    const err = body?.error ?? {}
    const code = err.code ?? 'unknown'

    const retryable = ['server_error', 'service_unavailable', 'too_many_requests'].includes(code)
    if (retryable && attempt < retries) {
      const wait = Number(res.headers.get('Retry-After')) || 2 ** attempt
      await new Promise((r) => setTimeout(r, wait * 1000))
      continue
    }

    throw new ApiError(res.status, code, err.message ?? res.statusText, err.request_id ?? requestId, err.details)
  }
}

// Verwendung
try {
  const { data } = await matevApi('/products/schneepflug-sp-250')
} catch (e) {
  if (e instanceof ApiError && e.code === 'not_found') {
    // 404-Seite zeigen
  } else {
    console.error(`API-Fehler ${e.code} (Request ${e.requestId})`)
  }
}

PHP (Laravel Http) ​

php
use Illuminate\Http\Client\RequestException;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;

$requestId = 'shop-'.Str::uuid();

$response = Http::baseUrl('https://admin.matev.eu/api/v1')
    ->acceptJson()
    ->withToken(config('services.matev.token'))
    ->withHeaders(['X-Request-Id' => $requestId])
    ->retry(3, 1000, function (Throwable $e): bool {
        // Only retry server-side and rate-limit errors.
        return $e instanceof RequestException
            && in_array($e->response->json('error.code'), ['server_error', 'service_unavailable', 'too_many_requests'], true);
    }, throw: false)
    ->get('/content/products/schneepflug-sp-250');

if ($response->failed()) {
    $code = $response->json('error.code', 'unknown');

    logger()->warning('matev API error', [
        'status' => $response->status(),
        'code' => $code,
        'request_id' => $response->json('error.request_id', $requestId),
    ]);

    if ($code === 'not_found') {
        abort(404);
    }

    throw new RuntimeException("matev API: {$code}");
}

$product = $response->json('data');

curl ​

bash
curl -sS https://admin.matev.eu/api/v1/content/media \
  -H 'Accept: application/json' \
  -H "Authorization: Bearer $MATEV_TOKEN" \
  -H 'X-Request-Id: test-20261009-01' \
  | jq '.error // "ok"'