RFC 9457 Problem Details for HTTP APIs: stable errors clients can rely on
Use application/problem+json to return structured API errors with types, titles, and extension fields. Mapping exceptions, validation, and proxy behavior for production HTTP services.
A mobile client shows a generic “Something went wrong” toast while your logs contain a precise Postgres deadlock stack trace. A partner integration retries the same bad payload for an hour because 400 Bad Request carries no machine-readable reason. A gateway strips your JSON body and the frontend parses HTML as JSON. These failures share a root cause: HTTP status codes alone are not a contract. They tell clients that something failed, rarely what went wrong in a stable, programmatic way.
RFC 9457 (“Problem Details for HTTP APIs”) formalizes a small JSON document for errors: a type URI, optional title, status, detail, and instance, plus extension members. This article explains why that shape matters, how to implement it without leaking internals, and where it intersects with gateways, i18n, and observability—patterns that show up repeatedly when helping teams ship predictable, production-grade HTTP surfaces.
The contract: application/problem+json
Problem Details responses use the media type application/problem+json. The minimal payload looks like this:
{
"type": "https://api.example.com/problems/insufficient-credits",
"title": "Insufficient credits",
"status": 402,
"detail": "Account acc_9fx has 0 remaining credits for this operation.",
"instance": "https://api.example.com/correlation/01HZXK9Q7Y"
}
type is a URI that identifies the problem category. It should be stable across versions: clients (and API gateways) can branch on it without brittle string matching on detail. Many teams use HTTPS URLs under their own domain that resolve to human-readable documentation; that is recommended in the RFC, not mandatory—a URN or about:blank style URI works if you document it.
title is a short, constant summary for the type (not a stack trace). status repeats the HTTP status for clients that only inspect the body (e.g. buffered error pages). detail is where this occurrence-specific text lives—safe to vary per request. instance may identify the specific occurrence (often a correlation or request id URL or opaque id).
Extension members are additional JSON properties at the top level—for example balance, retry_after_seconds, or field_errors—as long as they do not collide with registered names.
Why not plain { "error": "..." }?
Ad hoc JSON errors proliferate: one route returns { error: string }, another { message, code }, another NestJS-style { statusCode, message, error }. Client SDKs end up with special cases per endpoint. Problem Details give you:
- A shared envelope so generated clients and middleware can deserialize once.
- A primary key for logic:
type(and optionally a registered subclass extension) instead of parsing English sentences indetail. - Alignment with HTTP semantics:
401vs403vs422remain authoritative; the body narrows the failure inside that status class.
The trade-off is discipline: you must curate a catalog of type URIs and keep them backward compatible, similar to managing error codes in a public SDK.
Mapping domain failures to types
In freelance and consulting projects, a useful pattern is a single internal taxonomy (enum or union of string constants) mapped to:
- HTTP status — resource semantics (
404missing,409conflict,422validation). typeURI — stable id for client branching.title— fixed per type;detail— safe, contextual message for humans or logs.
Avoid using detail as the primary machine-readable signal: copy changes, localization, and support edits will break clients.
Validation and 422 / 400
Validation errors are where extension members shine. Example:
{
"type": "https://api.example.com/problems/validation-error",
"title": "Validation failed",
"status": 422,
"detail": "One or more fields failed validation.",
"errors": [
{ "field": "email", "message": "must be a valid email address" },
{ "field": "quantity", "message": "must be >= 1" }
]
}
Here errors is an extension. Document it in your API reference and keep the shape stable. Some APIs nest RFC 7807 “legacy” invalid-params from older drafts; if you standardize on one extension layout, stick to it across routes.
Trade-off: returning every field error vs failing fast affects payload size and attacker probing surface—expose enough for legitimate clients, avoid echoing raw database constraint names unless they are part of your public contract.
Proxies, gateways, and Content-Type
Problem Details assume the body reaches the client intact. In practice:
- Reverse proxies may replace error bodies with HTML unless you configure custom error pages per route or disable body substitution for API paths.
- API gateways sometimes normalize errors; ensure they preserve
application/problem+jsonor map their own format to Problem Details at the edge.
If the client receives text/html with a 502, your structured error never arrived—fix infrastructure before tweaking JSON schemas.
Internationalization
The RFC allows title and detail in any language. For public APIs, teams often:
- Keep
typeand extension codes locale-independent. - Serve
Accept-Language-awaretitle/detailor return fixed English in the body and localize in the client usingtype+ a message catalog.
Mixing translated detail strings with client-side parsing is fragile; prefer type + structured extensions for logic and human text for display only.
Practical example: Express-style middleware
The following TypeScript sketch wires centralized error handling and emits Problem Details. It is illustrative—adapt validation libraries and logger interfaces to your stack.
import type { Request, Response, NextFunction } from "express";
export class ApiProblem extends Error {
constructor(
public readonly problem: {
type: string;
title: string;
status: number;
detail?: string;
instance?: string;
extensions?: Record<string, unknown>;
}
) {
super(problem.title);
this.name = "ApiProblem";
}
}
export function problemJsonHandler() {
return (err: unknown, req: Request, res: Response, _next: NextFunction) => {
const status =
err instanceof ApiProblem ? err.problem.status : 500;
const base =
err instanceof ApiProblem
? err.problem
: {
type: "https://api.example.com/problems/internal-error",
title: "Internal Server Error",
status: 500,
detail: "An unexpected error occurred.",
};
const body = {
type: base.type,
title: base.title,
status,
...(base.detail ? { detail: base.detail } : {}),
instance:
base.instance ??
(req.id ? `urn:uuid:${req.id}` : undefined),
...(base.extensions ?? {}),
};
res.status(status).type("application/problem+json").json(body);
};
}
// Usage in a route handler:
// throw new ApiProblem({
// type: "https://api.example.com/problems/insufficient-credits",
// title: "Insufficient credits",
// status: 402,
// detail: "Not enough credits to run this job.",
// extensions: { balance: 0 },
// });
In production you would log the internal cause (including stack traces) server-side only, map known failures to ApiProblem, and return generic detail for unclassified 500s to avoid leaking implementation details.
For Next.js Route Handlers or other environments, the same object shape applies: set Content-Type: application/problem+json and the JSON body; frameworks that default to HTML errors benefit from explicit serialization.
Common mistakes and pitfalls
- Overloading
detailfor client logic — breaks as soon as wording changes; usetypeand extensions. - Using generic
typeURLs for every error — collapses the benefit; maintain a finite catalog of problem types. - Mismatch between HTTP
statusand JSONstatus— confuses caches and clients; keep them equal. - Leaking stack traces or SQL in
detail— treat Problem Details as externally visible unless the endpoint is strictly internal. - Ignoring intermediaries — HTML error pages from nginx or CloudFront mask your JSON; verify end-to-end with real domains, not just localhost.
- No correlation id — omitting
instance(or a header likeX-Request-Id) makes support handoffs painful.
Conclusion
RFC 9457 Problem Details turn HTTP errors from ad hoc JSON into a small, predictable contract: type for programmatic handling, title/detail for humans, extensions for domain-specific structure—while 401/404/422 remain the coarse HTTP signal. The investment pays off in SDK stability, gateway-friendly responses, and clearer boundaries between what operators see in logs and what clients consume.
If you are designing or standardizing HTTP APIs for services that must scale and integrate cleanly, treating errors as part of the public interface—not an afterthought—saves rework across mobile, web, and partner integrations. For more context on how this site approaches engineering work, see About; for collaboration or inquiries, Contact.
Assine a newsletter
Receba um e-mail quando novos artigos forem publicados. Sem spam — apenas novos posts deste blog.
Via Resend. Você pode cancelar a inscrição em qualquer e-mail.