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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XYnWFt9pJEwvAmNFN38XD
This commit is contained in:
Andreas Reinhold / reini
2026-09-10 10:42:21 +02:00
co-authored by Claude Opus 5
parent 992f3da084
commit 126c0a5cdb
6 changed files with 67 additions and 18 deletions
+13
View File
@@ -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
+16 -16
View File
@@ -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]),
]);
+2 -2
View File
@@ -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...
],
+1
View File
@@ -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
+1
View File
@@ -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
+34
View File
@@ -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', [])