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:
- Changing the URL to something Cloudflare caches
- Setting the correct headers
- Creating a stateless middleware group
- 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.