Gestione errori API Laravel 12: da stack trace in produzione a Problem Details RFC 9457 con renderable exceptions

Gestione errori API Laravel 12: da stack trace in produzione a Problem Details RFC 9457 con renderable exceptions

In una piattaforma marketplace con migliaia di utenti attivi, un'integrazione con un partner ha fallito per tre giorni perché l'API restituiva {"message": "Server Error"} con HTTP 500 - senza alcun contesto su quale parametro fosse errato o quale risorsa mancasse. Il team del partner ha aperto 7 ticket di supporto, ciascuno richiedendo un round-trip di debug. L'OWASP Error Handling Cheat Sheet definisce il principio: "When an unexpected error occurs, a generic response is returned by the application but the error details are logged server side for investigation, and not returned to the user." Il problema è che "generico" non significa "inutile" - RFC 9457 (luglio 2023, IETF Standards Track) standardizza un formato che è sia sicuro (nessun stack trace) sia informativo (tipo, titolo, dettaglio, istanza).

Cos'è RFC 9457 e perché sostituisce RFC 7807 per i dettagli di errore API?

RFC 9457 ("Problem Details for HTTP APIs") ha sostituito RFC 7807 (marzo 2016) come standard per comunicare dettagli di errore nelle API HTTP. Il media type è application/problem+json. I cinque campi standard sono tutti opzionali: type (URI che identifica il tipo di problema), title (descrizione breve), status (codice HTTP, advisory), detail (spiegazione specifica dell'occorrenza), instance (URI dell'occorrenza specifica). RFC 9457 aggiunge rispetto a 7807 un registro di problem type URI comuni, chiarimenti sulla gestione di problemi multipli in una singola risposta e guida per URI type non dereferenziabili. I codici HTTP stessi sono definiti nella Sezione 15 di RFC 9110 (HTTP Semantics, giugno 2022), che ha sostituito RFC 7231.

Laravel non implementa RFC 9457 nativamente. Il comportamento di default del framework (classe Handler, metodo convertExceptionToArray()) restituisce: con APP_DEBUG=true, message, exception (FQCN), file, line e trace completo - informazioni che l'OWASP Testing Guide classifica come vettore di ricognizione; con APP_DEBUG=false, solo {"message": "Server Error"}. La documentazione di Laravel 12 offre due meccanismi per conformarsi a RFC 9457: withExceptions() in bootstrap/app.php e le renderable exceptions.

/* bootstrap/app.php - error handling RFC 9457-compliant */
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Http\Request;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;

return Application::configure(basePath: dirname(__DIR__))
    ->withExceptions(function (Exceptions $exceptions) {
        ## Forza JSON per tutte le richieste API
        $exceptions->shouldRenderJsonWhen(function (Request $request, Throwable $e) {
            return $request->is('api/*') || $request->expectsJson();
        });

        ## Catch-all: converte ogni eccezione in formato RFC 9457
        $exceptions->render(function (Throwable $e, Request $request) {
            if (! $request->is('api/*') && ! $request->expectsJson()) {
                return null; ## Lascia il rendering HTML per le richieste web
            }

            $status = $e instanceof HttpExceptionInterface
                ? $e->getStatusCode()
                : 500;

            return response()->json([
                'type'   => 'https://httpstatuses.io/' . $status,
                'title'  => match (true) {
                    $status === 404 => 'Resource Not Found',
                    $status === 422 => 'Validation Error',
                    $status === 403 => 'Forbidden',
                    $status === 401 => 'Unauthenticated',
                    $status >= 500  => 'Internal Server Error',
                    default         => 'Client Error',
                },
                'status' => $status,
                'detail' => $status < 500
                    ? $e->getMessage()
                    : 'An internal error occurred. Please try again later.',
            ], $status, ['Content-Type' => 'application/problem+json']);
        });
    })
    ->create();

/* app/Exceptions/InsufficientStockException.php - renderable exception */
namespace App\Exceptions;

use Exception;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;

class InsufficientStockException extends Exception
{
    public function __construct(
        private readonly int $productId,
        private readonly int $requested,
        private readonly int $available,
    ) {
        parent::__construct("Insufficient stock for product {$productId}");
    }

