Every HTTP API eventually invents its own error shape. One service returns { "error": "..." }, the next returns { "code": 42, "message": "..." }, the third puts everything in the status text. Clients end up writing per-service adapters for what should be the most boring part of an API.
RFC 9457 — Problem Details for HTTP APIs is the IETF's answer to this. It was published in July 2023 and obsoletes RFC 7807 (March 2016). The two are intentionally backward-compatible — if you already speak 7807, you already speak 9457.
What the spec actually defines
A small JSON object served under the media type application/problem+json (or application/problem+xml for the XML serialization). The five standard members are:
| Field | Required | What it is |
|---|---|---|
type | no* | A URI identifying the problem type. Should ideally dereference to human-readable docs. Defaults to "about:blank". |
title | no* | A short, human-readable summary of the problem type. Shouldn't change per occurrence. |
status | no | The HTTP status code, duplicated in the body so it survives proxies that rewrite responses. |
detail | no | A human-readable explanation specific to this occurrence. |
instance | no | A URI identifying the specific occurrence (often the failing request path). |
*Technically all members are optional, but consumers are told to treat type as the primary identifier and to fall back to title when they don't recognize the URI.
You're also allowed — and encouraged — to add extension members for whatever your domain needs. Unknown extensions must be ignored by clients, so adding a trace_id or balance field doesn't break anyone.
A canonical example, lifted from the RFC itself:
HTTP/1.1 403 Forbidden Content-Type: application/problem+json Content-Language: en { "type": "https://example.com/probs/out-of-credit", "title": "You do not have enough credit.", "status": 403, "detail": "Your current balance is 30, but that costs 50.", "instance": "/account/12345/msgs/abc", "balance": 30, "accounts": ["/account/12345", "/account/67890"] }
balance and accounts are extension members. A client that doesn't know about them just ignores them; one that does can use them to render a useful error UI without a second round-trip.
What 9457 changed from 7807
Three things, all in Appendix D:
- A problem type registry. Section 4.2 introduces an IANA registry of common problem type URIs so multiple APIs can converge on the same
typefor the same situation instead of each minting their own. - Clearer guidance for multiple problems. 7807 left a door open to returning a list of unrelated problems (and the
207 Multi-Statuspattern that often came with it). 9457 closes that door — a problem details response should describe one problem. Related sub-errors (like field-level validation failures) still belong in an extension array — typically callederrors— but they should share a single overall type. - Non-dereferenceable type URIs. Section 3.1.1 explicitly OKs schemes like
tag:for cases where you don't want to (or can't) host docs at the URL.
In practice, the most common payload that came out of these changes is the validation-error shape:
{ "type": "https://example.net/validation-error", "title": "Your request is invalid.", "status": 422, "errors": [ { "detail": "must be a positive integer", "pointer": "#/age" }, { "detail": "must be 'green', 'red' or 'blue'", "pointer": "#/profile/color" } ] }
One overall problem (validation-error), several field-level details, each pointing at the offending location with a JSON Pointer. That's the 9457-blessed pattern.
Drop-in implementations
You don't have to hand-roll this. The Node ecosystem alone has at least two well-maintained options worth knowing about.
For NestJS, nest-problem-details-filter, by Abdeldjalil Fortas, wires an exception filter that converts every HttpException into an application/problem+json response — no controller changes required.
// main.ts import { HttpAdapterHost } from '@nestjs/core'; import { HttpExceptionFilter } from 'nest-problem-details-filter'; app.useGlobalFilters( new HttpExceptionFilter(app.get(HttpAdapterHost)), );
You keep throwing BadRequestException, NotFoundException, etc., and clients start seeing well-formed Problem Details on the wire. It also handles the Retry-After header per RFC 9110 (for 429/503), ships an optional Swagger decorator, and supports both the Express and Fastify adapters.
For Hono — and by extension anything running on the edge (Cloudflare Workers, Deno, Bun, Node) — hono-problem-details, by Ryota Ikezawa, registers as the global error handler:
import { problemDetailsHandler } from 'hono-problem-details'; app.onError(problemDetailsHandler());
It has zero runtime dependencies beyond Hono itself, plugs into Zod / Valibot / Standard Schema to produce the validation-error shape above, and ships a typed problem registry so you can define your domain error types once and get compile-time checks at every throw site.
For Python, rfc9457 — published under the NRWLDev org by Daniel Edgecombe, lead engineer at Narwhal Engineering — is the core exception library. You subclass a base problem class, set a title, raise it from your endpoint, and call .marshal() to get the RFC-shaped dict. For FastAPI specifically, the companion fastapi-problem package wires that into an exception handler so you don't have to:
from fastapi import FastAPI from fastapi_problem.handler import new_exception_handler, add_exception_handler from rfc9457 import NotFoundProblem class UserNotFoundError(NotFoundProblem): title = "User not found." app = FastAPI() add_exception_handler(app, new_exception_handler()) @app.get("/users/{user_id}") async def get_user(user_id: str): raise UserNotFoundError(detail=f"No user with id {user_id}.")
If you'd rather skip the third-party-package question entirely, Litestar (Python's high-performance ASGI framework) ships first-party RFC 9457 support via its built-in ProblemDetailsPlugin. Register it on the app and flip one config flag to auto-convert every HTTPException into a Problem Details response:
from litestar import Litestar from litestar.plugins.problem_details import ProblemDetailsPlugin, ProblemDetailsConfig app = Litestar( plugins=[ ProblemDetailsPlugin( ProblemDetailsConfig(enable_for_all_http_exceptions=True), ), ], )
You can also pass an exception_to_problem_detail_map to wire your own domain exceptions into the same response shape.
Other major frameworks have first-party support too: Spring Boot ships ProblemDetail natively since 6.x, and ASP.NET has ProblemDetails baked into the controller pipeline. Whichever you pick, the bytes on the wire come out the same shape.
If you'd rather work at the library layer than the framework layer, there's also a framework-agnostic error catalog in early development: JohnAdib/rfc9457, by John Adib, ships 39 prebuilt errors with semantic aliases (errors.client.idNotFound(), errors.server.db(), and so on) and zero runtime dependencies. You construct the error object and still wire the HTTP response yourself outside of its built-in Hono middleware. Worth knowing about, but it's new enough (single-digit stars at the time of writing) that I'd kick the tires before betting on it.
If you're on standalone Fastify specifically, there's also an early-stage fastify-rfc9457 plugin by Francesco Capurso that wires the Problem Details format into Fastify's reply pipeline and ships built-in i18n for error messages (EN/IT/ES/DE/FR) — same nascent-project caveat applies.
Connect with the maintainers
The libraries above are open-source efforts maintained by individual engineers. If you find any of them useful in your stack, the people behind them are worth following.





Worth adopting?
Probably yes, mostly because the alternative is doing the same design work yourself for worse interop. You were going to invent some error shape regardless. Picking 9457 means the shape is documented, the libraries exist, and anyone who's hit another 9457 API already knows how to parse it. A few hours wiring up a filter (or a middleware, or whatever your framework calls it) buys you the rest for free.
Links

