WordPress自定义插件传大视频到Bunny.net遇413错误求解决
解决WordPress插件大文件上传413错误(支持20GB+视频)
问题背景
自定义WordPress插件实现视频上传至Bunny.net,但文件超过300MB时触发**413 (Request Entity Too Large)**错误,需支持20GB以上大文件上传。
解决方案
1. 调整服务器上传限制
413错误本质是服务器对请求体大小的限制,需修改服务器和PHP配置(不要依赖代码里的@ini_set,服务器配置会覆盖runtime设置):
Nginx配置
修改nginx.conf或站点专属配置文件:
client_max_body_size 0; # 0表示不限制大小 client_body_timeout 300s; # 延长超时时间适配大文件 client_body_buffer_size 10M;
重启Nginx:sudo systemctl restart nginx
Apache配置
- 修改
php.ini:
upload_max_filesize = 0 post_max_size = 0 memory_limit = 256M # 无需设为-1,满足运行即可 max_execution_time = 3600 max_input_time = 3600
- 修改
httpd.conf或站点根目录.htaccess:
LimitRequestBody 0
重启Apache:sudo systemctl restart apache2
2. 替换为分片上传方案
一次性上传大文件(20GB)不可行,必须采用Bunny.net支持的分片上传方案:
前端分片上传代码(jQuery)
替换原有提交逻辑,实现分片上传:
$form.on('submit', async function (e) { e.preventDefault(); const $status = $form.find('.upload-status'); const $progressContainer = $form.find('.progress-container'); const $progress = $form.find('.upload-progress'); const $percentage = $form.find('.progress-percentage'); const $submitButton = $form.find('button[type="submit"]'); const $fileInput = $(SELECTORS.fileInput); if ($fileInput[0].files.length === 0) { showStatus($status, ub_ajax.i18n.select_file || '请选择要上传的视频文件。', true); return; } const file = $fileInput[0].files[0]; const chunkSize = 5 * 1024 * 1024; // 5MB分片,可按需调整 const totalChunks = Math.ceil(file.size / chunkSize); let currentChunk = 0; $submitButton.prop('disabled', true); $progressContainer.show(); showStatus($status, ub_ajax.i18n.upload_progress || '上传中...'); try { // 1. 在Bunny.net创建空视频资源 const createRes = await $.ajax({ url: ub_ajax.ajax_url, type: 'POST', data: { action: 'create_bunny_video', nonce: ub_ajax.nonce, filename: file.name, collection_id: $fileInput.data('collection-id') }, timeout: 30000 }); if (!createRes.success) throw new Error(createRes.data.message); const videoId = createRes.data.video_id; // 2. 逐个上传分片 while (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('chunkNumber', currentChunk + 1); formData.append('totalChunks', totalChunks); formData.append('videoId', videoId); formData.append('action', 'upload_bunny_chunk'); formData.append('nonce', ub_ajax.nonce); await $.ajax({ url: ub_ajax.ajax_url, type: 'POST', data: formData, processData: false, contentType: false, timeout: 60000 }); currentChunk++; const percent = (currentChunk / totalChunks) * 100; $progress.val(percent); $percentage.text(`${Math.round(percent)}%`); } // 3. 通知Bunny.net合并分片 const finalizeRes = await $.ajax({ url: ub_ajax.ajax_url, type: 'POST', data: { action: 'finalize_bunny_upload', nonce: ub_ajax.nonce, videoId: videoId }, timeout: 30000 }); if (!finalizeRes.success) throw new Error(finalizeRes.data.message); $progress.val(100); $percentage.text('100%'); showStatus($status, '视频上传成功!'); setTimeout(() => location.reload(), 1500); } catch (error) { const msg = error.responseJSON?.data?.message || error.message || '上传出错,请重试。'; showStatus($status, msg, true); $submitButton.prop('disabled', false); } finally { $progressContainer.hide(); $submitButton.prop('disabled', false); } });
后端分片上传处理代码
替换原有ub_upload_video函数,新增三个AJAX处理函数:
add_action('wp_ajax_create_bunny_video', 'ub_create_bunny_video'); add_action('wp_ajax_nopriv_create_bunny_video', 'ub_create_bunny_video'); add_action('wp_ajax_upload_bunny_chunk', 'ub_upload_bunny_chunk'); add_action('wp_ajax_nopriv_upload_bunny_chunk', 'ub_upload_bunny_chunk'); add_action('wp_ajax_finalize_bunny_upload', 'ub_finalize_bunny_upload'); add_action('wp_ajax_nopriv_finalize_bunny_upload', 'ub_finalize_bunny_upload'); define('ALLOWED_MIME_TYPES', ['video/mp4', 'video/mkv', 'video/avi']); // 兜底配置,建议直接修改php.ini @ini_set('upload_max_filesize', '6M'); // 略大于分片大小 @ini_set('post_max_size', '6M'); @ini_set('memory_limit', '128M'); @set_time_limit(0); function ub_create_bunny_video() { try { verify_upload_request(); $filename = isset($_POST['filename']) ? sanitize_file_name($_POST['filename']) : ''; $collection_id = isset($_POST['collection_id']) ? sanitize_text_field($_POST['collection_id']) : ''; $credentials = get_bunny_credentials(); // 创建带分片上传权限的空视频 $api_url = UploadBunny::get_base_url() . '/library/' . $credentials['library_id'] . '/videos'; $data = [ 'title' => $filename, 'collectionId' => $collection_id, 'enableDirectUpload' => true ]; $response = wp_remote_post($api_url, [ 'headers' => [ 'Content-Type' => 'application/json', 'AccessKey' => $credentials['api_key'], ], 'body' => json_encode($data), 'timeout' => 30, ]); if (is_wp_error($response)) throw new Exception($response->get_error_message(), 500); $code = wp_remote_retrieve_response_code($response); $body = json_decode(wp_remote_retrieve_body($response), true); if (!in_array($code, [200,201])) { $msg = $body['message'] ?? '创建视频失败'; throw new Exception($msg, $code); } if (!isset($body['guid'])) throw new Exception('Bunny.net响应无效', 500); wp_send_json_success(['video_id' => $body['guid']]); } catch (Exception $e) { wp_send_json_error(['message' => $e->getMessage(), 'code' => $e->getCode()]); } } function ub_upload_bunny_chunk() { try { verify_upload_request(); if (empty($_FILES['chunk']) || $_FILES['chunk']['error'] !== UPLOAD_ERR_OK) { throw new Exception('分片上传失败', 400); } $chunk = $_FILES['chunk']; $chunkNum = (int)$_POST['chunkNumber']; $videoId = sanitize_text_field($_POST['videoId']); $credentials = get_bunny_credentials(); // 上传分片到Bunny.net $api_url = UploadBunny::get_base_url() . '/library/' . $credentials['library_id'] . '/videos/' . $videoId . '/parts/' . $chunkNum; $content = file_get_contents($chunk['tmp_name']); $response = wp_remote_request($api_url, [ 'method' => 'PUT', 'headers' => [ 'AccessKey' => $credentials['api_key'], 'Content-Type' => 'application/octet-stream', 'Content-Length' => filesize($chunk['tmp_name']) ], 'body' => $content, 'timeout' => 60, ]); if (is_wp_error($response)) throw new Exception($response->get_error_message(), 500); $code = wp_remote_retrieve_response_code($response); if (!in_array($code, [200,201])) { $body = json_decode(wp_remote_retrieve_body($response), true); $msg = $body['message'] ?? '分片上传失败'; throw new Exception($msg, $code); } wp_send_json_success(); } catch (Exception $e) { wp_send_json_error(['message' => $e->getMessage(), 'code' => $e->getCode()]); } } function ub_finalize_bunny_upload() { try { verify_upload_request(); $videoId = sanitize_text_field($_POST['videoId']); $credentials = get_bunny_credentials(); // 通知Bunny.net合并分片 $api_url = UploadBunny::get_base_url() . '/library/' . $credentials['library_id'] . '/videos/' . $videoId . '/complete'; $response = wp_remote_post($api_url, [ 'headers' => ['AccessKey' => $credentials['api_key']], 'timeout' => 30, ]); if (is_wp_error($response)) throw new Exception($response->get_error_message(), 500); $code = wp_remote_retrieve_response_code($response); if ($code !== 200) { $body = json_decode(wp_remote_retrieve_body($response), true); $msg = $body['message'] ?? '合并分片失败'; throw new Exception($msg, $code); } wp_send_json_success(['message' => __('视频上传成功!', 'upload-bunny')]); } catch (Exception $e) { wp_send_json_error(['message' => $e->getMessage(), 'code' => $e->getCode()]); } } // 保留原有验证和凭证函数 function verify_upload_request() { check_ajax_referer('ub_ajax_nonce', 'nonce'); if (!current_user_can('stm_lms_instructor')) { throw new Exception('你没有执行此操作的权限。', 403); } } function get_bunny_credentials() { $user_id = get_current_user_id(); $library_id = get_option('_bunny_library_id_' . $user_id); $library_api_key = get_option('_bunny_library_api_key_' . $user_id); if (!$library_id || !$library_api_key) { throw new Exception('缺少Bunny.net库凭证。', 400); } return ['library_id' => $library_id, 'api_key' => $library_api_key]; }
3. 额外优化建议
- 增加断点续传:记录已上传分片号,下次上传跳过已完成部分
- 前端添加分片重试机制:单个分片失败时自动重试
- 服务器端清理临时分片:定期删除未完成上传的临时文件
- 调整分片大小:根据带宽情况设置为10MB-50MB
内容的提问来源于stack exchange,提问作者Sok Leaphea
相关产品推荐
相关产品推荐

