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

Problem Details Bundle Laravel Package

mnavarrocarter/problem-details-bundle

A Symfony bundle for RFC 7807 problem details that turns exceptions and validation errors into consistent application/problem+json API responses with configurable type, title, status, detail, and custom extension fields. Ideal for standardizing HTTP API error handling.

View on GitHub
Deep Wiki
Context7

Getting Started

Reality check before you start. 0.1.0 is the initial release — there is no prior stable line to upgrade from, no deprecations, and nothing that could break you. What the tag does tell you is the date: an April 2018 first release with no stars and no dependents means the initial cut is also, in practice, the final one. It targets the Symfony 3.3–4.0 / PHP 7.1 era, so on any modern stack (PHP 8.1+, Symfony 6/7) composer require will almost certainly fail on platform or symfony/* constraints, and if you force it you inherit an unmaintained listener running in your exception path. Treat the steps below as "how to evaluate and trial it," and read the last section before committing to production.

The release notes are worth reading closely, because they define the whole surface area in two bullets: an exception listener, and serialization of common exceptions — Doctrine, HttpFoundation and Symfony Security. That second bullet is the actual selling point and the thing to test first.

1. Verify it installs at all.

composer require mnavarrocarter/problem-details-bundle --dry-run

If that resolves, you're on an old enough app that the rest is straightforward. If it doesn't, stop here — the concepts still transfer to a hand-rolled listener.

2. Register the bundle.

Symfony 4 Flex (config/bundles.php):

return [
    // ...
    MNavarroCarter\ProblemDetailsBundle\ProblemDetailsBundle::class => ['all' => true],
];

Symfony 3.x (app/AppKernel.php):

$bundles = [ /* ... */ new MNavarroCarter\ProblemDetailsBundle\ProblemDetailsBundle() ];

3. First test: throw the exceptions it already knows about. Before you write a line of config, find out what 0.1.0 serializes out of the box. Hit an API route and trigger, in order:

  • a NotFoundHttpException (HttpFoundation) — expect a 404 problem document;
  • a Doctrine EntityNotFoundException / NotFoundHttpException from a repository lookup — expect 404;
  • a BadRequestHttpException or MethodNotAllowedHttpException — expect 400/405;
  • a Symfony\Component\Security\Core\Exception\AccessDeniedException — expect 403 (see the firewall caveat in Gotchas);
  • one of your own domain exceptions — expect the fallback behaviour (HTML, or a generic 500) until you map it.
curl -i -H 'Accept: application/json' http://localhost:8000/api/widgets/42
{"type":"https://example.com/problems/widget-not-found",
 "title":"Widget not found",
 "status":404,
 "detail":"No widget with id 42",
 "instance":"/widgets/42"}

Whatever that zero-config probe produces is your real baseline. Everything else is configuration on top.

4. Where to look first. The repo is small and thinly documented, so read the source in this order:

  • DependencyInjection/Configuration.php — the real documentation: the config key (expect problem_details:), the debug flag, default type URI, and how the common-exception map is expressed/overridable.
  • The listener in EventListener/ — shows you how an exception becomes a response, at what priority, and where the Doctrine/HttpFoundation/Security branches live.
  • The normalizers/serializers for those three families — this is where 0.1.0's second bullet is implemented, and where you'll want to hook in to scrub or extend output.
  • Any Exception/ or Problem/ namespace — the exception types you're expected to throw and the interface your own exceptions should implement.

5. First real use case — kill the HTML error page on API routes. Throw a domain exception from a controller and let the listener render it:

public function show($id)
{
    $widget = $this->widgets->find($id);
    if (!$widget) {
        throw new WidgetNotFound($id); // map in config, or implement the bundle's interface
    }
    return $this->json($widget);
}

If you still get HTML, the listener isn't firing (wrong bundle registration, or your firewall's ExceptionListener handled it first — see Gotchas).

Implementation Patterns

Start from the built-in map, don't reimplement it. The headline feature of 0.1.0 is that Doctrine, HttpFoundation and Symfony Security exceptions already become problem documents. So the pattern is: configure/scope the listener, then write only mapping for your own domain exceptions. If you find yourself writing a normalizer for NotFoundHttpException, stop — you're duplicating the bundle and you'll fight it on priority.

Treat the Doctrine branch as convenient-but-dangerous. Auto-serializing Doctrine exceptions is a huge day-to-day win (no more 500s with stack traces for a missing entity) and simultaneously the biggest liability in the package: DBAL and ORM exception messages routinely contain SQL fragments, table/column names, parameter values, connection DSNs. Wire the debug flag to %kernel.debug% so detail is verbose locally and generic in prod, and spot-check the actual JSON for a UniqueConstraintViolationException and a DriverException before you ship.

Map domain exceptions, don't build responses in controllers. Define one exception per failure mode and let configuration (or an interface method returning status/type) supply the problem shape. Keep detail machine-generated and safe; put human-facing copy in title.

