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 Bundle Laravel Package

libero/api-problem-bundle

Libero API Problem Bundle adds RFC 7807 Problem Details support to Symfony APIs, helping you return consistent, machine-readable error responses. Map exceptions to standard problem types and keep JSON error payloads predictable across services.

View on GitHub
Deep Wiki
Context7

Getting Started

This is a Symfony bundle, not a Laravel package — it hooks Symfony's kernel.exception event to render exceptions as application/problem+json. If you're coming from Laravel, read it as "the Handler::render() equivalent, packaged as a listener + problem factory" and borrow the patterns rather than expecting a config/api-problem.php.

Minimum steps

  1. Confirm you're on a Symfony version the bundle supports (it predates Symfony 5; assume 3.4/4.x unless you've verified otherwise).
  2. Install and pin — do not use a floating constraint:
    composer require libero/api-problem-bundle:0.2.0
    
  3. Register the bundle:
    // Symfony 4+ config/bundles.php
    Libero\ApiProblemBundle\ApiProblemBundle::class => ['all' => true],
    
    // Symfony 3.x app/AppKernel.php
    new Libero\ApiProblemBundle\ApiProblemBundle(),
    
  4. Smoke-test with a route that throws:
    curl -i -H 'Accept: application/problem+json' https://localhost/api/nope
    
    Expect HTTP/1.1 404, Content-Type: application/problem+json, and a body with type, title, status, detail, instance.

Where to look first (day one)

  • The problem value object and problem type classes — this is your extensibility surface; everything else is plumbing.
  • The kernel.exception listener — check its priority. If Symfony's default ExceptionListener/ErrorListener still wins, you'll get HTML or the debug page.
  • The exception-to-problem mapping (factory/resolver) — where you decide which exception becomes which type URI.

First real use case: stop returning ad-hoc {"error": "..."} shapes. Define one custom exception per client-visible failure (ResourceNotFound, InvalidStateTransition, QuotaExceeded), map each to a stable type URI, throw them from controllers/services, and let the bundle serialize them. Clients then branch on type, not on English detail text.

Implementation Patterns

1. Exception-per-problem, stable type URIs Treat type as part of your public API contract. Point them at documentation pages you actually host:

throw new ProblemException(
    type: 'https://api.example.com/problems/quota-exceeded',
    title: 'Quota exceeded',
    status: 429,
    detail: sprintf('Plan %s allows %d requests/day.', $plan, $limit),
);

Version the URIs, never the wording. Changing title should be non-breaking; changing type is breaking.

2. Map framework exceptions explicitly Don't let Symfony's NotFoundHttpException leak its own title/detail if you want consistent copy. Add a small resolver/listener that translates the handful of Symfony exceptions you actually emit (NotFoundHttpException, AccessDeniedException, MethodNotAllowedHttpException, HttpExceptionInterface generally) into your problem types, preserving the original status code.

3. Validation → violations extension For ConstraintViolationListInterface, emit the RFC 7807bis-style extension and a single problem type:

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

Normalize propertyPath to dot/bracket form (items[3].sku) at the boundary — Symfony gives you the raw path, and clients build form-field mappings from it. Never localize message server-side if clients render their own copy; ship a stable code alongside it if you need i18n.

4. Extensions for operational context Use extension members (traceId, retryAfter, requestId, docs) rather than stuffing them into detail. Add them from a listener/subscriber so controllers don't care:

$problem = $problem->withExtension('traceId', $request->attributes->get('trace_id'));

This also gives you a free correlation key between the JSON body and your logs.

5. Logging without double-logging v0.2.0 added logging. Make sure it logs at error only for 5xx and at info/warning for 4xx, includes the traceId, and does not include the request body or the exception message verbatim for 4xx (user input). If Symfony's own ErrorListener already logs, either lower its verbosity or accept two entries — just know which one your alerting keys on.

6. Testing contract, not prose In functional tests, assert the envelope, not the sentence:

$client->request('GET', '/api/widgets/999', [], [], ['HTTP_ACCEPT' => 'application/problem+json']);

$response = $client->getResponse();
self::assertSame(404, $response->getStatusCode());
self::assertStringStartsWith('application/problem+json', $response->headers->get('Content-Type'));
self::assertJsonStringEqualsJsonString(
    '{"type":"https://api.example.com/problems/not-found","status":404}',
    // or use json_encode on a decoded subset
    json_encode(array_intersect_key(
        json_decode($response->getContent(), true),
        ['type' => true, 'status' => true]
    ))
);

Add one test per problem type — this becomes your de-facto schema registry.

7. Keep it behind an adapter boundary Because the bundle is pre-1.0 and archived, don't scatter ProblemException throws across your domain. Either:

  • throw domain exceptions and translate in one kernel listener of your own, or
  • expose a thin ApiProblemFactory interface in your app and let the bundle be one implementation.

Six months from now, swapping the bundle for 40 lines of your own listener should touch one file.

Gotchas and Tips

  • It's archived (last release Feb 2019). Pin 0.2.0 exactly, no ^. No security patches, no Symfony 5/6/7 support, no PHP 8.1+ fixes. If you're on a modern stack, budget a few hours to either vendor-patch or reimplement — the RFC 7807 logic is genuinely ~100 lines.
  • Content negotiation. Only requests with Accept: application/json / application/problem+json are likely to get JSON; browsers hitting the same route may still get HTML. Test with the header your SPA/mobile client actually sends, and consider forcing JSON for /api/* via a format listener or route requirement.
  • Debug mode masks everything. In dev, Symfony's ErrorHandler/profiler intercepts before your listener and shows the exception page. Always verify problem rendering in prod-like config — this is the #1 "it doesn't work" cause.
  • Listener priority matters. Your problem listener must run before the default exception listener; otherwise HttpException gets rendered by the framework first. Check priorities and verify one 404 and one 500 end-to-end.
  • Fatal errors bypass it. kernel.exception only covers exceptions. PHP fatals, exit, and out-of-memory errors produce a 500 without the problem body — that's a gap in the envelope, not a bug to chase.
  • Reserved members. type, title, status, detail, instance cannot be reused as extension keys. If you write a generic withExtension() helper, guard against collisions or you'll silently overwrite status.
  • detail leaks. Exception messages routinely contain SQL, file paths, or internal hostnames. Default to a generic detail for 5xx in prod and log the real message; keep a debug flag that flips this only off-prod.
  • status in the body must match the HTTP status. They're separate fields; a mismatch breaks strict clients. Assert this in tests.
  • instance should be the request URI, not a UUID — some clients use it for dedup. If you want a correlation ID, add traceId as an extension instead.
  • Cache headers. Errors should generally be Cache-Control: no-store; a 429 with Retry-After is the exception where you do want caching/reuse guidance.
  • Don't localize title/detail server-side unless your API is single-locale. type + optional code extension are stable; prose is not.
  • Logging (added in 0.2.0) is sparsely documented. Grep for the logger call and confirm: PSR-3 vs. custom, level, and whether request context is included. Sanitize before it hits your log aggregator — problem bodies frequently contain user input via validation messages.
  • Extension points are your escape hatch. If a mapping doesn't fit, prefer adding a listener/subscriber that decorates the problem over forking the bundle. When forking becomes necessary (it will, given the archive status), fork only the listener and keep the value object shape identical so client code is untouched.
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.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console