From b9754c80e72d643c67420e914e2479657b5b0a32 Mon Sep 17 00:00:00 2001 From: kptdobe Date: Thu, 7 May 2026 15:05:00 +0200 Subject: [PATCH] fix(copy): return 400 instead of 500 when /copy POST body is not form-encoded COR-1 daily review (2026-05-07) surfaced 5 production POST 500s on /copy/scdemos/demo[/about-us.html] with unhandled: TypeError: Unrecognized Content-Type header value. FormData can only parse the following MIME types: multipart/form-data, application/x-www-form-urlencoded Cloudflare's req.formData() throws synchronously on unsupported MIME types. The throw was bubbling out of the worker as a 500. Wrap it in try/catch and return a structured 400 via the existing { error } shape. Coralogix query (last 24h): source logs last 24h | filter \$d.ScriptName == 'da-admin' | filter \$d.Outcome == 'exception' Co-Authored-By: Paperclip --- src/helpers/copy.js | 12 +++++++++++- test/routes/copy.test.js | 29 +++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/src/helpers/copy.js b/src/helpers/copy.js index b081062a..39fd4eb2 100644 --- a/src/helpers/copy.js +++ b/src/helpers/copy.js @@ -14,8 +14,18 @@ const NO_DEST_ERROR = { status: 400, }; +const BAD_CONTENT_TYPE_ERROR = { + body: JSON.stringify({ error: 'Invalid Content-Type. Expected multipart/form-data or application/x-www-form-urlencoded.' }), + status: 400, +}; + export default async function copyHelper(req, daCtx) { - const formData = await req.formData(); + let formData; + try { + formData = await req.formData(); + } catch { + return { error: BAD_CONTENT_TYPE_ERROR }; + } if (!formData) return {}; const fullDest = formData.get('destination'); if (!fullDest) return { error: NO_DEST_ERROR }; diff --git a/test/routes/copy.test.js b/test/routes/copy.test.js index 2d91dc46..ff25ed9f 100644 --- a/test/routes/copy.test.js +++ b/test/routes/copy.test.js @@ -70,6 +70,35 @@ describe('Copy Route', () => { assert.strictEqual(false, copyCalled[0].m); }); + it('Test copyHandler returns 400 when request body is not form-encoded', async () => { + const copyCalled = []; + const copyObject = (e, c, d, m) => { + copyCalled.push({ + e, c, d, m, + }); + return { status: 200 }; + }; + + const copyHandler = await esmock('../../src/routes/copy.js', { + '../../src/storage/object/copy.js': { + default: copyObject, + }, + '../../src/utils/auth.js': { hasPermission: () => true }, + }); + + const req = { + formData: () => { + throw new TypeError('Unrecognized Content-Type header value. FormData can only parse the following MIME types: multipart/form-data, application/x-www-form-urlencoded'); + }, + }; + + const resp = await copyHandler({ req, env: {}, daCtx: { key: 'my/src.html' } }); + assert.strictEqual(resp.status, 400); + assert.strictEqual(copyCalled.length, 0); + const body = JSON.parse(resp.body); + assert.match(body.error, /Content-Type/i); + }); + it('Test copyHandler - no destination provided', async () => { const copyCalled = []; const copyObject = (e, c, d, m) => {