An HTTP status code tells a client the broad class of failure. It rarely gives enough information to fix the request, display a useful message, or connect the failure to support logs.
RFC 9457 defines a common problem-details document for that gap. It replaces RFC 7807 and standardizes a small JSON object that APIs can extend without inventing a different error envelope for every endpoint.
Quick answer
Return RFC 9457 problem details when an HTTP API needs a machine-readable error document:
HTTP/1.1 422 Unprocessable Content
Content-Type: application/problem+json
{
"type": "https://api.example.com/problems/validation-failed",
"title": "Request validation failed",
"status": 422,
"detail": "Two request fields are invalid.",
"instance": "https://api.example.com/problem-occurrences/req_01JZ8Y",
"errors": [
{
"pointer": "#/email",
"code": "invalid_format"
}
]
}
Use a stable type URI to identify the category of problem. Keep title stable for that type, use detail for this occurrence, and use instance to identify this particular failure. Add typed extension members for information clients must process.
Do not expose stack traces, SQL, internal hostnames, secrets, or raw exception messages. Clients must branch on type or defined extension fields, not parse the human-readable detail string.
What RFC 9457 standardizes
The authoritative source is RFC 9457, published in July 2023. It obsoletes RFC 7807 and defines the application/problem+json and application/problem+xml media types.
The JSON object has five standard members:
| Member | Purpose |
|---|---|
| type | URI reference identifying the problem type |
| status | Advisory copy of the HTTP response status |
| title | Short human-readable summary of the type |
| detail | Human-readable explanation of this occurrence |
| instance | URI reference identifying this occurrence |
The object can include extension members. An extension is not a free-form dumping ground; it is part of the problem type’s contract.
A minimal response can be:
{
"title": "Not Found",
"status": 404
}
When type is absent, it defaults to about:blank. In that case, the problem has no semantics beyond the HTTP status code and title should match the status phrase. Use a custom type only when it communicates a reusable, documented condition.
Read REST API Status Codes Explained before choosing the response status.
Type is the machine-readable identity
Type is the most important field for client logic.
A useful type URI is stable and controlled by the API owner:
{
"type": "https://api.example.com/problems/insufficient-credit"
}
The RFC recommends that an HTTP problem type URI resolve to human-readable documentation when possible. That page can explain:
- What the problem means.
- The usual HTTP status.
- Which extension fields may appear.
- Whether a retry can succeed.
- What the client should change.
- Security or privacy considerations.
- Example requests and responses.
Do not create a new type for every exception class. Internal names such as NullPointerException or DatabaseTimeoutException are implementation details, not public API concepts.
Also avoid unstable identifiers:
{
"type": "https://api.example.com/problems/build-20260718-error-42"
}
A client needs a durable category such as quota-exceeded, validation-failed, or account-suspended. The occurrence belongs in instance, logs, and tracing.
Keep title and detail separate
Title summarizes the type and should remain stable across occurrences except for localization.
{
"title": "Request validation failed"
}
Detail explains the current occurrence:
{
"detail": "The start date must be before the end date."
}
Clients should display detail only when the API guarantees it is safe for the end user. They should not parse it. If a client needs a field name, retry time, balance, or error code, expose a typed extension.
Bad client contract:
{
"detail": "Field email has error INVALID_FORMAT"
}
The frontend now has to parse English prose.
Better:
{
"detail": "One request field is invalid.",
"errors": [
{
"pointer": "#/email",
"code": "invalid_format"
}
]
}
The message can change or be translated while pointer and code remain stable.
Status is advisory
The actual HTTP status line remains authoritative for generic HTTP software:
HTTP/1.1 409 Conflict
Content-Type: application/problem+json
If the body includes status, it must match what the origin server generated:
{
"status": 409
}
Why duplicate it? Problem objects may be logged, stored, or forwarded without the original HTTP metadata. The field is convenient in those contexts.
A proxy could change the HTTP status while a stored body still says something else, so clients must not use the JSON member to override transport behavior.
Never return HTTP 200 with a body whose status says 400. That breaks caching, monitoring, SDKs, and every generic HTTP component between the caller and server.
Instance is an occurrence identifier
Instance identifies one particular occurrence, not the problem category.
A useful absolute URI is:
{
"instance": "https://api.example.com/problem-occurrences/req_01JZ8Y"
}
It does not have to be publicly dereferenceable. It can be an opaque identifier that support staff map to a request ID or trace.
If it is dereferenceable, protect it like any other sensitive diagnostic resource. Require authorization and prevent one tenant from reading another tenant’s incident data.
Relative instance values can be confusing because they resolve against the document base URI. If using a relative reference, include a complete path:
{
"instance": "/problem-occurrences/req_01JZ8Y"
}
Do not put stack traces, database keys, email addresses, or access tokens in the URI.
Design validation errors deliberately
Validation often produces several errors that share one problem type. RFC 9457 includes an example of an errors extension with JSON Pointer locations.
A practical contract is:
{
"type": "https://api.example.com/problems/validation-failed",
"title": "Request validation failed",
"status": 422,
"detail": "Two request fields are invalid.",
"errors": [
{
"pointer": "#/email",
"code": "invalid_format"
},
{
"pointer": "#/profile/displayName",
"code": "too_short",
"minimum": 3
}
]
}
Use a documented pointer format. JSON Pointer works well for JSON request bodies and aligns with existing JSON tooling. Use stable codes, not localized strings, for client decisions.
When problems do not share one type, RFC 9457 recommends representing the most relevant or urgent problem rather than combining unrelated failures into a generic batch.
Read JSON Pointer Resolver for testing pointer paths and JSON Schema vs OpenAPI Schema for documenting validation contracts.
Spring Boot implementation
Spring Framework supports ProblemDetail as an RFC 9457 representation. The Spring error response documentation explains its integration with ErrorResponse and ResponseEntityExceptionHandler.
A focused exception handler can translate a domain exception:
@RestControllerAdvice
public final class ApiExceptionHandler {
@ExceptionHandler(OrderStateConflictException.class)
ResponseEntity<ProblemDetail> handleOrderConflict(
OrderStateConflictException exception,
HttpServletRequest request
) {
ProblemDetail problem =
ProblemDetail.forStatus(HttpStatus.CONFLICT);
problem.setType(URI.create(
"https://api.example.com/problems/order-state-conflict"
));
problem.setTitle("Order state conflict");
problem.setDetail(
"The order cannot transition from " +
exception.currentState() +
" to " +
exception.requestedState() +
"."
);
problem.setInstance(URI.create(request.getRequestURI()));
problem.setProperty("errorCode", "order_state_conflict");
problem.setProperty("requestId", request.getHeader("X-Request-Id"));
return ResponseEntity
.status(HttpStatus.CONFLICT)
.contentType(MediaType.APPLICATION_PROBLEM_JSON)
.body(problem);
}
}
The handler maps a public domain condition, not every raw exception. A final fallback handler should log the internal exception and return a generic 500 problem without exception text.
For field validation, map binding errors to a typed list:
record FieldProblem(String pointer, String code) {}
List<FieldProblem> errors = bindingResult.getFieldErrors().stream()
.map(error -> new FieldProblem(
"#/" + error.getField().replace(".", "/"),
error.getCode() == null ? "invalid" : error.getCode()
))
.toList();
problem.setProperty("errors", errors);
Do not expose rejected password, token, or secret values from a binding error.
Node.js and Express implementation
Express recognizes error-handling middleware by its four arguments. The Express error handling guide recommends passing asynchronous failures to the error pipeline.
Define public errors separately from unexpected exceptions:
class ApiProblem extends Error {
constructor({ type, title, status, detail, extensions = {} }) {
super(detail);
this.type = type;
this.title = title;
this.status = status;
this.detail = detail;
this.extensions = extensions;
}
}
function problemDetails(error, req, res, next) {
if (res.headersSent) {
return next(error);
}
const known = error instanceof ApiProblem;
const status = known ? error.status : 500;
if (!known) {
req.log.error({ error, requestId: req.id }, "Unhandled request error");
}
const body = {
type: known
? error.type
: "https://api.example.com/problems/internal-error",
title: known ? error.title : "Internal server error",
status,
detail: known
? error.detail
: "The request could not be completed.",
instance: "/problem-occurrences/" + req.id,
...(known ? error.extensions : {}),
requestId: req.id
};
res.status(status)
.type("application/problem+json")
.json(body);
}
A route can raise a documented problem:
throw new ApiProblem({
type: "https://api.example.com/problems/order-state-conflict",
title: "Order state conflict",
status: 409,
detail: "A shipped order cannot be cancelled.",
extensions: {
errorCode: "order_state_conflict"
}
});
Register the middleware after routes. Keep logging and response serialization centralized so one route cannot accidentally leak a stack trace.
Content negotiation and compatibility
A server can return application/problem+json even when a client listed application/json, depending on normal HTTP negotiation. In practice, document the behavior and test the clients you support.
If an existing API already returns a stable error envelope, migration needs care. Replacing this:
{
"error": "validation_failed",
"message": "The request is invalid."
}
with a problem document can break generated clients and frontend code.
Safer migration options include:
- Introduce problem details in a new API version.
- Negotiate with Accept during a transition.
- Preserve a stable errorCode extension.
- Update OpenAPI schemas and SDKs before switching.
- Measure old-client traffic before removing the legacy shape.
Problem details avoid inventing a new base format; they do not remove the need for compatibility planning.
Read API Versioning Explained and API Deprecation Headers Explained before changing a public error contract.
Security and privacy review
RFC 9457 warns that problem documents can expose attack and privacy information.
Never include:
- Stack traces or source paths.
- Raw SQL or database names.
- Internal service URLs.
- Secrets, tokens, cookies, or Authorization values.
- Password rules that reveal whether a specific account exists.
- Another tenant’s identifiers.
- Detailed dependency failures useful for reconnaissance.
Be careful with validation. Returning that an email is already registered can enable account enumeration. A password-reset endpoint may need the same outward response whether an account exists or not.
Instance URLs and request IDs also require review. They are useful for support but should not reveal sequential internal IDs or grant unauthenticated access to logs.
Log more detail internally, with redaction and access control. Keep the public problem focused on what the client can safely do next.
OpenAPI contract
Define reusable schemas rather than copying fields across responses.
Problem:
type: object
required: [type, title, status]
properties:
type:
type: string
format: uri-reference
title:
type: string
status:
type: integer
minimum: 100
maximum: 599
detail:
type: string
instance:
type: string
format: uri-reference
errorCode:
type: string
Document specific problem types as examples or specialized schemas. List the responses an operation can return and keep the HTTP status aligned with the problem object.
Use the JSON Schema Generator to prototype a schema and OpenAPI Explained for Backend Developers for reusable response components.
Contract and integration tests
Test problem documents as public API behavior:
- Content-Type is application/problem+json.
- HTTP status and body status match.
- Type is the expected stable URI.
- Title is stable for the problem type.
- Detail contains no raw exception text.
- Instance or requestId maps to the internal log entry.
- Validation pointers use the documented format.
- Unknown extension members do not break clients.
- Authentication failures do not leak account existence.
- A fallback 500 response never includes stack traces.
- OpenAPI examples match actual responses.
Also test localization if title or detail changes with Accept-Language. Machine-readable fields must remain stable across languages.
Common mistakes
The first mistake is changing only Content-Type while keeping an undocumented custom body.
The second mistake is using detail as a machine-readable error code.
The third mistake is generating a new type URI for each request or exception class.
The fourth mistake is exposing internal debugging data in extensions.
The fifth mistake is returning 200 while the problem body says 4xx or 5xx.
The sixth mistake is calling an RFC 7807 implementation current without checking RFC 9457 differences and documentation.
The seventh mistake is returning validation values that contain passwords, tokens, or personal data.
Practical recommendation
Start with a small registry of documented public problem types. For each type, define:
- Stable type URI and title.
- Expected HTTP status.
- Safe detail guidance.
- Typed extension fields.
- Retry behavior.
- Security considerations.
- OpenAPI example.
- Contract tests.
Centralize serialization in Spring advice or Express error middleware, preserve request correlation, and keep a generic safe fallback for unexpected exceptions. Problem details work best as a deliberate API vocabulary, not an automatic dump of the exception hierarchy.
Related reading
Read REST API Error Handling Best Practices, REST API Status Codes Explained, and API Authentication Best Practices for the surrounding error contract.
For adjacent operational standards, read API Rate Limit Headers Explained and API Deprecation Headers Explained. Review RFC 9457, the Spring ProblemDetail documentation, and the Express error handling guide before implementation.