Skip to main content
Laravel, shipping fast.
Chapter 6 · The Request Pipeline

The Things That Aren’t Your Code

Julian Beaujardin

Three topics come up in every discussion of an API’s pipeline. In each, the right amount of code to write is close to none.

CORS. Laravel’s HandleCors middleware is already in the global stack, and its default configuration is open: any origin may call anything under api/*. For a browser-facing API you narrow that to your own front ends. For an API called only by other servers, as this one is, CORS shouldn’t apply at all, because it is a browser mechanism. Publish the config with php artisan config:publish cors and set paths to an empty array. The open default does little harm to an API that authenticates with a bearer token and no cookies, but “does little harm” is not a setting you chose. (Laravel documentation: Routing › Cross-Origin Resource Sharing (CORS).)

Compression. Gzip belongs to the web server or the CDN in front of your application. Nginx compresses a JSON response faster than PHP can, knows which content types to skip, and sets the Vary header correctly.

Transport security. Serve the API over HTTPS only, and refuse plain HTTP outright at the load balancer rather than redirecting it. By the time a redirect is sent, the bearer token has already crossed the network in clear text. Strict-Transport-Security and X-Content-Type-Options: nosniff are one line each in the web server’s configuration.

The Complete Request

POST /api/licenses
  server    limit by address               429
  global    proxies, CORS, size limit      Laravel
  api       SetLocale
            authentication                 401
            throttle by consumer           429
            ability check                  403
  request   StoreLicenseRequest            422
  action    LicenseController@store        201
  after     the request is recorded        terminate()

Read the right-hand column from top to bottom. A request is refused as early and as cheaply as possible: an unauthenticated one costs a token lookup, a throttled one costs a cache read, and neither comes near the provider. Arranging that order is what the pipeline is for.

Testing the Pipeline

Middleware is tested through the endpoints it protects:

// tests/Feature/ThrottleTest.php
it('throttles a consumer past its limit', function () {
    Sanctum::actingAs(
        Consumer::factory()->create(),
        ['licenses:read'],
    );
    $this->mock(LicenseContract::class)
        ->shouldReceive('all')->andReturn(collect());

    foreach (range(1, 60) as $attempt) {
        $this->getJson('/api/licenses')->assertOk();
    }

    $this->getJson('/api/licenses')
        ->assertTooManyRequests()
        ->assertHeader('Retry-After');
});

The contract is replaced so the test never touches the provider. What is under test is the sixty-first request.

And one for the claim this chapter made about refusals:

// tests/Feature/RequestLogTest.php
it('records a refused request', function () {
    $this->getJson('/api/licenses')->assertUnauthorized();

    $this->assertDatabaseHas('api_requests', [
        'status' => 401,
        'consumer_id' => null,
    ]);
});

Chapter 6 Summary

  • Middleware is configured in bootstrap/app.php. Use api() for your own, not the global stack.
  • Read the list of middleware Laravel already runs before writing one.
  • Define the api rate limiter once, keyed by consumer. Laravel sends the rate-limit headers and the 429.
  • Authentication runs before that limiter, so cap requests by address in front of the application, and answer 429 there too.
  • Tell Laravel which proxies to trust, by address, or every anonymous caller shares one counter.
  • Set the locale from Accept-Language with getPreferredLanguage().
  • Record every request in terminate(), the refused ones included. Never the token. Bodies only when you have decided to.
  • CORS, compression, and transport security are configuration. Close CORS if no browser calls you, and refuse plain HTTP.

The audio could not be loaded. Try again in a moment.