From 126c0a5cdb599f3769da1e9d59a417da70dbaf03 Mon Sep 17 00:00:00 2001 From: Andreas Reinhold / reini Date: Thu, 10 Sep 2026 10:42:21 +0200 Subject: [PATCH] Fix uploads over 4 GB failing with a misleading size error Livewire's temporary upload rule was hard-coded to max:4194304 (4 GB), so /livewire/upload-file rejected any larger file no matter how high PHP_UPLOAD_MAX_FILESIZE or the admin's max file size were set. _uploadErrored() then replaced Livewire's actual message with "file exceeds the maximum size of N MB", quoting the admin limit the file was under. The cap is removed: PHP's upload_max_filesize is the hard limit and the admin setting is still enforced in updatedFiles() and createShare(). A rejected upload now logs the real validation errors and tells the user the server could not accept the file. max_upload_time is configurable via LIVEWIRE_MAX_UPLOAD_TIME, and the README documents every limit large uploads depend on. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_017XYnWFt9pJEwvAmNFN38XD --- README.md | 13 ++++++++++++ app/Livewire/FileUploader.php | 32 +++++++++++++++--------------- config/livewire.php | 4 ++-- docker-compose.example.yml | 1 + docker-compose.yml | 1 + tests/Feature/FileUploadTest.php | 34 ++++++++++++++++++++++++++++++++ 6 files changed, 67 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 7bd0881..b8e655f 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,19 @@ Migrations run automatically on startup. Open your configured domain — the Set | `caddy_data` | `/data` | TLS certificates | | `caddy_config` | `/config` | Caddy configuration | +**Large files:** + +Uploads beyond the defaults need these limits raised together: + +| Limit | Where | Default | +|-------|-------|---------| +| `PHP_UPLOAD_MAX_FILESIZE` / `PHP_POST_MAX_SIZE` | Environment | `4G` — hard cap per file / per upload batch | +| Max file size / Max size per share | Admin → Settings | 100 MB / 2 GB | +| `LIVEWIRE_MAX_UPLOAD_TIME` | Environment | 30 minutes per upload | +| `OCTANE_MAX_EXECUTION_TIME` / `PHP_MAX_EXECUTION_TIME` | Environment | 300 seconds — encrypting a large file takes a while | + +Behind a reverse proxy, raise its request body limit and read timeout as well (nginx: `client_max_body_size`, `proxy_read_timeout`). + ### Manual (without Docker) ```bash diff --git a/app/Livewire/FileUploader.php b/app/Livewire/FileUploader.php index d45d0d4..2f61552 100644 --- a/app/Livewire/FileUploader.php +++ b/app/Livewire/FileUploader.php @@ -4,6 +4,7 @@ namespace App\Livewire; use App\Models\Setting; use App\Services\ShareService; +use Illuminate\Support\Facades\Log; use Illuminate\Validation\ValidationException; use Livewire\Attributes\Layout; use Livewire\Component; @@ -36,30 +37,29 @@ class FileUploader extends Component $this->expiration = Setting::get('default_expiration', '7d') ?: '7d'; } + /** + * Handle an upload the temporary upload endpoint did not accept. + * + * Validation errors (a 422) mean the whole file reached the server and was + * rejected there, so the real reason is logged for the administrator rather + * than guessed at in front of the user. Anything else is a transport failure. + */ public function _uploadErrored($name, $errorsInJson, $isMultiple): void { $this->dispatch('upload:errored', name: $name)->self(); - $maxFileSize = (int) Setting::get('max_file_size', 100 * 1024 * 1024); - $maxFileSizeMb = (int) ($maxFileSize / (1024 * 1024)); + $errors = is_null($errorsInJson) ? null : (json_decode($errorsInJson, true)['errors'] ?? null); - if (! is_null($errorsInJson)) { - $errors = json_decode($errorsInJson, true)['errors'] ?? null; + if ($errors) { + Log::warning('File upload rejected by the temporary upload endpoint.', ['errors' => $errors]); - if ($errors) { - $messages = []; - foreach ($errors as $messages_array) { - foreach ((array) $messages_array as $msg) { - $messages[] = $msg; - } - } - - throw ValidationException::withMessages([ - 'files' => __('Upload failed: file exceeds the maximum size of :max MB.', ['max' => $maxFileSizeMb]), - ]); - } + throw ValidationException::withMessages([ + 'files' => __('Upload failed: the server could not accept the file. Please try again or contact the administrator.'), + ]); } + $maxFileSizeMb = (int) ((int) Setting::get('max_file_size', 100 * 1024 * 1024) / (1024 * 1024)); + throw ValidationException::withMessages([ 'files' => __('Upload failed: file may be too large (max :max MB) or the connection was interrupted.', ['max' => $maxFileSizeMb]), ]); diff --git a/config/livewire.php b/config/livewire.php index 6767b6e..21b84b5 100644 --- a/config/livewire.php +++ b/config/livewire.php @@ -130,7 +130,7 @@ return [ 'temporary_file_upload' => [ 'disk' => env('LIVEWIRE_TEMPORARY_FILE_UPLOAD_DISK'), // Example: 'local', 's3' | Default: 'default' - 'rules' => ['required', 'file', 'max:4194304'], // 4GB — fine-grained limits enforced per-component + 'rules' => ['required', 'file'], // No size cap: PHP's upload_max_filesize is the hard limit, the admin limit is enforced per-component 'directory' => null, // Example: 'tmp' | Default: 'livewire-tmp' 'middleware' => null, // Example: 'throttle:5,1' | Default: 'throttle:60,1' 'preview_mimes' => [ // Supported file types for temporary pre-signed file URLs... @@ -138,7 +138,7 @@ return [ 'mov', 'avi', 'wmv', 'mp3', 'm4a', 'jpg', 'jpeg', 'mpga', 'webp', 'wma', ], - 'max_upload_time' => 30, // Max duration (in minutes) before an upload is invalidated... + 'max_upload_time' => (int) env('LIVEWIRE_MAX_UPLOAD_TIME', 30), // Max duration (in minutes) before an upload is invalidated... 'cleanup' => true, // Should cleanup temporary uploads older than 24 hrs... ], diff --git a/docker-compose.example.yml b/docker-compose.example.yml index 29fad00..4bd1900 100644 --- a/docker-compose.example.yml +++ b/docker-compose.example.yml @@ -59,6 +59,7 @@ services: # PHP_MAX_EXECUTION_TIME: "300" # Upload timeout in seconds # PHP_MAX_INPUT_TIME: "300" # Input processing timeout # PHP_MEMORY_LIMIT: "512M" # PHP memory limit + # LIVEWIRE_MAX_UPLOAD_TIME: "30" # Minutes a single upload may take (raise for large files on slow links) healthcheck: test: ["CMD", "curl", "--silent", "--fail", "http://localhost/up"] interval: 30s diff --git a/docker-compose.yml b/docker-compose.yml index d323895..1d1d181 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -38,6 +38,7 @@ services: PHP_MAX_EXECUTION_TIME: ${PHP_MAX_EXECUTION_TIME:-300} PHP_MAX_INPUT_TIME: ${PHP_MAX_INPUT_TIME:-300} PHP_MEMORY_LIMIT: ${PHP_MEMORY_LIMIT:-512M} + LIVEWIRE_MAX_UPLOAD_TIME: ${LIVEWIRE_MAX_UPLOAD_TIME:-30} healthcheck: test: ["CMD", "curl", "--silent", "--fail", "http://localhost/up"] interval: 30s diff --git a/tests/Feature/FileUploadTest.php b/tests/Feature/FileUploadTest.php index d02c097..e0b4bef 100644 --- a/tests/Feature/FileUploadTest.php +++ b/tests/Feature/FileUploadTest.php @@ -5,6 +5,7 @@ use App\Livewire\SystemPasswordPrompt; use App\Models\Setting; use App\Models\Share; use Illuminate\Http\UploadedFile; +use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Storage; use Livewire\Livewire; @@ -119,6 +120,39 @@ test('files added in multiple batches end up in the same share', function () { expect($share->files->pluck('original_name')->all())->toBe(['first.txt', 'second.txt']); }); +test('files larger than 4 GB can be shared when within the admin file size limit', function () { + Storage::fake('shares'); + Setting::set('max_file_size', 15000 * 1024 * 1024); + Setting::set('max_size_per_share', 20 * 1024 * 1024 * 1024); + + Livewire::test(FileUploader::class) + ->set('files', [UploadedFile::fake()->create('backup.dump', 6 * 1024 * 1024)]) + ->assertHasNoErrors('files') + ->call('createShare') + ->assertHasNoErrors() + ->assertRedirectContains('/share/'); + + expect(Share::query()->first()->total_size)->toBe(6 * 1024 * 1024 * 1024); +}); + +test('a rejected upload logs the real reason instead of blaming the file size limit', function () { + Log::spy(); + Setting::set('max_file_size', 15000 * 1024 * 1024); + + $errors = ['files.0' => ['The files.0 failed to upload.']]; + + $component = Livewire::test(FileUploader::class) + ->call('_uploadErrored', 'files', json_encode(['errors' => $errors]), true) + ->assertDispatched('upload:errored'); + + expect($component->errors()->first('files')) + ->toBe('Upload failed: the server could not accept the file. Please try again or contact the administrator.'); + + Log::shouldHaveReceived('warning') + ->withArgs(fn (string $message, array $context): bool => $context['errors'] === $errors) + ->once(); +}); + test('file upload requires at least one file', function () { Livewire::test(FileUploader::class) ->set('files', [])