Laravel + Filament + Private Disk on Cloudways
Introduction
After a long, frustrating debugging exercise, I have discovered a quick tip for my Cloudways + Laravel friends. The “why” is still a touch fuzzy for me at the moment, but I’ll at least explain the symptoms and the fix.
The goal: Add a private file upload to a Filament resource form, ensuring anonymous users cannot download/view the file.
The issue: Everything was working great in my ddev environment. I could preview and open the files if I was logged in, and I would see a 403 if I was logged out. When I moved this to a Cloudways server I was getting automatically logged-out every time I hit a private file URL. Weird, right?
Code
Here’s the final code (the Cloudways “fix” is further down this page):
Filament resource > Form > Field (app/Filament/Resources/SubmissionResource.php)
1 2 3 4 5 6 7 8 |
FileUpload::make('file_path') ->nullable() ->label('File') ->disk('private') ->directory('submissions') ->visibility('private') ->openable() ->acceptedFileTypes(['application/pdf', 'image/*']), |
Private Disk (config/filesystems.php)
1 2 3 4 5 6 |
'private' => [ 'driver' => 'local', 'root' => storage_path('app/private'), 'url' => env('APP_URL') . '/private-file', 'visibility' => 'private', ] |
Route (routes/web.php)
1 2 3 |
Route::get('/private-file/submissions/{filename}', [ PrivateFileController::class, 'downloadSubmission' ])->name('private-file.downloadSubmission'); |
Controller (app/Http/Controllers/PrivateFileController.php)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<?php namespace App\Http\Controllers; class PrivateFileController extends Controller { public function downloadSubmission($filename) { if (!auth()->check()) { abort(403); } $path = storage_path('app/private/submissions/' . $filename); if (!file_exists($path)) { abort(404); } return response()->file($path); } } |
Debugging
My debugging journey for this issue took me down many different paths. It wasn’t pretty. Here are the avenues I explored:
- File permissions different on the server?
- Session handling different on the server, or misconfigured?
- CSRF issues?
- Caching issues?
- Issues with how I implemented the FileUpload Filament field?
- Issues with the downloadSubmission() method in my controller?
- Route missing necessary middleware? (this was a rabbit hole on this, my first Laravel 11 app)
My debugging lead me to a key realization: it seemed like the issue only presented itself when loading a file preview (in the resource form), or loading the resource (via direct URL). I ended up creating some simple test routes:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
Route::get('/private-file/submissions/test', function () { return '001 visiting this url does not log me out'; }); Route::get('/private-file/submissions/test.test', function () { return '002 visiting this url does not log me out'; }); Route::get('/private-file/submissions/test.jpg', function () { return '003 visiting this url does log me out'; }); Route::get('/private-file/submissions/{filename}', function () { return '004 visiting this url does log me out'; }); |
Take a look at the return messages; these explain what happens when I hit each of those URLs. I realized, through this testing, that static-file-looking URLs seemed to be triggering the logout. The test.jpg failed but test.test didn’t. A real file (/private-file/submissions/test.png) failed, but /test didn’t. And ALL of these worked fine on my local machine; I didn’t get logged out.
I attempted to use .htaccess to make sure these “static file” urls were handled by index.php, but this didn’t affect things at all. Ultimately I remembered that Cloudways does some nginx handling of static files. I did some digging and discovered I could tell nginx to ignore specific paths. Bingo!
Solution
Tell nginx to exclude /private-file/ urls so Laravel is able to handle them. In the Cloudways control panel you browse to [Your Application] ➙ Application Settings ➙ Varnish Settings and click Add New Exclusion. Add the URL pattern, save the rule, then go back to the General tab and click the Purge button.
Sadly I don’t know why visiting a static-looking nginx-handled URL would cause me to lose the session/authentication. I don’t have time to look into it at the moment (it’s 1:50am) but intend to give it some thought soon. I had to get this all out of my head before retiring for the evening.