1

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],
});
halfer
20.2k20 gold badges111 silver badges208 bronze badges
asked Jun 17, 2025 at 16:06

2 Answers 2

0

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']);
answered Jun 27, 2025 at 15:02
Sign up to request clarification or add additional context in comments.

Comments

0

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

answered Jul 23, 2025 at 14:23

Comments

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.