Ana Sayfa / Blog / API error response formatı: Problem Details (RFC 7807) pratikte

API error response formatı: Problem Details (RFC 7807) pratikte

Her API kendi error format'ını uyduruyor, client developer'lar farklı parse logic yazıyor. RFC 7807 Problem Details bu kaosun standart çözümü, 3 projede uyguladım.

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:

  1. Parallel format: hem eski format hem Problem Details döndür. Accept header’a göre birini seç.
  2. Deprecation: eski format’ta X-Deprecated header, 3 ay süre.
  3. 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.

Bu konuda bir projeniz mi var?

Kısa bir özet bırakın, 24 saat içinde size dönüş yapayım.

İletişime Geç