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

Cloudflare Workers子域名转子目录后文件上传失败问题排查

问题描述

我有一个部署在blog.domain.com的博客,希望通过Cloudflare Workers将其从子域名切换到子目录形式(对应地址为https://www.domain.in/blog/)。目前除文件上传功能外,其余功能均正常。当上传文件时,浏览器会发送一个携带FormData的XHR POST请求,具体信息如下:

Request URL:
https://www.domain.in/blog/ghost/api/admin/images/upload/
Request Method:
POST
Status Code:
422 Unprocessable Content
Referrer Policy:
strict-origin-when-cross-origin

文件上传的Content-Type由浏览器自动设置:

Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryglip7bP33nuVUYAE

我的Cloudflare Worker代码如下:

/**
 * Welcome to Cloudflare Workers! This is your first worker.
 *
 * - Run `npm run dev` in your terminal to start a development server
 * - Open a browser tab at http://localhost:8787/ to see your worker in action
 * - Run `npm run deploy` to publish your worker
 *
 * Learn more at https://developers.cloudflare.com/workers/
 */

export interface Env {
    // Example binding to KV. Learn more at https://developers.cloudflare.com/workers/runtime-apis/kv/
    // MY_KV_NAMESPACE: KVNamespace;
    //
    // Example binding to Durable Object. Learn more at https://developers.cloudflare.com/workers/runtime-apis/durable-objects/
    // MY_DURABLE_OBJECT: DurableObjectNamespace;
    //
    // Example binding to R2. Learn more at https://developers.cloudflare.com/workers/runtime-apis/r2/
    // MY_BUCKET: R2Bucket;
    //
    // Example binding to a Service. Learn more at https://developers.cloudflare.com/workers/runtime-apis/service-bindings/
    // MY_SERVICE: Fetcher;
    //
    // Example binding to a Queue. Learn more at https://developers.cloudflare.com/queues/javascript-apis/
    // MY_QUEUE: Queue;
}

export default {
    async fetch(request : Request){
        async function handleRequest(request : Request) {
            const url = new URL(request.url);
            const originUrl = url.toString().replace("https://www.domain.in", "https://blog.domain.in");
            const cookieHeader = request.headers.get("Cookie");
            const headers = new Headers(request.headers);
        
            if (cookieHeader) {
              headers.append("Cookie", cookieHeader);
            }
        
            let originPage;
        
            if (["POST", "PUT", "DELETE"].includes(request.method)) {
              let contentType = request.headers.get("Content-Type") ;
              if (contentType && contentType.includes("multipart/form-data")) {
                let formData = await request.formData();
                originPage = await fetch(originUrl, {
                  method: request.method,
                  headers,
                  body: formData,
                });
              } else {
                originPage = await fetch(originUrl, {
                  method: request.method,
                  headers,
                  body: await request.text(),
                });
              }
            } else {
              originPage = await fetch(originUrl, {
                method: request.method,
                headers,
              });
            }
        
            const newResponse = new Response(originPage.body, originPage);
            return newResponse;
          };

        const response = await handleRequest(request);
        return response ;
    }
};

请求已到达源服务器,但multer未填充文件路径,导致上传被拒绝并提示验证错误。直接从blog.domain.in上传文件时功能正常。

源服务器报错日志如下:

[90mValidationError: Please select an image.
    at uploadValidation (/var/lib/ghost/versions/5.74.0/core/server/web/api/middleware/upload.js:149:25)
    at Layer.handle [as handle_request] (/var/lib/ghost/versions/5.74.0/node_modules/express/lib/router/layer.js:95:5)
    at next (/var/lib/ghost/versions/5.74.0/node_modules/express/lib/router/route.js:144:13)
    at /var/lib/ghost/versions/5.74.0/core/server/web/api/middleware/upload.js:76:9
    at Immediate._onImmediate (/var/lib/ghost/versions/5.74.0/node_modules/multer/lib/make-middleware.js:53:37)
    at process.processImmediate (node:internal/timers:478:21)[39m
[39m
[2023-11-28 04:36:17] [31mERROR[39m The "path" argument must be of type string or an instance of Buffer or URL. Received undefined
[31m
[31mThe "path" argument must be of type string or an instance of Buffer or URL. Received undefined[39m

[1m[37mError Code: [39m[22m
    [90mERR_INVALID_ARG_TYPE[39m

我的wrangler.toml配置如下:

name = "ghost"
main = "src/index.ts"
compatibility_date = "2023-11-21"

route = { pattern = "https://www.domain.in/blog*", zone_name = "domain.in" }

问题分析与解决方案

核心问题

  1. Multipart请求解析破坏原始结构:手动调用await request.formData()解析请求体后再转发,会丢失浏览器自动生成的boundary标识,导致multer无法正确识别文件字段,进而无法解析上传的文件。
  2. Cookie头重复添加:new Headers(request.headers)已经复制了原始请求的所有头信息,包括Cookie,再次append("Cookie", cookieHeader)会导致Cookie头重复,可能引发服务器端的解析异常。
  3. 非Multipart请求体处理不当:将POST/PUT/DELETE请求体转成text()再转发,会破坏原始请求的编码格式(如JSON请求的二进制结构),可能导致服务器无法正确解析请求。

修复后的Worker代码

export interface Env {}

export default {
    async fetch(request: Request) {
        async function handleRequest(request: Request) {
            const url = new URL(request.url);
            // 替换域名生成源站请求地址
            const originUrl = url.toString().replace("https://www.domain.in", "https://blog.domain.in");
            // 复制原始请求头,避免修改原始对象
            const headers = new Headers(request.headers);

            // 移除可能的Host头,让fetch自动设置为源站域名(blog.domain.in)
            headers.delete("Host");

            let originPage;
            // 对于写请求,直接转发原始请求体,不需要手动解析
            if (["POST", "PUT", "DELETE"].includes(request.method)) {
                originPage = await fetch(originUrl, {
                    method: request.method,
                    headers,
                    body: request.body,
                    // 保留原始请求的mode和credentials配置
                    mode: request.mode,
                    credentials: request.credentials
                });
            } else {
                originPage = await fetch(originUrl, {
                    method: request.method,
                    headers,
                    mode: request.mode,
                    credentials: request.credentials
                });
            }

            // 构造响应返回,保留源站的所有响应头和状态码
            const newResponse = new Response(originPage.body, originPage);
            return newResponse;
        };

        return handleRequest(request);
    }
};

关键修改说明

  • 直接转发原始请求体:不再手动解析FormData,直接使用request.body转发,完整保留multipart请求的boundary和文件数据,确保multer能正确解析。
  • 修复Cookie头问题:移除重复添加Cookie的代码,依赖new Headers(request.headers)复制原始Cookie头,避免重复。
  • 移除Host头:让fetch自动设置Host为源站域名blog.domain.in,确保源站接收正确的Host标识。
  • 保留请求模式:添加mode和credentials配置,确保跨域请求的凭证(如Cookie)正确传递。

内容的提问来源于stack exchange,提问作者Naga Sai

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.05 04:02:03