diff --git a/.changeset/tidy-poets-shake.md b/.changeset/tidy-poets-shake.md new file mode 100644 index 000000000..c886dc719 --- /dev/null +++ b/.changeset/tidy-poets-shake.md @@ -0,0 +1,5 @@ +--- +'@blinkk/root-cms': patch +--- + +Send CSV imports as JSON instead of multipart/form-data. Some deployments sit behind WAFs/proxies that block multipart requests; the csv.import endpoint now accepts a JSON body (`{"csv": "..."}`) and the CMS UI sends JSON. Multipart uploads remain supported for backwards compatibility. diff --git a/packages/root-cms/core/api.ts b/packages/root-cms/core/api.ts index adf7a8f64..bd7a7c7b2 100644 --- a/packages/root-cms/core/api.ts +++ b/packages/root-cms/core/api.ts @@ -388,6 +388,11 @@ export function api(server: Server, options: ApiOptions) { * Imports a CSV file and returns a JSON array of objects representing the * CSV. * + * Accepts either a JSON body (`{"csv": ""}`) or a + * `multipart/form-data` upload with a `file` field. The CMS UI sends JSON + * since some deployments sit behind WAFs that block multipart requests; + * multipart is kept for backwards compatibility. + * * Sample response: * * ```json @@ -404,14 +409,21 @@ export function api(server: Server, options: ApiOptions) { '/cms/api/csv.import', multipartMiddleware(), (req: Request, res: Response) => { - if (req.method !== 'POST' || !req.files || !req.files.file) { + if (req.method !== 'POST') { res.status(400).json({success: false, error: 'BAD_REQUEST'}); return; } try { - const file = req.files.file; - const csvString = file.buffer.toString('utf8'); + let csvString: string; + if (req.files?.file) { + csvString = req.files.file.buffer.toString('utf8'); + } else if (typeof req.body?.csv === 'string') { + csvString = req.body.csv; + } else { + res.status(400).json({success: false, error: 'BAD_REQUEST'}); + return; + } const rows = csvToArray(csvString); res.status(200).json({success: true, data: rows}); } catch (err) { diff --git a/packages/root-cms/core/plugin.ts b/packages/root-cms/core/plugin.ts index 3de607a92..87989294a 100644 --- a/packages/root-cms/core/plugin.ts +++ b/packages/root-cms/core/plugin.ts @@ -870,11 +870,16 @@ export function cmsPlugin(options: CMSPluginOptions): CMSPlugin { serverOptions: ConfigureServerOptions ) => { // The AI "prepare" routes can carry a moderately large body (the edit - // flow sends the JSON object being edited). Use a larger limit for those - // routes only; body-parser short-circuits when `req._body` is already - // set, so the global parser below is a no-op for them. + // flow sends the JSON object being edited), and csv.import receives the + // full CSV text as JSON. Use a larger limit for those routes only; + // body-parser short-circuits when `req._body` is already set, so the + // global parser below is a no-op for them. server.use( - ['/cms/api/ai.chat.prepare', '/cms/api/ai.edit.prepare'], + [ + '/cms/api/ai.chat.prepare', + '/cms/api/ai.edit.prepare', + '/cms/api/csv.import', + ], bodyParser.json({limit: '4mb'}) ); server.use(bodyParser.json()); diff --git a/packages/root-cms/ui/components/LocalizationModal/LocalizationModal.tsx b/packages/root-cms/ui/components/LocalizationModal/LocalizationModal.tsx index 2ffa526b4..03102434d 100644 --- a/packages/root-cms/ui/components/LocalizationModal/LocalizationModal.tsx +++ b/packages/root-cms/ui/components/LocalizationModal/LocalizationModal.tsx @@ -711,12 +711,14 @@ LocalizationModal.Translations = (props: TranslationsProps) => { fileInput.addEventListener('change', async () => { const file = fileInput.files?.[0]; if (file) { - const formData = new FormData(); - formData.append('file', file); try { + // Send the CSV as JSON rather than multipart/form-data; some + // deployments sit behind WAFs that block multipart requests. + const csv = await file.text(); const res = await fetch('/cms/api/csv.import', { method: 'POST', - body: formData, + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({csv}), }); if (res.status !== 200) { const errorText = await res.text();