Loslegen
Javaneer
Zurück zur Stufe
Stufe 5·API- & Vertragsdesign

Fehler & Idempotenz

Ein konsistenter, maschinenlesbarer Fehlervertrag (RFC 9457 Problem Details) und Idempotenzschlüssel, damit eine wiederholte Anfrage nicht doppelt belastet - die Verträge, die eine API sicher konsumierbar machen.

15 Min. LesezeitExperte

Deutsche Übersetzung in Arbeit

Diese Lektion ist noch nicht ins Deutsche übersetzt und wird daher auf Englisch angezeigt. Der Rest der Seite ist vollständig lokalisiert.

Auf dieser Seite

Two aspects of an API contract are routinely neglected and cause the worst production incidents: how errors are communicated and what happens when a request is retried. A consistent, machine-readable error format lets clients handle failure sanely instead of scraping prose; idempotency lets a client safely retry after a timeout without double-charging a customer. Both are part of the contract, and getting them right is what separates a robust API from a fragile one.

A consistent, machine-readable error contract

The worst error handling returns different shapes from different endpoints - {"error": "..."} here, {"message": "...", "code": 5} there, a raw stack trace somewhere else. Clients can't handle any of it uniformly. The standard fix is RFC 9457 Problem Details (formerly RFC 7807): one JSON shape for every error across your API:

HTTP/1.1 409 Conflict
Content-Type: application/problem+json
{
  "type":     "https://api.ledger.dev/problems/insufficient-funds",
  "title":    "Insufficient funds",
  "status":   409,
  "detail":   "Account 42 has a balance of 30.00 USD; the transfer requires 50.00 USD.",
  "instance": "/accounts/42/transfers/abc",
  "balance":  "30.00"
}
  • type - a stable URI identifying the kind of error, which clients can branch on programmatically.
  • title - a short human-readable summary; detail - specifics for this occurrence.
  • status - mirrors the HTTP status.
  • Extension members (balance) - domain-specific data the client can use.

Spring Boot supports this natively (ProblemDetail and @ExceptionHandler). The win: one error shape everywhere, machine-readable type codes, and no leaking of stack traces. Consumers write error handling once.

Never leak internals in errors

An error body is an attacker's reconnaissance goldmine and a consumer's confusion. Never return raw exception messages, stack traces, SQL, or internal class names in a production API response - they expose your implementation and can leak secrets. Return a clean Problem Details document with a stable type and a helpful (but internal-free) detail; log the full exception server-side, correlated by a trace id (recall observability) so you can still debug.

Idempotency: making retries safe

Networks fail after a request is processed but before the response arrives. The client times out, doesn't know if it worked, and retries. For a GET or PUT that's fine (idempotent by nature). But for a POST /transfers or POST /payments, a naive retry charges the customer twice. This is one of the most common and damaging real-world API bugs.

The solution is an idempotency key: the client generates a unique key per logical operation and sends it with the request; the server remembers keys it has processed and returns the original result for a repeat:

POST /payments
Idempotency-Key: 7f3a-...-c1     ← client-generated, unique per logical payment

Server logic:
  seen this key before?
    no  → process the payment, store (key → result), return 201
    yes → do NOT process again; return the stored original result (201)

Now a retried request with the same key is a no-op that returns the first response - the customer is charged exactly once, no matter how many times the client retries. This is exactly how Stripe and other payment APIs work, and it's the correct pattern for any non-idempotent, side-effecting operation. (Note the connection to the batch/messaging modules: idempotency is the same defense against duplicate delivery, here at the API edge.)

Status codes carry retry semantics

The error contract and idempotency meet in status codes, which tell the client whether to retry:

  • 4xx - the client's fault (bad input, not found, conflict). Retrying the same request won't help; fix it first. Don't auto-retry.
  • 5xx and timeouts - possibly transient server/network trouble. Safe to retry if the operation is idempotent (naturally, or via an idempotency key). This is why idempotency and error design are one topic: together they tell a client exactly when a retry is safe.
A duplicate check with a reference number

Idempotency is how banks stop you paying a bill twice. You write a check with a reference number on it; if the payment system sees that reference has already cleared, it doesn't debit you again - it just confirms the original payment went through. Without the reference, a jittery 'did that go through?' resend would double-pay the bill. The idempotency key is that reference number for API calls: the client stamps each logical operation with one, and the server honors 'I've already done this one' by returning the first outcome rather than doing it again. And Problem Details is the bank's standardized rejection slip - same form every time, with a code you can act on - instead of a different scribbled note from each teller.

Make the payment endpoint safe

A POST /orders/{id}/pay endpoint charges a customer's card. Under flaky mobile networks, clients frequently time out and retry, and support is fielding complaints about double charges. Additionally, when the card is declined, the endpoint returns 200 with {"ok": false, "why": "declined"}. Fix both problems and explain how a client should now behave on a timeout.

What problem does an idempotency key solve, and how?

Key takeaways

  • Use a consistent, machine-readable error contract - RFC 9457 Problem Details - so clients handle every error uniformly via a stable type code.
  • Never leak stack traces, SQL, or internal messages in error responses; return a clean Problem Details doc and log the full error server-side, correlated by trace id.
  • Non-idempotent operations (POST payments/transfers) need idempotency keys so a client retry after a timeout returns the original result instead of double-processing.
  • The server stores processed idempotency keys and returns the first response for repeats - exactly one effect regardless of retries (the Stripe pattern).
  • Status codes carry retry semantics: 4xx is the client's fault (don't blindly retry); 5xx/timeouts are retry-safe when the operation is idempotent.
War diese Lektion hilfreich?
Diese Seite auf GitHub bearbeiten