Problem documents
An API that answers its errors in a shape of its own makes every client learn that shape.
RFC 9457 defines one shape for all of them, the problem
document, served as application/problem+json:
{
"type": "about:blank",
"title": "Not Found",
"status": 404
}
type identifies the kind of problem and title names it, the same for every occurrence. detail
says what happened this time and instance which occurrence it was. A problem without a type of
its own is about:blank, and its title is the phrase of its status code.
Which requests get one
The package answers the failures of an API request with a problem document: a request under one of
the prefixes in http-api.problems.route_prefixes, by default api, and a request that expects
JSON or asks for application/problem+json. Every other request is answered as Laravel answers it,
so an HTML page stays an HTML page.
| The failure | The answer |
|---|---|
| a validation | 422, or the status the validation was given, with the fields and their messages in errors |
| an unauthenticated request | 401, with WWW-Authenticate: Bearer, the scheme http-api.problems.www_authenticate names |
an HTTP exception, abort(403, '…') and a throttled request among them | its own status and headers, Retry-After included, and the message it was thrown with as detail |
| a refused authorization | 403, or the status the policy gave it, with the policy's answer as detail |
| a missing model or a missing route | 404, without the message of a missing model, which names its class and its key |
| an exception that provides its own problem | that problem, see below |
| an exception that carries its own response, and a validation given one | that response, as it is |
| anything else | 500, with a sentence and nothing the exception said |
With app.debug on, an exception nobody anticipated is left to Laravel, whose debug answer is what
a developer wants to read while building.
A problem of your own
An exception that implements ProvidesProblem is answered with the document it returns:
use Pushery\HttpApi\Problems\Problem;
use Pushery\HttpApi\Problems\ProvidesProblem;
final class OutOfStock extends RuntimeException implements ProvidesProblem
{
public function toProblem(): Problem
{
return new Problem(
409,
type: 'https://example.com/problems/out-of-stock',
title: 'Out of stock',
detail: 'Two of the items are no longer available.',
members: ['items' => [3, 7]],
);
}
}
Members beside the five standard ones are extension members, written after them. An extension member
can never replace a standard one, so a member named status cannot change the status a client
reads.
A controller that answers a problem without throwing returns one directly:
use Pushery\HttpApi\Problems\Problem;
return (new Problem(429, detail: 'Try again in a minute.', headers: ['Retry-After' => '60']))->toResponse();
The package's own problems
The problems the idempotency middleware answers with carry a type of their own, which starts with
http-api.problems.type_base. By default that is the page that documents each one:
- idempotency-key-missing,
400 - idempotency-key-malformed,
400 - idempotency-key-in-flight,
409 - idempotency-answer-not-kept,
409 - idempotency-key-reused,
422 - idempotency-scope-missing,
500
Switching it off
HTTP_API_PROBLEMS=false leaves every failure to Laravel. An application with an exception handler
of its own, one without Laravel's render callbacks, gets no renderer from the package and calls
ProblemRenderer::render() itself, which answers a failure or returns null for a request that is
no API request.