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" }
问题分析与解决方案
核心问题
- Multipart请求解析破坏原始结构:手动调用
await request.formData()解析请求体后再转发,会丢失浏览器自动生成的boundary标识,导致multer无法正确识别文件字段,进而无法解析上传的文件。 - Cookie头重复添加:
new Headers(request.headers)已经复制了原始请求的所有头信息,包括Cookie,再次append("Cookie", cookieHeader)会导致Cookie头重复,可能引发服务器端的解析异常。 - 非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
相关产品推荐
相关产品推荐

