返回

Laravel 文件上传失败 PostTooLargeException 错误解决

php

Laravel 文件上传失败:PostTooLargeException 错误排查与解决

使用 Laravel 开发时上传图片,遇到了下面的报错:

Error: Illuminate\Http\Exceptions\PostTooLargeException

简单来说,就是上传的文件太大了。我改过 php.ini 文件:

  • post_max_size = 1g
  • upload_max_filesize = 1g
  • max_execution_time = 3000
  • max_input_time = 3000

改完重启了 Apache,问题依旧。这挺让人头疼的,接下来我一步步排查问题,并给出解决办法。

一、问题原因分析

PostTooLargeException 错误很明显地告诉我们,问题出在上传文件的大小超过了服务器的限制。通常,这个限制由多个配置共同决定,改了php.ini没效果,可能是下面几个原因:

  1. 改错配置文件: 服务器可能加载了多个 php.ini 文件,你修改的可能不是正在使用的那个。
  2. Web 服务器限制: 除了 PHP 的配置,Web 服务器(如 Apache、Nginx)也可能有自己的上传大小限制。
  3. Laravel 框架层限制: 尽管不常见,但也有可能是 Laravel 的某些中间件或配置限制了上传大小。
  4. 客户端限制: 网页表单enctype属性值错误。

二、 解决步骤

1. 确认 php.ini 文件路径

确保改对地方!使用 phpinfo() 函数查看 PHP 正在使用的配置文件路径。

操作步骤:

  • 创建一个 PHP 文件 (比如 info.php),写入以下内容:

    <?php
    phpinfo();
    ?>
    
  • 通过浏览器访问这个文件,找到 "Loaded Configuration File" 这一项,它会告诉你当前生效的 php.ini 文件路径。

2. 检查并修改 php.ini 设置

找到正确的 php.ini 文件后,确保以下配置正确设置:

post_max_size = 1G    ; POST 数据最大尺寸
upload_max_filesize = 1G ; 单个上传文件最大尺寸
memory_limit = 256M   ; PHP 脚本内存限制 (按需调整,通常大于 upload_max_filesize)
max_execution_time = 300 ; 脚本最大执行时间 (秒)
max_input_time = 300     ; 脚本接收输入数据的最大时间 (秒)

说明:

  • post_max_size 要大于或等于 upload_max_filesize
  • memory_limit 最好也设置得大一些,给 PHP 足够的内存处理大文件。
  • 可以设置更大或更小, 取决你的项目.
  • 如果上述时间太长,也根据实际上传文件调整,上传完最好修改小一些。

安全提示:upload_max_filesizepost_max_size 设置得过大会增加服务器遭受拒绝服务攻击 (DoS) 的风险。尽量根据实际需求设置合理的大小。

3. 检查并修改 Web 服务器配置 (Apache/Nginx)

Apache (httpd.conf 或 .htaccess):

  • 如果你用的是 Apache,可能还需要修改 httpd.conf.htaccess 文件。
  • 找到 <IfModule mod_php.c><IfModule mod_php7.c> 类似的部分,增加或者更改。
<IfModule mod_php.c>
    LimitRequestBody 1073741824  ;  1GB (以字节为单位)
</IfModule>

# 如果没有, 你可以在VirtualHost里面进行增加。例如:

<VirtualHost *:80>
	# .....你的设置

    LimitRequestBody 1073741824

</VirtualHost>

  • 修改后需要重启Apache才能生效。

Nginx (nginx.conf):

  • Nginx 通过 client_max_body_size 指令控制上传大小。
  • 找到 httpserverlocation 块,添加或修改:
http {
    ...
    client_max_body_size 1G;
    ...
}

server {
    ...
     client_max_body_size 1G;
     ...
}
location /upload {
       ...
       client_max_body_size 1G;
       ...
}

  • 修改后使用 nginx -s reload 平滑重启 Nginx。

4. 检查 Laravel 配置(通常不是这的问题, 但也要排查)

  • Middleware: 检查你的 Laravel 项目中是否有自定义的中间件限制了请求大小。可以在 app/Http/Middleware 目录下查看。
  • 确保中间件里面没有如下限制:
// Handle uploaded file that exceeds PHP's 'upload_max_filesize' directive
        if ($this->isMaximumSizeExceededError()) {
            throw new PostTooLargeException();
        }

  • 验证规则: 如果你在表单请求或控制器中使用了验证规则,确保 max 规则的值没有限制上传大小。
    比如下代码的 max:2048, 如果实际上传的大小比这更大, 应该移除它。
    $request->validate([
        'file' => 'required|max:2048', // 限制 2MB
    ]);
  • config/filesystems.php: Laravel 会从该文件加载存储配置, 但通常和PostTooLargeException 异常没有关系, 主要是配置存储的方式(本地,S3,FTP等) 和权限问题相关.

5. 检查表单的 enctype 属性

务必在你的文件上传的 form 中加上 enctype="multipart/form-data" 属性。

