Appearance
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"
}
}| Feld | Typ | Bedeutung |
|---|---|---|
message | String | Kurzer Fehlertext. Steht aus Kompatibilitätsgründen auch auf oberster Ebene, so wie Laravel es von Haus aus sendet. |
error.code | String | Maschinenlesbarer Fehlercode, siehe Tabelle unten. Darauf sollte ein Client reagieren. |
error.message | String | Derselbe Text wie message. Englisch, für Menschen gedacht. Kann sich ändern. |
error.status | Integer | Der HTTP-Status, noch einmal im Body. |
error.request_id | String | Kennung dieser Anfrage, siehe Request-ID. |
error.details | Objekt | Nur wenn es Zusatzinformationen gibt, zum Beispiel die fehlerhaften Felder bei einer Validierung. Fehlt sonst ganz. |
errors | Objekt | Nur 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
code | HTTP | Wann | Typischer Text |
|---|---|---|---|
not_found | 404 | Endpunkt gibt es nicht, oder der Datensatz fehlt bzw. ist nicht freigegeben | „This endpoint or resource does not exist.“ |
unauthenticated | 401 | Kein Token, oder das Token ist ungültig oder widerrufen | „Authentication required: send a valid Bearer token.“ |
forbidden | 403 | Token gültig, aber ohne die nötige Ability | „Invalid ability provided.“ |
validation_failed | 422 | Parameter ungültig (siehe oben) | „The given data was invalid.“ |
method_not_allowed | 405 | Falsche 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_requests | 429 | Zu 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_error | 500 (und andere 5xx) | Unerwarteter Fehler auf dem Server | „An unexpected error occurred. Quote the request id when reporting it.“ |
bad_request | 400 | Anfrage nicht lesbar | – |
conflict | 409 | Konflikt mit dem Zustand auf dem Server | – |
gone | 410 | Ressource dauerhaft entfernt | – |
unsupported_media_type | 415 | Format der Anfrage wird nicht unterstützt | – |
unprocessable | 422 | Anfrage verstanden, aber nicht verarbeitbar (ohne Feldliste) | – |
service_unavailable | 503 | Server vorübergehend nicht verfügbar, z. B. Wartungsmodus | – |
client_error | andere 4xx | Sonstiger Fehler des Clients | – |
application_error | meist 422 | Fachlicher 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-abc12345json
{
"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:
- HTTP-Status prüfen. Alles außer 2xx ist ein Fehler.
- Body als JSON lesen und
error.codeauswerten. - Je nach Code reagieren:
not_found: Inhalt ausblenden oder 404-Seite zeigen. Nicht wiederholen.unauthenticatedoderforbidden: Konfigurationsfehler. Token prüfen, Betrieb informieren. Nicht wiederholen.too_many_requests:Retry-Afterabwarten, dann einmal wiederholen.server_error,service_unavailableund Netzwerkfehler: mit kurzer Pause wenige Male wiederholen (z. B. nach 1 s, 2 s, 4 s), danach aufgeben.- Alles andere: nicht wiederholen, protokollieren.
- Immer
error.request_idprotokollieren. - 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"'