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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 20:43:09