Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

ahoi's avatar
Level 5

HTTP 500 in PEST tests when using password.confirm middleware

Hi there,

I am using Laravel 11 with Sanctum and Fortify. Now I want to test an api endpoint, which got an additional middleware: password.confirm.

The tests throw HTTP 500.:

RuntimeException: Session store not set on request. in /Users/ahoi/Entwicklung/Laravel/carter/vendor/laravel/framework/src/Illuminate/Http/Request.php:564

Outside of the test scope this does not happen.

This is my test:

it('will only accept valid payloads', function (User $user, array $payload, int $httpResponseCode) {
    //Arrange
    login($user);

    //Act
    $request = patchJson(
        uri : route('user.name.update', $user),
        data: $payload
    );

    //Assert
    expect($request)
        ->toHaveStatus($httpResponseCode);
})->with('requests');

The login function is defined this way:

function login(User $user = null): void
{
    actingAs($user ?? User::factory()->createOne());
}

If I remove the middleware, everything works as expected.

Any idea why this is happening?

0 likes
7 replies
ahoi's avatar
Level 5

@martinbean Maybe I am missing your point or I did not go into detail as it would be expected.

I am using Sanctum, which ensures that requests are stateful. The fortify package is explicitly designed to be integrated in SPA applications using Sanctum.

martinbean's avatar

@ahoi Nope. JavaScript will send the Sanctum cookie with requests. The auth:api middleware then just pulls the user from that encrypted cookie and authenticates them for that single request. This is not stateful authentication, and also does not rely on sessions (which are stateful).

ahoi's avatar
Level 5

@martinbean Oh wow, in that case, all the names (SANCTUM_STATEFUL_DOMAINS, EnsureFrontendRequestsAreStateful, ...) are highly misleading to me.

In this case I wonder why it does work when I use the browser and not when I use the test. Sure, I am sending these cookies using my real browser:

XSRF-TOKEN=eyJpdiI6IjlDRVA...%3D; laravel_session=eyJpdiI6Iit...IxIiwidGFnIjoiIn0%3D"

But I wonder how else I can check, whether my API endpoint behaves like expected.

ahoi's avatar
Level 5

I was pausing working on that and it bubbled up again on my project.

I reread https://laravel.com/docs/11.x/fortify#password-confirmation:

If the password matches the user's current password, Fortify will redirect the user to the route they were attempting to access. If the request was an XHR request, a 201 HTTP response will be returned.

So although the docs are mentioning the fact, that it can handle XHR requests, it cannot support sanctum? Is that correct?

ahoi's avatar
Level 5

I debugged a request coming from my SPA application:

protected function shouldConfirmPassword($request, $passwordTimeoutSeconds = null)
    {
        $confirmedAt = time() - $request->session()->get('auth.password_confirmed_at', 0);
        dd($request->session());

        return $confirmedAt > ($passwordTimeoutSeconds ?? $this->passwordTimeout);
    }

The result:

Illuminate\Session\Store {#1231 ▼
  #id: "aiVJ7ctHyOa0LJxT3xXDV4RfLMWJFbtUR0T34bCJ"
  #name: "laravel_session"
  #attributes: array:5 [▼
    "_token" => "Q0yINOB5idtt5yyZ6i5DzDOTvBc5HjouNlgNd44V"
    "_previous" => array:1 [▶]
    "_flash" => array:2 [▶]
    "login_web_59ba36addc2b2f9401580f014c7f58ea4e30989d" => "9ccde9e9-0c0b-4cab-9773-84256e9ed659"
    "password_hash_web" => "y$/GiZEGdxkGPlaWR5TS9y1OAyur2u2XGQF5dga8u9zfKG7ueZikKgi"
  ]
  #handler: 
Illuminate\Session
\
DatabaseSessionHandler {#1230 ▶}
  #serialization: "php"
  #started: true
}

When running the test via pest, the requests session is null.

So I guess, I'm confused, @martinbean ;-)

ahoi's avatar
Level 5

I solved the problem.

It is required to add a referer header to the request. In this case, the EnsureFrontendRequestsAreStateful middleware recognizes the request as frontend request:

public static function fromFrontend($request)
    {
        $domain = $request->headers->get('referer') ?: $request->headers->get('origin');

        if (is_null($domain)) {
            return false;
        }

        $domain = Str::replaceFirst('https://', '', $domain);
        $domain = Str::replaceFirst('http://', '', $domain);
        $domain = Str::endsWith($domain, '/') ? $domain : "{$domain}/";

        $stateful = array_filter(config('sanctum.stateful', []));

        return Str::is(Collection::make($stateful)->map(function ($uri) {
            return trim($uri).'/*';
        })->all(), $domain);
    }

So in my case, it has to be:

    $request = $this->withHeaders(['referer' => 'https://localhost'])->patchJson(
        uri : route('user.name.update', $user),
        data: $payload
    );

This returns HTTP 423 as expected. Now you can go ahead and add the password confirmation request after getting HTTP 423 and retry the request, which then returns HTTP 200.

1 like

Please or to participate in this conversation.