You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Laravel集成Tus协议结合Uppy上传时的异常问题求助

Laravel集成Tus协议结合Uppy上传时的异常问题求助

各位好,我目前在Laravel项目中尝试通过Tus协议搭配Uppy实现断点续传上传功能,但遇到了一系列异常问题,具体表现如下:

  • 上传文件时,服务器始终接收不到文件数据
  • 初始的HEAD请求返回404错误,提示文件不存在
  • POST请求发送后,服务器确实创建了对应的文件和.info信息文件,但后续的PATCH请求依旧提示文件找不到
  • PATCH请求中获取到的Upload-Length值为0,浏览器开发者工具里也能看到相同的异常

以下是我编写的FileUploadController代码,麻烦各位帮忙排查下问题所在:

<?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')
        ;
    }
}

我自己初步排查了几个点,但还是没找到问题根源:

  • 已经在OPTIONS请求中配置了完整的CORS头,跨域相关的响应头应该没有问题
  • POST请求的日志里能正确获取到Upload-Length,说明初始请求的参数是正常的
  • 检查过本地存储路径,POST请求确实创建了对应的空文件和.info信息文件,但后续HEAD、PATCH请求似乎无法正确定位到这些文件

希望有经验的大佬能帮忙分析下问题出在哪里,谢谢!

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.08 08:59:29