Use type as a stable, documented URI. Point it at real docs (https://api.example.com/problems/validation-failed). Clients switch on type, not title, so never change a type URI after publishing it — add a new one instead. Note the built-in families will have the bundle's notion of type unless you override it; decide early whether you accept those URIs or normalise everything to your own namespace, and document the answer.

Validation errors → 422 with an extension member. RFC 7807 lets you add arbitrary members; this is where field-level errors belong:

{
  "type": "https://api.example.com/problems/validation-failed",
  "title": "Validation failed",
  "status": 422,
  "errors": {"email": ["This value is not a valid email address."]}
}

Catch the framework's ValidationException (or your own wrapper) in a small mapping layer and attach violations as extensions, rather than letting each form/validator leak its own format.

Scope it to the API. Wrap the listener's work in a guard so browser routes still get Twig error pages and 302 redirects:

if (!str_starts_with($request->getPathInfo(), '/api')) {
    return; // let Symfony's normal exception handling continue
}

Otherwise every 404 in the UI becomes a JSON blob — a classic half-day-of-debugging mistake. This matters more with 0.1.0 than with a hand-rolled listener, because the pre-mapped HttpFoundation/Security branches will happily convert front-end 404s and access-denied redirects too.

Always honour Accept. Only emit application/problem+json when the client asked for JSON (or */* from an API route). A curl with Accept: text/html should still get HTML — verify this explicitly, since built-in normalizers tend to be unconditional.

Log at the same seam. In the same listener (or a decorator around it), log 5xx problems with a correlation/request id and put that id in an extension member (trace_id). That turns "user reports a 500" into a one-query investigation, and it's the single highest-value thing to add beyond what the bundle gives you.

Test the contract, not the exception class.

$client->request('GET', '/api/widgets/42');
$this->assertResponseStatusCodeSame(404);
$this->assertResponseHeaderSame('Content-Type', 'application/problem+json');
$this->assertJsonStringEqualsJsonString('{"type":"...","title":"...","status":404,...}', $client->getResponse()->getContent());

These tests survive the inevitable day you replace the bundle with a native listener, which is exactly why they're worth writing. Add one test per built-in family too (Doctrine not-found, HttpFoundation bad-request, Security access-denied) so you have a spec for reimplementing them later.

In dev, keep errors verbose; in prod, strip them. Use the bundle's debug flag (usually wired to %kernel.debug%) or your own guard so detail/stack traces never reach prod responses while staying useful on your machine.

Gotchas and Tips

  • It is abandoned. This is the dominant fact about the package, and 0.1.0 being an "Initial Release" makes it starker: there is no mature line, no follow-up patch, no maintenance commitment implied by the tag. Symfony 6.3+ ships a built-in ProblemNormalizer that turns HttpExceptionInterface into application/problem+json; Nelmio's ApiProblemBundle is the maintained community equivalent; API Platform has its own. On a modern Symfony the right "day-to-day use" of this bundle is to read its listener and its Doctrine/HttpFoundation/Security normalizers as a reference implementation and write ~150 lines yourself. You get the same output, no abandoned dependency, and full control over what leaks out of Doctrine messages.
  • The Doctrine/Security auto-serialization is the leak vector. Framework exception messages are written for developers staring at a stack trace, not for API consumers. Anything the bundle puts in detail for those families should be assumed hostile until you've read the normalizer and confirmed a debug gate. Sanitise or override before going anywhere near production.
  • Listener priority and the security firewall are the #1 source of "it doesn't work." Symfony's security ExceptionListener handles AccessDeniedException/AuthenticationException (redirects for HTML, 401 for JSON) and may return before yours runs — which means the bundle's Security serialization may never actually fire for the cases you care about. Verify ordering with debug:event-dispatcher kernel.exception and set an explicit priority if needed. Pick one of: the firewall's entry point or the bundle.
  • Exceptions thrown outside the request/kernel cycle (console command, kernel.boot listener, PHP fatal, OOM) never reach kernel.exception. Your error envelope is not a universal safety net.
  • application/problem+json is not application/json. Strict HTTP clients, some SDKs and a few API gateways will reject or fail to parse it. If your consumers are inflexible, configure the bundle (or your listener) to emit application/json with the same body — RFC 7807 §3 permits it, and this is often the pragmatic call.
  • Extension members must survive serialization. If you attach objects (raw ConstraintViolation instances, Doctrine entities) they may serialize into something unusable or throw. Normalise extensions to scalars/arrays before they hit the response.
  • No stack traces in the payload, ever. The single most common misconfiguration is leaving debug on in staging and shipping absolute file paths and SQL to clients.
  • instance is decoration, not an identifier. It's conventionally the request URI; don't let clients treat it as a resource key.
  • Conflicts with API Platform / other exception listeners. Two listeners both building a response means the winner is decided by priority, and you'll debug output that doesn't match your config. Run only one problem-details layer.
  • CORS. If the bundle short-circuits an exception before your CORS listener adds headers, browser clients see an opaque network error instead of your nicely shaped 4xx. Make sure the response goes through the CORS handling path.
  • Extension points to prefer if you do adopt it: a decorator around the listener (to add logging, trace_id, and to post-process the built-in Doctrine/Security output without forking), a custom normalizer/serializer for extension members, and a marker interface on your exceptions so mapping lives in code rather than YAML. Prefer decorating over editing vendor code — you will need to migrate off this bundle eventually, and a thin decorator makes that a delete rather than a rewrite.
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.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings