Skip to content

ValidatesInboundRequest's "returns 422 automatically" promise breaks for non-JSON-expecting requests #98

Description

@morcen

Problem

The ValidatesInboundRequest docblock guarantees: "A validation failure returns a 422 response automatically and the request is never forwarded upstream." PassageController::handle() implements this with plain $request->validate($handlerInstance->rules()):

if ($handlerInstance instanceof ValidatesInboundRequest) {
    $request->validate($handlerInstance->rules());
}

Request::validate() throws Illuminate\Validation\ValidationException on failure. Laravel's default exception handler only converts this to a JSON 422 when $request->expectsJson() is true (i.e. an Accept: application/json header, or an AJAX/PJAX request). Otherwise it calls $this->invalid($request, $e), which does redirect(url()->previous())->withInput(...). url()->previous() and withInput() both call $request->session(), which throws RuntimeException('Session store not set on request.') on any request that hasn't gone through the StartSession middleware — the normal case for an API-gateway-style route registered without the web middleware group.

So for any client that doesn't send Accept: application/json (a plain curl -X POST, many webhook senders, non-fetch/XHR form posts), a validation failure on a Passage route without session middleware crashes with an uncaught RuntimeException instead of the documented 422.

The only existing test (tests/Feature/PassageIntegrationTest.php, ValidatesInboundRequest describe block) uses postJson(), which always sends Accept: application/json and therefore never exercises this path.

Location

  • src/Http/Controllers/PassageController.php:96-98
  • src/Contracts/ValidatesInboundRequest.php (the docblock making the unconditional 422 promise)
  • tests/Feature/PassageIntegrationTest.php:166-186 (only covers the JSON-expecting case)

Why it matters

Passage is an API gateway; its routes are typically registered outside the web middleware group and callers won't reliably send Accept: application/json. The gap turns an expected validation error into an unhandled crash, which is a worse failure mode than skipping validation entirely.

Suggested fix

Wrap the $request->validate() call and catch ValidationException explicitly, returning response()->json(['error' => ..., 'errors' => $e->errors()], 422) unconditionally rather than relying on Laravel's default (session-dependent) redirect behavior. Add a test that submits invalid input without an Accept: application/json header (e.g. plain post()) to a ValidatesInboundRequest handler and asserts a 422 JSON response rather than a crash.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingtestsTest suite improvements

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions