If you’ve ever parsed an API error response and found yourself writing conditional logic to handle five different error shapes, you already know why RFC 7807 exists.
What Is RFC 7807?
RFC 7807 — Problem Details for HTTP APIs — defines a standard JSON format for communicating machine-readable error information in HTTP responses. The media type is application/problem+json.
A problem details response looks like this:
{
"type": "https://api.example.com/problems/insufficient-credits",
"title": "Insufficient credits",
"status": 422,
"detail": "Your account has 3 credits remaining, but this operation requires 10.",
"instance": "/documents/abc-123/generate"
}
Each field has a specific purpose:
| Field | Required | Purpose |
|---|---|---|
type |
Yes | A URI that identifies the problem type. Clients can switch on this value. |
title |
Yes | A short, human-readable summary. Should not change between occurrences. |
status |
No | The HTTP status code (duplicated here for convenience when the body is logged without headers). |
detail |
No | A human-readable explanation specific to this occurrence. |
instance |
No | A URI identifying the specific occurrence (useful for correlating with logs). |
You can extend the object with additional members. For example, you might add a validation_errors array for 400 responses, or an error_code string for internal tracking.
Why Adopt It?
The short answer: predictability.
Without a standard, every endpoint invents its own error shape. One returns {"error": "not found"}, another returns {"message": "Not found", "code": 404}, and a third returns {"errors": [{"field": "email", "msg": "invalid"}]}. Your client code ends up full of defensive parsing for every possible shape.
With RFC 7807, every error response follows the same structure. Client code can:
- Route on
type— use the URI as a stable identifier to drive UI behaviour (show a specific dialog, redirect to a payment page, retry with backoff). - Display
titleanddetail— present human-readable context without guessing which field contains the message. - Log
instance— correlate a user-reported error to a specific request in your backend logs. - Extend predictably — extra fields are always siblings of the standard fields, never nested in an unpredictable way.
Where Does the Response Come From?
A common misconception is that application/problem+json is an API Gateway feature. It isn’t. The format is returned by your application code — your Lambda handler, your Express middleware, your FastAPI exception handler.
API Gateway has its own error responses (throttling, authoriser denials, malformed requests), but those use Gateway Response templates with a different format. When you control the handler, you control the response body and the Content-Type header — that’s where RFC 7807 lives.
Here’s what this looks like in a Python Lambda handler:
import json
def problem_response(status, problem_type, title, detail=None, instance=None, **extra):
body = {
"type": problem_type,
"title": title,
"status": status,
}
if detail:
body["detail"] = detail
if instance:
body["instance"] = instance
body.update(extra)
return {
"statusCode": status,
"headers": {
"Content-Type": "application/problem+json",
},
"body": json.dumps(body),
}
And a caller:
def handler(event, context):
document_id = event["pathParameters"]["id"]
document = get_document(document_id)
if not document:
return problem_response(
status=404,
problem_type="https://api.example.com/problems/document-not-found",
title="Document not found",
detail=f"No document exists with ID '{document_id}'.",
instance=f"/documents/{document_id}",
)
# ... success path
Validation Errors
RFC 7807 works well with validation errors by extending the base object. A common pattern:
{
"type": "https://api.example.com/problems/validation-error",
"title": "Validation failed",
"status": 400,
"detail": "The request body contains invalid fields.",
"errors": [
{
"field": "email",
"message": "Must be a valid email address."
},
{
"field": "name",
"message": "Must be between 1 and 100 characters."
}
]
}
The errors array is a custom extension — RFC 7807 permits any additional members as long as they don’t conflict with the standard fields.
Gateway Errors vs Application Errors
It’s worth being explicit about the boundary:
- API Gateway errors (429 throttle, 403 authoriser denial, 500 integration timeout) — these are generated by the gateway before your code runs. You can customise their shape using Gateway Response templates, but they typically use a simpler
{"message": "..."}format. - Application errors (400 validation, 404 not found, 409 conflict, 422 business rule violation) — these are returned by your handler. This is where RFC 7807 applies.
If you want full consistency, you can configure API Gateway’s Gateway Response templates to also return application/problem+json, but the real value is in your application layer where you have the context to populate detail, instance, and custom extension fields.
Adopting It Incrementally
You don’t need to retrofit every endpoint at once. A practical adoption path:
- Create a shared helper — a
problem_response()function (or equivalent in your framework) that builds the response with the correctContent-Typeheader. - Start with new endpoints — every new endpoint returns
application/problem+jsonfor errors from day one. - Migrate existing endpoints — when you next touch an endpoint, update its error responses.
- Document your
typeURIs — maintain a registry of problem types so clients can rely on them.
The type URIs don’t need to resolve to a real page (though it’s nice if they do). They just need to be stable identifiers that clients can match on.
When Not to Use It
RFC 7807 is designed for HTTP APIs returning JSON. It’s not the right fit for:
- HTML error pages — browsers expect HTML, not JSON.
- gRPC or WebSocket protocols — these have their own error conventions.
- Streaming responses — once you’ve started streaming a 200 response, you can’t switch to a problem details object mid-stream.
For REST and REST-like HTTP APIs, though, it’s the standard worth adopting.
Summary
RFC 7807 gives you a single, predictable shape for every error your API returns. Clients get stable type URIs to route on, human-readable title and detail fields to display, and extensibility for domain-specific fields like validation errors. The format lives in your application code — your Lambda handlers, your middleware — not in the API gateway. And you can adopt it incrementally, starting with new endpoints and backfilling as you go.
If you have control over your API, structured error responses are one of the simplest improvements you can make for your consumers.