Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tidy-poets-shake.md
Original file line number Diff line number Diff line change
@@ -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.
18 changes: 15 additions & 3 deletions packages/root-cms/core/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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": "<csv text>"}`) 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
Expand All @@ -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) {
Expand Down
13 changes: 9 additions & 4 deletions packages/root-cms/core/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Loading