Skip to main content

Caching Intervention/Image responses using Laravel and Cloudflare

Today I was tasked with fetching images from a remote server, resizing and converting the images to jpeg, and making sure the responses are getting cached by Cloudflare.

To achieve this task I used the intervention/image package, because it allows you to easily modify images to your hearts content.
It ships with Laravel support out of the box wich is a plus.

After a while, the only thing that was left on my chart was getting Cloudflare to cache the responses, which turned out to be trickier than expected.
No matter what I tried, I was still greeted with CF-Cache-Status: BYPASS.

Solution

The solution I came up with consists of four parts:

  1. Changing the URL to something Cloudflare caches
  2. Setting the correct headers
  3. Creating a stateless middleware group
  4. Adding a new routes file

Let's dive right in!

Changing the URL to something Cloudflare caches

By default Cloudflare only caches these file extensions: https://support.cloudflare.com/hc/en-us/articles/200172516
To get Cloudflare to even consider caching our response I had to change the URL to include .jpg.

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\ImageController;
Route::get('/image/{image}.jpg', [ImageController::class, 'resize']);

With Cloudflare now considering the URL for caching it's time to move on to the next step.

Setting the correct headers

To get the images to be cached we need to apply some HTTP headers to the response. Luckily, Laravel offers a Cache Control Middleware out of the box: https://laravel.com/docs/8.x/responses#cache-control-middleware

Using the snippet from the previous example we get the following route:

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\ImageController;
Route::middleware('cache.headers:public;max_age=2628000;etag')->group(function () {
    Route::get('/image/{image}.jpg', [ImageController::class, 'resize']);
});

In the example we set the max-age to 2628000 seconds, declare the response as cachable by any cache by setting the public attribute and let Laravel automatically add an ETag to the response.

With those changes applied we are ready to move to the next step.

Creating a stateless middleware group

In order to make our response look like we just read the file from the file system we need to get rid of the cookies in the response. An easy way to achieve this, is to add a new middleware group in our Kernel.php.

protected $middlewareGroups = [
        // ...

        'stateless' => [
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
        ]
    ];

By not using default web middleware we prevent the response from sending session cookies.

With the middleware created we are ready for the final step.

Adding a new routes file

The final piece to the puzzle is to add a new file within the routes folder of our Laravel application.

In this file we place the previous steps code.

routes/stateless.php:

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\ImageController;
Route::middleware('cache.headers:public;max_age=2628000;etag')->group(function () {
    Route::get('/image/{image}.jpg', [ImageController::class, 'resize']);
});

To get Laravel to use our newly created file we need to make some modifications to the RouteServiceProvider.php.

public function boot()
{
    // ...

    $this->routes(function () {
        // ...
        Route::middleware('stateless')
            ->namespace($this->namespace)
            ->group(base_path('routes/stateless.php'));
    });
}

Our modified code tells Laravel to fetch the routes from routes/stateless.php and to use the stateless middleware we created earlier.

That's it. Cloudflare should now cache the responses.

Published on February 25, 2021
Last modified on April 13, 2021

Did you like what you read? Feel free to share! Make sure to follow me on Twitter to stay in the loop.