<form action="/upload" method="POST" enctype="multipart/form-data">
    @csrf
    <input type="file" name="image">
    <button type="submit">Upload</button>
</form>

6.进阶技巧:分块上传

假如上传非常大的文件时,仅仅改变上述参数不是最好的解决手段, 如果用户网速慢,长久的连接更不稳定。

  • 对于超大文件上传,可以考虑使用分块上传(Chunked Upload)。

  • 基本思路:把大文件切成小块,分别上传,然后在服务器端合并。

  • 这样即使某一片上传失败,也只需要重传那一小块。

    这里介绍一种实现思路,代码仅供参考:

前端 (JavaScript, 使用 FileReader API):

function chunkedUpload(file, url) {
  const chunkSize = 1024 * 1024 * 2; // 2MB 每块
  const totalChunks = Math.ceil(file.size / chunkSize);
  let currentChunk = 0;

  function uploadNextChunk() {
    if (currentChunk < totalChunks) {
      const start = currentChunk * chunkSize;
      const end = Math.min(start + chunkSize, file.size);
      const chunk = file.slice(start, end);

      const formData = new FormData();
      formData.append('chunk', chunk);
      formData.append('chunk_index', currentChunk);
      formData.append('total_chunks', totalChunks);
      formData.append('filename', file.name); // 还要发送原始文件名

      // 使用 XMLHttpRequest 或 Fetch API 发送
         fetch(url, {
                method: 'POST',
                body: formData,
               headers: {
                     'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content') //Laravel CSRF
                 },
            })
            .then(response => response.json())
            .then(data => {
                 if(data.success){
                        currentChunk++;
                       uploadNextChunk(); // 递归上传下一块
                  }else{
                    // 错误处理...
                  }
             })
           .catch( error =>{
             //错误处理...
            });
    } else {
      // 全部上传完成,通知服务器合并
        fetch(url + '/complete', { //不同的路由用于通知合并
           method: 'POST',
           body: JSON.stringify({ filename: file.name }),
            headers: {
               'Content-Type': 'application/json',
                 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content') //Laravel CSRF
             },
         })
        .then(response => response.json())
         .then(data => {
               //合并成功或失败后的逻辑.
           });
    }
  }

  uploadNextChunk(); // 开始上传
}

//使用的时候
const fileInput = document.getElementById('fileInput');

fileInput.addEventListener('change', function() {
  const file = this.files[0];
  if (file) {
    chunkedUpload(file, '/upload-chunk'); // 上传的 URL
  }
});

Laravel 后端(控制器):

// 接收分块的路由
public function uploadChunk(Request $request)
{
    $chunk = $request->file('chunk');
    $chunkIndex = $request->input('chunk_index');
    $totalChunks = $request->input('total_chunks');
    $filename = $request->input('filename');

    $path = 'chunks/' . $filename . '_' . $chunkIndex;
     Storage::disk('local')->put($path, file_get_contents($chunk)); //或者其他的Storage方式, 取决你的配置.
    return response()->json(['success'=>true]);
}

//合并文件
public function completeUpload(Request $request)
{
       $filename = $request->input('filename');
        $finalPath = 'uploads/' . $filename;

    $finalFile = Storage::disk('local')->path($finalPath); // 使用 path 方法获取真实物理路径

   if (file_exists($finalFile)) {  //删除原来的总文件,防止冲突.
      unlink($finalFile);
   }

    $out = fopen($finalFile, 'wb'); // 创建最终文件 (可写,二进制)

   $chunkDir = Storage::disk('local')->path('chunks');
    if ($out) {

         $index =0;
          while(true){
            $chunkPath = $chunkDir.'/'.$filename. '_'.$index;
              if (file_exists($chunkPath)) {

                    $in = fopen($chunkPath, "rb");  //读取每一个chunk
                     if ($in) {
                             while ($buff = fread($in, 4096)) {
                                  fwrite($out, $buff);
                             }
                      } else {
                           //chunk 文件打开失败,错误处理...
                       }
                       fclose($in);
                        unlink($chunkPath); // 删除临时的 chunk 文件
                }else{ //已经不存在更多的index, 跳出循环。
                  break;
               }
               $index = $index+1; //寻找下一个index.
          }
         fclose($out);

          //检查是否合并正确...
         //返回给前端 合并的结果
       return response()->json(['success' => true, 'path' => $finalPath]);

    } else {
         //文件创建失败,合并出错,需要处理错误。
    }
}

注意事项

  • 前端库: 前端有很多成熟的库可以简化分块上传,例如: Dropzone.js, Uppy, Resumable.js等,它们都处理了分块,断点续传等问题,可以避免重复造轮子。
  • 错误处理: 代码中需要处理各种可能的错误, 例如, chunk丢失, 合并失败等。
  • 并发上传:一些库允许并发上传多个chunk文件,加快上传速度。

总的来说,解决 PostTooLargeException,核心在于正确配置 PHP 和 Web 服务器,了解他们如何共同作用限制了文件大小.实在不行还可以考虑分块上传. 希望大家上传顺利!