Problem Details instead of inconsistent error responses
RFC 7807 defines a single, predictable JSON format for error responses: type, title, status, detail and instance. This article shows how a PHP API implements this format consistently, from the Problem Details class through the global exception handler to business validation errors with their own extension fields.
Table of Contents
- 1. Why inconsistent error formats are a problem
- 2. What RFC 7807 prescribes
- 3. The core fields: type, title, status, detail, instance
- 4. Extension fields for business validation errors
- 5. Building a ProblemDetails class in PHP
- 6. A global exception handler for Problem Details
- 7. Setting the application/problem+json content type correctly
- 8. Modeling error types as dereferenceable URIs
- 9. Ad hoc error format vs. RFC 7807 compared
- 10. Summary
- 11. FAQ
1. Why inconsistent error formats are a problem
Most PHP APIs develop their error format organically: the first endpoint returns {"error": "not found"}, the second {"message": "invalid input"}, a third {"errors": ["field required"]}. After a year of development, every team in the project has established its own error format, and every client has to write its own error handling for every endpoint. This is exactly the problem that RFC 7807 solves, the official format for Problem Details in HTTP APIs.
Without a unified error format, growing PHP projects accumulate a quiet but costly maintenance burden: every new error source gets a new ad hoc structure, every frontend team has to model additional special cases in its own error handling. RFC 7807 ends this fragmentation by defining a single, extensible JSON schema for all error responses, regardless of whether the error concerns validation, authorization, or an internal server problem.
The practical benefit shows especially with multiple consuming teams: once an API responds with RFC 7807, every client team can use the same generic error handling logic instead of writing custom parsing rules per endpoint. This reduces integration effort and turns error handling itself into a stable, documented part of the API contract.
2. What RFC 7807 prescribes
RFC 7807, officially "Problem Details for HTTP APIs", defines a JSON object with five standardized fields that together describe an error unambiguously. The specification deliberately does not fix which business specific extra information an error may contain, it only defines the shared frame and the matching media type application/problem+json. This openness makes RFC 7807 flexible enough for simple 404 responses and complex validation errors with several field errors at once.
A central idea of the specification: the HTTP status code alone is rarely enough to make an error understandable for humans and machines alike. A 422 says nothing about which field was invalid, a 403 says nothing about which permission is missing. RFC 7807 closes exactly this gap between status code and actual cause, without changing HTTP semantics itself.
3. The core fields: type, title, status, detail, instance
The type field is a URI that identifies the error type, defaulting to about:blank when no more specific type exists. The title field is a short, human readable summary of the error type and should not change between multiple occurrences of the same error type. The status field mirrors the HTTP status code from the response, but is included redundantly in the body so that logging systems and clients can evaluate it without access to the HTTP header.
The detail field contains the concrete, incident specific error description, unlike title, which stays generic. The instance field is a URI identifying the concrete incident, often used for a request ID or a correlation identifier that lets the error be found later in logs. Together these five fields form the foundation of every RFC 7807 response, additional fields may be added freely according to the specification.
{
"type": "https://api.example.com/problems/insufficient-stock",
"title": "Insufficient stock",
"status": 409,
"detail": "Only 2 units of SKU MS-4821 are available, but 5 were requested.",
"instance": "/orders/attempts/9f3a2c1e"
}
4. Extension fields for business validation errors
The real strength of RFC 7807 shows with validation errors affecting several fields. The specification allows arbitrary extension fields next to the five core fields, in practice a field named errors has established itself for this, a list of objects with field name and associated error message. This pattern is not part of the official specification, but has practically become an unofficial standard, among other things because frameworks like Spring and ASP.NET Core implement it the same way.
It is important that extension fields are documented and named consistently across every endpoint of a PHP API. Whoever calls it errors in one endpoint and fieldErrors in the next forfeits exactly the consistency gain that RFC 7807 is supposed to provide. A shared base class for all Problem Details responses ensures that extension fields are named and structured uniformly project wide.
{
"type": "https://api.example.com/problems/validation-error",
"title": "Validation failed",
"status": 422,
"detail": "One or more fields failed validation.",
"instance": "/customers/register/attempts/7c2e1a90",
"errors": [
{ "field": "email", "message": "Must be a valid email address." },
{ "field": "password", "message": "Must be at least 12 characters long." }
]
}
5. Building a ProblemDetails class in PHP
The practical starting point for RFC 7807 begins with a single, immutable value object that encapsulates the five core fields plus arbitrary extension fields. This class also handles serialization to JSON and creating the matching HTTP response with the correct status code and content type, so controller code only needs to build and return a ProblemDetails object.
Construction via static factory methods like ProblemDetails::notFound() or ProblemDetails::validationFailed() keeps calling code short and prevents slightly different field names or status codes for the same logical error from appearing at different places in the project.
<?php
declare(strict_types=1);
/**
* Immutable RFC 7807 Problem Details value object.
*/
final class ProblemDetails
{
/**
* @param array<int, array{field: string, message: string}> $errors
*/
private function __construct(
private readonly string $type,
private readonly string $title,
private readonly int $status,
private readonly string $detail,
private readonly ?string $instance = null,
private readonly array $errors = [],
) {
}
public static function validationFailed(array $errors, ?string $instance = null): self
{
return new self(
type: 'https://api.example.com/problems/validation-error',
title: 'Validation failed',
status: 422,
detail: 'One or more fields failed validation.',
instance: $instance,
errors: $errors,
);
}
public static function notFound(string $resource, ?string $instance = null): self
{
return new self(
type: 'https://api.example.com/problems/not-found',
title: 'Resource not found',
status: 404,
detail: "The requested {$resource} could not be found.",
instance: $instance,
);
}
public static function internalServerError(?string $instance = null): self
{
return new self(
type: 'about:blank',
title: 'Internal Server Error',
status: 500,
detail: 'An unexpected error occurred while processing the request.',
instance: $instance,
);
}
/**
* @return array<string, mixed>
*/
public function toArray(): array
{
$payload = [
'type' => $this->type,
'title' => $this->title,
'status' => $this->status,
'detail' => $this->detail,
];
if ($this->instance !== null) {
$payload['instance'] = $this->instance;
}
if ($this->errors !== []) {
$payload['errors'] = $this->errors;
}
return $payload;
}
public function status(): int
{
return $this->status;
}
}
6. A global exception handler for Problem Details
So that not every controller has to build a ProblemDetails object manually, a central exception handler pays off that automatically translates business exceptions into RFC 7807 responses. Every business exception implements a small interface for this with a method that returns the matching ProblemDetails object, the handler calls this method and writes the response.
For unexpected, non business exceptions, the handler returns a generic 500 response with minimal detail text, so as not to expose internal implementation details. This two tier approach, business exceptions with their own problem type and everything else as a generic internal error, is the basic pattern for robust error handling with RFC 7807 in PHP.
<?php
declare(strict_types=1);
interface ProblemAwareException
{
public function toProblemDetails(): ProblemDetails;
}
/**
* Central exception handler translating exceptions into RFC 7807 responses.
*/
final class ProblemDetailsExceptionHandler
{
public function handle(\Throwable $exception): void
{
$problem = $exception instanceof ProblemAwareException
? $exception->toProblemDetails()
: ProblemDetails::internalServerError();
http_response_code($problem->status());
header('Content-Type: application/problem+json');
echo json_encode($problem->toArray(), JSON_UNESCAPED_SLASHES);
}
}
7. Setting the application/problem+json content type correctly
A common mistake in PHP APIs that implement RFC 7807 only partially: the error response has the correct JSON structure, but is still delivered with Content-Type: application/json. The specification explicitly requires application/problem+json, so clients can distinguish successful responses from error responses purely by media type, without having to check the status code separately.
In practice, it pays off to set this content type centrally in the exception handler and never repeat it in individual controllers. Middleware based frameworks usually offer a central error responder for this, registered exactly once during bootstrapping, so every error response of the API automatically carries the correct media type, regardless of which endpoint triggered the error.
8. Modeling error types as dereferenceable URIs
The specification suggests that the type URI ideally be dereferenceable, meaning it actually points to a page with human readable documentation for that error type. In practice, many PHP APIs implement this only partially, often the URI remains a pure identifier without a real target page. This is permitted according to the specification, but does not exploit the full potential of RFC 7807.
A pragmatic middle ground for PHP projects: set up an internal error documentation page per project, describing every used error type with an example response and possible causes. The type URI then points to this page, so support and frontend teams can look up an unfamiliar error type directly instead of having to search the code for the triggering location.
9. Ad hoc error format vs. RFC 7807 compared
The following table contrasts a typical, historically grown ad hoc error format with an RFC 7807 compliant response.
| Aspect | Ad hoc format | RFC 7807 |
|---|---|---|
| Consistency across endpoints | Varies per team/endpoint | One schema for all errors |
| Media type | application/json | application/problem+json |
| Machine readable error type | Usually free text only | Unambiguous type URI |
| Traceability in logs | No standard field for instance ID | instance field for correlation ID |
| Client error handling | Custom per endpoint | Generic, reusable logic |
Migrating an existing system to RFC 7807 is rarely a big bang rewrite, it usually suffices to introduce the central exception handler and the ProblemDetails class, consistently apply them to new endpoints, and migrate existing endpoints incrementally.
Mironsoft
PHP API design, error handling and backend architecture
Need a unified error format for your PHP API?
We introduce RFC 7807 into existing PHP APIs, build the ProblemDetails class, the global exception handler, and document every error type consistently for all client teams.
Error format audit
Checking existing endpoints for inconsistent error formats
RFC 7807 rollout
Setting up ProblemDetails class and exception handler production ready
Documentation
Documenting error types as dereferenceable pages for support and partners
10. Summary
RFC 7807 solves a problem present in almost every grown PHP API: inconsistent, team specific error formats that make client integrations unnecessarily costly. The five core fields type, title, status, detail and instance, together with project wide consistent extension fields such as errors, form a complete, extensible format for every kind of error, from simple 404 responses to complex validation errors with multiple affected fields.
The cleanest implementation in PHP goes through a central ProblemDetails value object and a global exception handler that automatically translates business exceptions into the matching RFC 7807 response while consistently setting the content type application/problem+json. Whoever establishes this foundation once saves the recurring discussion about the correct error format for every new endpoint.
RFC 7807 in PHP: The Key Points at a Glance
Core fields
type, title, status, detail, instance together form a complete, standardized error object.
Content type
application/problem+json must be set on every error response, centrally in the exception handler, never in the controller.
Extension
A consistently named errors field covers business validation errors with multiple affected fields.
Implementation
A ProblemDetails class plus a central exception handler instead of ad hoc error arrays in every controller.