I have a problem with Tus protocol with Uppy and Laravel. When I upload a file, no data is sent to the server.
The Head controller return 404 error cause file not found. Then the post request is send and the file is created. Next this is the patch request... but the file is not found and the received data in Upload-Length is 0.
Idem with the browser inspector.
Here is my Laravel code:
<?php
class FileUploadController extends Controller {
private int $maxSize = 50000000000; // 50 GO
private string $disk = 'local';
private string $uploadPath = 'uploads';
public function options(): Response
{
return response('', 204)
->header('Vary', 'Origin')
->header('Access-Control-Allow-Origin', '*')
->header('Access-Control-Allow-Methods', 'POST, GET, OPTIONS, HEAD, PATCH')
->header('Access-Control-Allow-Headers', 'Tus-Resumable, Upload-Length, Upload-Metadata, Upload-Offset, Content-Type, Location, Authorization')
->header('Access-Control-Expose-Headers', 'Upload-Offset, Upload-Length, Location, Tus-Resumable, Upload-Metadata')
->header('Access-Control-Max-Age', '86400')
->header('Tus-Resumable', '1.0.0',)
->header('Tus-Version', '1.0.0',)
;
}
public function getFileResource(
Request $request,
string $fileId
): Response {
$infoPath = $this->uploadPath . '/' . $fileId . '.info';
$storage = Storage::disk($this->disk);
if (!$storage->exists($infoPath)) {
return response('Not found', Response::HTTP_NOT_FOUND)
->header('Access-Control-Allow-Origin', '*');
}
$info = json_decode($storage->get($infoPath), true);
$length = $info['length'];
return response('', 200)
->header('Access-Control-Allow-Origin', '*')
->header('Access-Control-Allow-Headers', 'Tus-Resumable, Upload-Length, Upload-Metadata, Upload-Offset, Content-Type, Location, Authorization')
->header('Access-Control-Allow-Methods', 'POST, GET, OPTIONS, HEAD, PATCH')
->header('Access-Control-Expose-Headers', 'Upload-Offset, Upload-Length, Location, Tus-Resumable, Upload-Metadata')
->header('Tus-Resumable', '1.0.0')
->header('Upload-Length', $length)
->header('Location', 'https://hopla.api.xefi.local.xyz/api/upload/' . $fileId)
;
}
public function createFileResource(
Request $request
): Response {
$log = "=== POST ===\n";
$log .= "HEADERS: " . var_export(getallheaders(), true) . "\n";
$log .= "Upload-Length: " . ($_SERVER['HTTP_UPLOAD_LENGTH'] ?? 'N/A') . "\n";
$log .= "Content-Length: " . ($_SERVER['CONTENT_LENGTH'] ?? 'N/A') . "\n";
\Log::debug($log);
$storage = Storage::disk($this->disk);
if (!$storage->exists($this->uploadPath)) {
$storage->makeDirectory($this->uploadPath);
}
$uploadLength = $request->header('Upload-Length');
$uploadMetadata = $request->header('Upload-Metadata', '');
$fileId = 'upload_' . \Str::uuid();
$storage->put($this->uploadPath . '/' . $fileId, '');
$storage->put($this->uploadPath . '/' . $fileId . '.info', json_encode([
'offset' => 0,
'length' => $uploadLength,
'metadata' => $uploadMetadata,
]));
return response('', Response::HTTP_CREATED)
->header('Access-Control-Allow-Origin', '*')
->header('Access-Control-Allow-Headers', 'Tus-Resumable, Upload-Length, Upload-Metadata, Upload-Offset, Content-Type, Location, Authorization')
->header('Access-Control-Allow-Methods', 'POST, GET, OPTIONS, HEAD, PATCH')
->header('Access-Control-Expose-Headers', 'Upload-Offset, Upload-Length, Location, Tus-Resumable, Upload-Metadata')
->header('Tus-Resumable', '1.0.0')
->header('Location', 'https://hopla.api.xefi.local.xyz/api/upload/' . $fileId)
;
}
public function uploadFileChunk(
Request $request,
string $fileId
): Response {
$storage = Storage::disk('local');
$infoPath = $this->uploadPath . '/' . $fileId . '.info';
$filePath = storage_path('app/' . $this->uploadPath . '/' . $fileId . '.part');
if (!$storage->exists($infoPath)) {
return response('File not found', Response::HTTP_NOT_FOUND);
}
$info = json_decode($storage->get($infoPath), true);
$uploadOffset = (int) $request->header('Upload-Offset', 0);
$currentOffset = $info['offset'] ?? 0;
if ($uploadOffset !== $currentOffset) {
return response('Offset mismatch', Response::HTTP_CONFLICT);
}
$inputStream = fopen('php://input', 'rb');
$fileStream = fopen($filePath, 'c+b');
fseek($fileStream, $uploadOffset);
$written = stream_copy_to_stream($inputStream, $fileStream);
fclose($inputStream);
fclose($fileStream);
$info['offset'] += $written;
$storage->put($infoPath, json_encode($info));
return response('', Response::HTTP_NO_CONTENT)
->header('Vary', 'Origin')
->header('Access-Control-Allow-Origin', '*')
->header('Access-Control-Allow-Headers', 'Tus-Resumable, Upload-Length, Upload-Metadata, Upload-Offset, Content-Type, Location, Authorization')
->header('Access-Control-Allow-Methods', 'POST, GET, OPTIONS, HEAD, PATCH')
->header('Access-Control-Expose-Headers', 'Upload-Offset, Upload-Length, Location, Tus-Resumable, Upload-Metadata')
->header('Tus-Resumable', '1.0.0')
->header('Upload-Offset', $info['offset'])
;
And my JS code:
const uppy = new Uppy({
id: 'stored_uppy',
debug: true,
maxFileSize: this.maxFileSize,
maxTotalFileSize: this.maxTotalFileSize,
autoProceed: true,
});
uppy.use(Tus, {
endpoint: user.value.apiUrl + '/api/upload',
headers: headers,
chunkSize: this.chunkSize,
retryDelays: [1000, 2000],
});
2 Answers 2
The issue you're facing is likely due to a mismatch in file handling and routing in your Laravel backend for the Tus protocol.
Make sure your route accepts HEAD:
Route::match(['HEAD'], '/upload/{fileId}', [FileUploadController::class, 'getFileResource']);
Comments
I encountered a client-side error while using Uppy for chunked uploads. The error was caused by a misconfiguration of the chunk size (chunkSize), which prevented the upload from working correctly. So Upload-Length is always 0