    ## La renderable exception definisce il proprio formato di risposta
    ## Il framework la chiama automaticamente - nessuna logica nell'handler
    public function render(Request $request): JsonResponse
    {
        return response()->json([
            'type'       => '/problems/insufficient-stock',
            'title'      => 'Insufficient Stock',
            'status'     => 409,
            'detail'     => "Product {$this->productId}: requested {$this->requested}, available {$this->available}.",
            'product_id' => $this->productId,
        ], 409, ['Content-Type' => 'application/problem+json']);
    }

    ## Opzionale: controlla il reporting (logging)
    public function report(): bool
    {
        return false; ## Non loggare come errore - è logica di business
    }
}

Il catch-all nell'handler copre tutte le eccezioni non gestite; le renderable exceptions gestiscono i casi di business specifici. Il mapping automatico di Laravel converte ModelNotFoundException in 404, AuthorizationException in 403, ValidationException in 422 - il catch-all li riceve già con il codice HTTP corretto. Il Content-Type: application/problem+json segnala ai client che la risposta è conforme a RFC 9457.

Come si confronta RFC 9457 con JSON:API e qual è la scelta pragmatica?

Un'alternativa a RFC 9457 è il formato errore di JSON:API v1.1: un array errors con oggetti che includono status (stringa, non numero), code (errore applicativo), source.pointer (JSON Pointer al campo responsabile), title e detail. La differenza strutturale: RFC 9457 usa un oggetto singolo, JSON:API un array - supportando nativamente errori multipli nella stessa risposta (utile per la validazione). La scelta pragmatica: se l'API segue già la specifica JSON:API per le risposte di successo, usare il formato errore JSON:API per coerenza; altrimenti, RFC 9457 è lo standard IETF e ha adozione più ampia (Spring Boot, ASP.NET, Django REST Framework lo supportano nativamente).

Errori comuni nella gestione errori API Laravel

Il primo errore è lasciare APP_DEBUG=true in produzione. L'OWASP Error Handling Cheat Sheet lo classifica come vulnerabilità: lo stack trace rivela versioni del framework, percorsi interni del filesystem e potenziali punti di injection. L'OWASP Testing Guide mostra un esempio concreto: un errore SQL che espone D:\app\index_new.php on line 188. In Laravel, APP_DEBUG=true restituisce l'intero stack trace nelle risposte JSON - un regalo per un attaccante in fase di ricognizione.

Il secondo è restituire {"message": "Server Error"} senza contesto per errori 4xx. Un client che riceve 422 senza sapere quale campo ha fallito la validazione non può auto-correggersi. Laravel gestisce la ValidationException con $e->errors() - un array chiave-campo/valore-messaggi - ma il formato di default non è RFC 9457. La conversione richiede un renderer dedicato che mappi $e->errors() nel campo detail o in un'estensione violations.

Il terzo è non separare il reporting dal rendering. Un'eccezione di business (stock insufficiente, limiti di piano superati) non è un errore di sistema - loggarla come ERROR genera rumore nei log applicativi e inquina le metriche di error rate. Il metodo report(): bool nelle renderable exceptions controlla se l'eccezione viene loggata: return false per le eccezioni di business, return true (o omissione) per gli errori di sistema.

Il quarto è non usare Form Request per la validazione API. La validazione inline nel controller ($request->validate([...])) funziona, ma quando la stessa API accetta input complessi (ordini con array di item, indirizzi annidati), la Form Request centralizza le regole e lancia ValidationException automaticamente - il catch-all nell'handler la converte in RFC 9457 senza codice aggiuntivo nel controller.

La gestione errori RFC 9457-compliant è il complemento dell'architettura clean dei controller: il controller orchestra, il servizio lancia eccezioni di business, l'handler le converte in risposte standard. I middleware aggiungono security header a queste risposte di errore così come a quelle di successo. Per conoscere il mio approccio alla progettazione API, visita la mia pagina professionale. Se le tue API restituiscono {"message": "Server Error"} senza contesto e gli stack trace sono visibili in produzione, contattami per una consulenza dedicata - partiamo dall'implementazione di RFC 9457 e dalla revisione della configurazione di error handling.

Ultima modifica: