API error response format’ı çözülmüş bir problem mi? Teoride evet, RFC 7807 Problem Details standardı var. Pratikte herkes kendi format’ını uyduruyor, client ekipleri her integration’da parse logic’i baştan yazıyor.
Son 3 API projemde Problem Details kullandım. İlk proje hesitantla, ikincide benimsedim, üçüncüde default. Bu yazıda neden ve nasıl.
Sorun: format anarşisi
Tipik API’lerin error format çeşitliliği:
Stripe:
{"error": {"type": "invalid_request", "code": "missing", "message": "..."}}GitHub:
{"message": "...", "errors": [...], "documentation_url": "..."}Google:
{"error": {"code": 400, "message": "...", "status": "INVALID_ARGUMENT"}}Twilio:
{"code": 20001, "message": "...", "more_info": "..."}Client ekibi her API için farklı handler yazıyor. Retry logic, user-facing message, debugging hepsi ayrı.
RFC 7807 Problem Details
IETF standardı 2016’da yayınlandı. Core idea: error response için standart schema.
HTTP/1.1 400 Bad Request
Content-Type: application/problem+json
{
"type": "https://example.com/errors/insufficient-funds",
"title": "Insufficient Funds",
"status": 400,
"detail": "Account balance is 100 TL, requested 150 TL.",
"instance": "/accounts/12345/transfer/xyz"
}5 standart field:
- type: error type URI (dokümantasyon linki veya unique ID)
- title: insan-okunur kısa başlık
- status: HTTP status code
- detail: bu specific instance’ın açıklaması
- instance: bu error hangi URI’den çıktı
Avantaj: client generic parser yazıyor, yeni API integration’da sadece type URI’leri öğreniyor.
Extension field’lar
Standart 5 field yetmezse extension ekliyorsun:
{
"type": "https://example.com/errors/validation",
"title": "Validation Failed",
"status": 400,
"detail": "Request body has errors.",
"instance": "/orders",
"errors": [
{"field": "email", "code": "invalid_format", "message": "Not a valid email"},
{"field": "age", "code": "too_small", "message": "Must be 18+"}
]
}errors array extension. Client bilen kullanır, bilmeyen ignore eder.
Type URI: dokümantasyon linki
type field’ı URI. Tercihen resolve edilebilir URL, dokümantasyona götürüyor.
{
"type": "https://api.example.com/errors/insufficient-funds"
}Bu URL’e gidince error detaylı açıklama var:
– Ne anlama geliyor
– Hangi durumlarda çıkıyor
– Client ne yapmalı (retry? user’a ne göster?)
– Örnek request + response
Resolvable URL olması güzel ama zorunlu değil. Sadece unique identifier olarak da kullanılabiliyor.
Implementation: PHP örneği
class ProblemResponse {
public static function fromException(\Throwable $e): array {
if ($e instanceof ValidationException) {
return [
'type' => '/errors/validation',
'title' => 'Validation Failed',
'status' => 400,
'detail' => $e->getMessage(),
'errors' => $e->getErrors()
];
}
if ($e instanceof InsufficientFundsException) {
return [
'type' => '/errors/insufficient-funds',
'title' => 'Insufficient Funds',
'status' => 400,
'detail' => $e->getMessage(),
'balance' => $e->getBalance(),
'requested' => $e->getRequested()
];
}
// Generic
return [
'type' => '/errors/internal',
'title' => 'Internal Server Error',
'status' => 500,
'detail' => 'Unexpected error occurred.'
];
}
}
// Usage
try {
$order = $service->createOrder($input);
} catch (\Throwable $e) {
http_response_code($e->code ?? 500);
header('Content-Type: application/problem+json');
echo json_encode(ProblemResponse::fromException($e));
exit;
}Middleware’da centralize edersiniz, controller’lar sadece exception throw eder.
Client parsing
Client:
async function apiCall(url, options) {
const response = await fetch(url, options);
if (!response.ok) {
const contentType = response.headers.get('content-type') || '';
if (contentType.includes('application/problem+json')) {
const problem = await response.json();
throw new APIProblem(problem);
}
throw new Error(`HTTP ${response.status}`);
}
return response.json();
}
class APIProblem extends Error {
constructor(problem) {
super(problem.detail || problem.title);
this.type = problem.type;
this.title = problem.title;
this.status = problem.status;
this.errors = problem.errors;
}
}Client her API’de aynı handler. Error type’a göre UI davranışı:
try {
await api.createOrder(data);
} catch (e) {
if (e instanceof APIProblem) {
if (e.type.endsWith('/validation')) {
showFieldErrors(e.errors);
} else if (e.type.endsWith('/insufficient-funds')) {
showInsufficientFundsDialog(e);
} else {
showGenericError();
}
}
}Localization
Problem Details spec i18n için ne diyor? Pek net değil. Practical approach:
Option 1: server-side localization. Accept-Language header’a göre title, detail çeviriliyor.
Option 2: client-side localization. Server type + context data gönderiyor, client kendisi localize ediyor.
Ben option 2 tercih ediyorum. Client daha iyi user context biliyor, server-side translation eksik kalıyor.
{
"type": "/errors/insufficient-funds",
"status": 400,
"balance": 100,
"requested": 150,
"currency": "TRY"
}Client kendi i18n dosyası ile mesaj oluşturuyor: “Bakiyeniz 100 TL, istenen 150 TL”.
Security consideration: over-disclosure
Error’da hassas bilgi vermemeye dikkat. Stack trace, internal path, DB hatası detail’i client’a gitmesin.
// Kötü
{
"detail": "SQL error: duplicate key constraint users_email_key"
}
// İyi
{
"detail": "Email already registered."
}Internal error (5xx) için:
{
"type": "/errors/internal",
"title": "Internal Server Error",
"status": 500,
"detail": "Something went wrong.",
"requestId": "req_abc123"
}requestId client debug için, internal log’da correlate edilebiliyor. Actual stack trace sadece log’da.
Validation error pattern
Form validation en sık error case. Pattern:
{
"type": "/errors/validation",
"title": "Validation Failed",
"status": 400,
"detail": "Request body has errors.",
"errors": [
{
"field": "email",
"code": "invalid_format",
"message": "Email is not valid"
},
{
"field": "items[0].quantity",
"code": "too_small",
"message": "Quantity must be at least 1",
"minimum": 1
}
]
}Field path JSON Pointer syntax (items[0].quantity). Client form UI’da field’ın üstüne error mesajı yazabiliyor.
Rate limit error
Rate limiting için özel pattern:
{
"type": "/errors/rate-limited",
"title": "Rate Limit Exceeded",
"status": 429,
"detail": "Too many requests. Retry after 60 seconds.",
"retry_after": 60,
"limit": 100,
"remaining": 0,
"reset": 1704067200
}HTTP Retry-After header ile kombine. Client automatic retry logic.
OpenAPI entegrasyonu
OpenAPI spec’inde Problem Details şeması tanımlarsın, her endpoint’in error response’larını referans edersin:
components:
schemas:
Problem:
type: object
properties:
type:
type: string
title:
type: string
status:
type: integer
detail:
type: string
required: [type, title, status]
paths:
/orders:
post:
responses:
'400':
description: Bad Request
content:
application/problem+json:
schema:
$ref: '#/components/schemas/Problem'Client code generation (OpenAPI Generator) Problem Details type’ını otomatik üretiyor.
Gerçek proje: migration story
Bir API projesinde custom error format’tan Problem Details’e migration yaptık. 3 aşamada:
- Parallel format: hem eski format hem Problem Details döndür.
Acceptheader’a göre birini seç. - Deprecation: eski format’ta
X-Deprecatedheader, 3 ay süre. - Cutover: eski format kapandı.
Client ekipleri migration period’unda yeni parser yazdı. 4 ay sonra tüm ekipler Problem Details’de.
Sonuç: integration effort %30 azaldı, error handling code’u merkeziye alındı.
Son tavsiye
Yeni API tasarlarken Problem Details default. Yenileme projelerinde parallel format + deprecation ile migrate.
Standart kullanmak ekosistem benefit sağlıyor: OpenAPI tooling, client library support, documentation auto-generation.
Uyduruk error format’ların zamanı geçti.