Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Api Problem Laravel Package

crell/api-problem

A framework-agnostic PHP library implementing RFC 9457 (formerly RFC 7807) Problem Details for HTTP APIs. It provides structured problem objects, exceptions, and response helpers for consistent, machine-readable API error responses.

View on GitHub
Deep Wiki
Context7

Getting Started

composer require crell/api-problem

There is nothing to publish, no service provider, no config file. It's a single small class (Crell\ApiProblem\ApiProblem) that models an RFC 7807 "problem details" document and knows how to serialize itself to JSON. Read src/ApiProblem.php first — it's short enough to read end-to-end in five minutes, and that will tell you more than the README.

The first use case is almost always "my API returns a bare {"message": "Not found"} and clients can't tell errors apart." Build the problem in the controller and return it with the correct media type:

use Crell\ApiProblem\ApiProblem;

public function show(string $id)
{
    $order = Order::find($id);

    if (! $order) {
        $problem = (new ApiProblem())
            ->setType('https://api.example.com/problems/order-not-found')
            ->setTitle('Order not found')
            ->setStatus(404)
            ->setDetail("No order exists with id {$id}.")
            ->setInstance(request()->fullUrl());

        return response((string) $problem, 404)
            ->header('Content-Type', 'application/problem+json');
    }

    return $order;
}

(string) $problem yields the JSON body; json_encode($problem) works too because the class implements JsonSerializable, so response()->json($problem, 404)->header('Content-Type', 'application/problem+json') is equally valid and slightly more Laravel-idiomatic.

Implementation Patterns

Centralize construction. Don't sprinkle new ApiProblem() through controllers. Create a small factory (or a handful of named subclasses) so type URIs, titles, and extension members stay consistent:

final class RateLimitedProblem extends ApiProblem
{
    public function __construct(int $retryAfter)
    {
        parent::__construct();
        $this->setType('https://api.example.com/problems/rate-limited')
            ->setTitle('Rate limit exceeded')
            ->setStatus(429)
            ->setDetail('Too many requests. Try again shortly.');
        $this['retry_after'] = $retryAfter;
    }
}

Subclassing is the intended extension point: the class extends ArrayObject, so $problem['retry_after'] = ... adds arbitrary extension members to the payload for free.

Wire it into the exception handler. In Laravel 11's bootstrap/app.php, map domain exceptions to problems so every API error has one shape:

->withExceptions(function (Exceptions $exceptions) {
    $exceptions->render(function (OrderNotFoundException $e, Request $request) {
        if (! $request->is('api/*')) {
            return null; // fall through to the default HTML/JSON handler
        }

        $problem = (new ApiProblem())
            ->setType('https://api.example.com/problems/order-not-found')
            ->setTitle('Order not found')
            ->setStatus(404)
            ->setDetail($e->getMessage())
            ->setInstance($request->fullUrl());

        $problem['trace_id'] = $request->header('X-Request-Id');

        return response((string) $problem, 404)
            ->header('Content-Type', 'application/problem+json');
    });
})

Do the same for ValidationException. Put $e->errors() into an extension member ($problem['errors'] = $e->errors();) with status 422. This is the single highest-value integration: it's where clients spend most of their error-handling code, and it keeps the standard fields (title, detail) free for humans.

Use instance for correlation, not navigation. The spec calls it "a URI reference that identifies the specific occurrence." A trace ID (urn:uuid:...) or the full request URL plus a log correlation key is more useful than the route path alone.

Gate on content negotiation, not route prefix. If a client sends Accept: application/problem+json, honour it; a shouldRenderJsonWhen rule or a small middleware is a cleaner switch than $request->is('api/*') scattered everywhere.

Gotchas and Tips

  • The status field is advisory. Laravel will happily return HTTP 200 with "status": 404 in the body if you forget the second argument. Set both, and consider a test asserting they agree.
  • Content-Type: application/problem+json is not optional. response()->json() sets application/json; overwrite it. Generic RFC 7807 clients key off the media type and will ignore a application/json payload.
  • type defaults to about:blank. Per the spec, that means "no semantics beyond the HTTP status" — and in that case title should just be the status reason phrase. Don't ship about:blank and a rich custom detail if you expect machine handling; mint a real type URI instead.
  • Don't leak internals in detail. It's rendered to end users. Stack traces, SQL, and model IDs belong in logs, not here.
  • The object is mutable and setters return $this. If you bind a pre-built "template" problem in the container or a static property and call setDetail() on it, you'll leak state between requests — especially under Octane. Always construct fresh (or clone a prototype) per response.
  • Reserved member names. Extension keys go through offsetSet; don't collide with type, title, status, detail, or instance unless you're deliberately overriding. Nested arrays serialize fine; objects need to be JsonSerializable.
  • Debugging: if the body looks wrong, json_encode($problem) in tinker and inspect — the ArrayObject storage plus the protected standard fields can surprise you. json_last_error() after encoding is your friend if a stray resource or closure slipped into an extension member.
  • RFC 9457 (2023) obsoletes RFC 7807. Wire format is compatible, but the package name and docs reference the older spec. Worth a comment in your own code so the next reader isn't confused by the naming.
  • You own all the glue. With zero dependents and a tiny surface, there is no Laravel adapter, no make:problem command, no config. That's fine — the class is small enough to fork and vendor if you need a change — but budget an afternoon for the factory, handler wiring, and tests rather than expecting composer require to be the whole job.
  • Test the contract, not the class. assertStatus, assertHeader('Content-Type', 'application/problem+json'), and assertJsonPath('type', ...) give you far more safety than unit-testing ApiProblem itself.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core
nodisrupt/laravel-agent
sierratecnologia/media-manager