RFC 9457 Problem Details Gives API Errors One Shape Instead of Fifty Ad Hoc JSON Bodies
Pick any five public APIs and look at what comes back on a failed request. One returns {"error": "invalid_token"}. Another nests it: {"errors": [{"code": 1042, "msg": "..."}]}. A third sends {"success": false, "message": "Something went wrong"} with a 200 status, which is its own sin. Every client has to learn every shape, and every shared tool, from SDK generators to API gateways, has to guess.
The status code already says what class of failure happened. What it can’t say is which problem, in enough detail for a program to act on. That gap is what RFC 9457, Problem Details for HTTP APIs, fills. It was published in July 2023 on the IETF standards track, written by Mark Nottingham, Erik Wilde and Sanjay Dalal, and it obsoletes RFC 7807, the 2016 version most people still cite.
Five members and a media type
A problem details response is a JSON object served as application/problem+json. There’s an XML twin, application/problem+xml, which almost nobody uses. The object has five standard members, all optional:
HTTP/1.1 403 Forbidden
Content-Type: application/problem+json
{
"type": "https://api.example.com/problems/out-of-credit",
"title": "You do not have enough credit.",
"status": 403,
"detail": "Your balance is 30, but that costs 50.",
"instance": "/account/12345/msgs/abc",
"balance": 30,
"accounts": ["/account/12345", "/account/67890"]
}
- type is a URI that identifies the kind of problem. Clients treat it as the primary key for error handling, the thing you switch on. It should point at human-readable docs, but the spec says clients shouldn’t fetch it automatically.
- title is a short summary of the type. It stays the same across occurrences, apart from translation.
- status repeats the HTTP status code. It’s advisory, and the server must send the same code on the actual response.
- detail explains this occurrence, written to help the caller fix it. Clients shouldn’t parse it.
- instance is a URI for this specific occurrence. Useful as a correlation ID your support team can look up.
balance and accounts above are extension members. A problem type can define as many as it needs, and clients must ignore the ones they don’t recognize. The RFC asks that extension names start with a letter, use only letters, digits and underscores, and run at least three characters. That’s how machine-readable data travels: as named fields, never buried in detail.
If type is missing, it defaults to about:blank, meaning “nothing beyond the status code”. In that case title should match the standard reason phrase. A bare {"status": 404, "title": "Not Found"} is a valid problem document, and a decent starting point for an API that has no richer errors yet.
What changed from RFC 7807
Less than the new number suggests. The format is the same, so a 7807 response is a valid 9457 response. The 2023 revision added three things worth knowing.
It created an IANA registry, HTTP Problem Types, for problem types that are common enough to share across APIs. Registration needs a published spec, and vendor-specific types aren’t allowed in. As of the RFC only one entry exists, about:blank, so the registry is a slot waiting for shared types more than a catalogue.
It says what to do with several errors at once. If they’re different types, report the most relevant or urgent one. Don’t invent a generic “batch” type that mixes them. If they’re many instances of one type, which is the classic form validation case, the type can define an array extension. The RFC’s own example uses an errors array where each entry carries a detail and a JSON Pointer to the bad field:
{
"type": "https://api.example.com/problems/validation",
"title": "Your request is not valid.",
"status": 422,
"errors": [
{ "detail": "must be a positive integer", "pointer": "#/age" },
{ "detail": "must be 'green', 'red' or 'blue'", "pointer": "#/profile/color" }
]
}
And it tightened the wording on relative type URIs and on when clients may dereference them. Use absolute URIs and the question goes away.
Where teams get it wrong
Stack traces in detail. The security section warns against leaking implementation details, and it’s the most common slip. detail is for the caller. Exception text goes to your logs, keyed by the instance value.
One type for everything. If every error is https://api.example.com/problems/error, you’ve rebuilt the old ad hoc format with a new media type. A type earns its place when a client would handle it differently: retry, re-authenticate, show a field error, stop.
Status mismatch. The status member and the real status code must agree. A proxy that rewrites the code won’t touch the body, and the spec admits it’s unclear which one wins when they differ. Don’t give clients the puzzle.
Problems for everything. The RFC says outright that generic conditions are often better expressed as a plain status code. A 404 with about:blank is fine. You don’t need https://api.example.com/problems/not-found.
Adopting it on an existing API
You don’t need a version bump. Clients that send Accept: application/json usually parse application/problem+json without complaint, since it’s JSON. The safe path is to add the standard members alongside your old error fields for a release, document the new types, then retire the old fields on your normal deprecation schedule.
Framework support makes this cheap. Spring Framework 6 has a ProblemDetail class and ASP.NET Core has a built-in problem details service, so on those stacks it’s often a configuration change plus a handful of type definitions.
Write the type docs first. Each one needs a URI, a title, the status code it goes with and what the client should do next. If you can’t write that last part, the type probably isn’t worth defining.
The status codes still carry the first layer of meaning, and getting those right comes before any of this. Problem details is the second layer. It’s small, it’s standard, and it means your error handling stops being one more thing every integrator has to learn from scratch.