skip to content

Why does a Laravel form request freshly generated by make:request answer every submission with a 403, and how do you fix it?

level: juniorimportance: should knowfreq 55%

answer

  1. look at the generated stub
  2. authorize() returns false by default
  3. AuthorizationException rendered as 403
  4. return true, or delete authorize()
  5. $this->route('event') over $this->event

basics

~20 s

The make:request stub's authorize() returns false, and a false result throws AuthorizationException, which Laravel renders as a 403. Return a real check (or true when authorization lives elsewhere), or delete the method, which counts as authorized.

solid answer

~30 s

The stub that `make:request` writes contains `authorize(): bool { return false; }`. `FormRequest::passesAuthorization()` calls it before building the validator, and a `false` makes `failedAuthorization()` throw `AuthorizationException`, which the exception handler converts to a 403 "This action is unauthorized." — the controller never runs. Fix it by returning an actual decision, for example `$this->user()?->can('register', $this->route('event'))`, or `true` when a policy or middleware already guards the route. Removing `authorize()` also works, because the base class treats a missing method as allowed. It may also return an `Illuminate\Auth\Access\Response`, so `Response::deny('Registration is closed.')` sets the message and `denyAsNotFound()` answers 404 instead.

code

php · 23 lines
php
<?php

namespace App\Http\Requests;

use Illuminate\Auth\Access\Response;
use Illuminate\Foundation\Http\FormRequest;

class StoreRegistrationRequest extends FormRequest
{
    public function authorize(): Response
    {
        $event = $this->route('event');

        return $event->registration_open
            ? Response::allow()
            : Response::deny('Registration for this event is closed.');
    }

    public function rules(): array
    {
        return ['ticket_type' => ['required', 'in:standard,vip']];
    }
}

go deeper

for a junior

Remember that the generated authorize() returns false, and that false means a 403 before any rule runs.

for a middle

Explain passesAuthorization(): missing method means allowed, a Response is enforced, and AuthorizationException is mapped to 403.

for a senior

Show why reading the bound model with route() matters, and choose between authorizing in the request, a policy call or route middleware.

for a principal

Decide where authorization lives across a codebase so reviewers never have to hunt three places to know who may submit a form.

## The symptom A developer runs `php artisan make:request StoreRegistrationRequest`, fills in `rules()`, type-hints the class on `store()`, and every submission — even from a logged-in admin — comes back as **403 Forbidden** with the message "This action is unauthorized." No validation errors appear and nothing in the controller executes. ## The cause: the stub denies by default The generated class looks like this: ```php public function authorize(): bool { return false; } ``` When the form request is resolved, `validateResolved()` calls `passesAuthorization()` **before** any rule runs. In `FormRequest`, that method: 1. Checks whether an `authorize()` method exists; if not, it returns `true`. 2. Calls it through the container, so it may type-hint dependencies. 3. If the result is an `Illuminate\Auth\Access\Response`, calls `->authorize()` on it, which throws on a denial. 4. Otherwise returns the boolean as-is. A `false` leads to `failedAuthorization()`, which throws `Illuminate\Auth\Access\AuthorizationException`. Laravel's exception handler maps that exception to Symfony's `AccessDeniedHttpException`, which is a **403**. The default message comes from the exception's constructor: "This action is unauthorized." The effect is fail-closed: a forgotten authorization decision denies access instead of silently allowing it. ## The fixes, and when each fits | Option | When it fits | |---|---| | Return a real check, e.g. `$this->user()?->can('register', $this->route('event'))` | The form request is the natural place to ask "may this user do this?" | | `return true;` | Authorization is enforced elsewhere: route middleware, a controller attribute, or a policy call in the action | | Delete `authorize()` entirely | Same as `true`; the base class treats a missing method as authorized | | Return `Response::deny('Registration is closed.')` | You want a specific message in the 403 body | | Return `Response::denyAsNotFound()` | You do not want to reveal that the resource exists | The gate and policy machinery behind `can()` is its own topic; the point here is only that `authorize()` is the hook and that its **return value** decides the outcome. ## Reading the route parameter safely Inside `authorize()` you usually need the model the route bound. Route model binding has already run, so two spellings look equivalent: - `$this->route('event')` reads the **route parameter** directly. - `$this->event` goes through `Request::__get()`, which looks in the **input first** and only falls back to the route parameter when no input key matches. That second behaviour matters for authorization: if a client posts a field named `event`, `$this->event` returns the posted value, not the bound model. Using `$this->route('event')` in `authorize()` removes that ambiguity. ## Ordering consequences - Because `authorize()` runs before the rules, a user who is not allowed never sees field errors — good for not leaking which fields exist. - `prepareForValidation()` runs **before** `authorize()`, so any input you merge there is already visible when authorization runs. Do not base an authorization decision on a value you merged from untrusted input. - A 403 from a form request is an ordinary `AuthorizationException`; anything that customises how that exception renders applies here too. ## Checklist when you see an unexpected 403 - Open the form request and read `authorize()` first — the stub's `false` is the usual culprit. - Check whether the route also has an authorization middleware or attribute that denies. - If `authorize()` uses a route parameter, confirm the parameter name matches the route definition, since `route('evnt')` returns `null` and a `null` model usually makes the check fail. - Confirm the user is actually authenticated; `$this->user()` is `null` for guests, and calling `can()` on `null` is an error unless you use the nullsafe operator. - Remember a 403 from `authorize()` is decided before validation, so adding or fixing rules will never change it. ## What interviewers listen for A good answer names the stub's `false` without prompting, knows that a missing `authorize()` counts as allowed, and can say why the framework chose to fail closed. A strong answer adds the `route()` versus property-access subtlety and the option of returning a `Response` for a clearer message or a 404, which shows the candidate has debugged this in a real app rather than only read about it.

  • Why is $this->route('event') safer than $this->event inside authorize()?
    `Request::__get()` returns a matching input key before it falls back to the route parameter, so a client that posts a field named `event` changes what `$this->event` returns. `$this->route('event')` always reads the bound route parameter.
  • How do you make a denied form request answer 404 instead of 403?
    Return `Response::denyAsNotFound()` from `authorize()`. The response's `authorize()` throws an `AuthorizationException` carrying a 404 status, and the handler renders an HTTP exception with that status instead of the default access-denied 403.

saying these in an interview costs you the question

  • The stub's authorize() returns true, so a 403 must come from middleware
  • Deleting authorize() makes every request fail authorization
  • A false authorize() produces a 422 validation response
  • $this->event always returns the route-bound model