From eb4ee00610e4bb56b7dc2e4e6275eb6311ce3e0f Mon Sep 17 00:00:00 2001 From: Andrew Plummer Date: Sat, 11 Nov 2023 23:54:18 +0900 Subject: [PATCH] allow files and data to be send in the same multipart request --- services/api/package.json | 1 + services/api/src/app.js | 4 +- .../utils/middleware/__fixtures__/test.png | Bin 0 -> 1939 bytes .../src/utils/middleware/__tests__/body.js | 39 ++++++++++++++++++ services/api/src/utils/middleware/body.js | 25 +++++++++++ services/api/src/utils/testing/request.js | 5 ++- services/web/src/utils/api/request.js | 2 +- 7 files changed, 71 insertions(+), 5 deletions(-) create mode 100644 services/api/src/utils/middleware/__fixtures__/test.png create mode 100644 services/api/src/utils/middleware/__tests__/body.js create mode 100644 services/api/src/utils/middleware/body.js diff --git a/services/api/package.json b/services/api/package.json index 8b804f45e..649adfede 100644 --- a/services/api/package.json +++ b/services/api/package.json @@ -35,6 +35,7 @@ "jszip": "^3.10.1", "koa": "^2.14.2", "koa-body": "^6.0.1", + "koa-compose": "^4.1.0", "lodash": "^4.17.21", "marked": "^5.1.0", "mongoose": "^6.9.1", diff --git a/services/api/src/app.js b/services/api/src/app.js index ab5620df5..2eea86952 100644 --- a/services/api/src/app.js +++ b/services/api/src/app.js @@ -1,9 +1,9 @@ const Router = require('@koa/router'); const Koa = require('koa'); const { version } = require('../package.json'); -const { koaBody: bodyParser } = require('koa-body'); const errorHandler = require('./utils/middleware/error-handler'); const corsMiddleware = require('./utils/middleware/cors'); +const bodyMiddleware = require('./utils/middleware/body'); const recordMiddleware = require('./utils/middleware/record'); const serializeMiddleware = require('./utils/middleware/serialize'); const { applicationMiddleware } = require('./utils/middleware/application'); @@ -48,7 +48,7 @@ if (['development'].includes(ENV_NAME)) { app.use(errorHandler); -app.use(logger.middleware()).use(bodyParser({ multipart: true })); +app.use(logger.middleware()).use(bodyMiddleware()); app.on('error', (err, ctx) => { if (err.code === 'EPIPE' || err.code === 'ECONNRESET') { diff --git a/services/api/src/utils/middleware/__fixtures__/test.png b/services/api/src/utils/middleware/__fixtures__/test.png new file mode 100644 index 0000000000000000000000000000000000000000..be9bf72ac3274a63f123feeb404ff0449146f397 GIT binary patch literal 1939 zcmV;E2WP)Px#lTb`lMgRZ*^7a4g^8OAW_5A(+0~7W1{Qm$8^Z>;2Eu{j<3IK&tl* zmi2mr`AqojtlIp0#QNp_{4)3I!TtFeoAuuC{tfl#jm-Mi{rji<_AsONANJ_k-2GCr z_>21UXZi1))B7Wy_IvvA?!5M|000I%Nkl0k7#8Rhu=%fHgT38eGm`f38{60*D;L((Pm z^Qi%(OUT}GjF2uVOUv(RTv%zBHq^rNDAAA$~ zY69u%EJr%&>g(xGzCpeB8dQYUgHIcGwKzUQl~{FK{4#Lo>J7+V`_6S(JzemH{m|W2 zbK94hfy5Thqq7`7R#AhWoWBU4_oa)v-HCI)DgDm7x9>3g)749Cfx6j>|2xeo_s${= zf7|Kw8G*XTVR#rn-ItoqTxvC=_Zfk@%VE64#CC6aL@(3M*|Pm6^^L-MS=i zy+7CUm6lGK>3k6-u_fx>;)j)z*hiiep4b9)_uG`yj3;(11J62QeMX{gn0}hs#O^G4 zhHiU+ffA@YW*ufUu~T1D#_1^c8G*V@_F;T$EU}M6t`MitKnc`+<{f4#v9r?o5=Sku zL)1CTg42v7cBe4PIkEE$ltA4-hg2Y$aOI>kkk}G+E0xo96Z>(?0x0Ar*D*Cvs=4Po3Fu({xHr z?0uF)%F|I3A1kht*b;SHK0$%Nv#R!!<=|;E<)+0$l}Jk zXx<~B7cl&h4-{y?Z_@Xo3LJ``SMB3u8`DZJw7R zBh@=!&`0sAN9a`}!L+jD3VoQa z5f_1o>X?3lu_2Ep><)_2=c@cND845!fw}}l2W zW5XLWO(Fg%Z9|D;gM>N_|CAWhB{qQ0t-Vu+BgQtATO1q0?dl6~WmjW5uqQabmAT&~ z0uYIe>Cm14U)~w@PjW+v?g>yQY8E0_p^^E1%uH)ddSqk>nlSSM5O6LheB3lGz(0lH zA`JHCg}C7JktM4Ba{CufF7004NL { + it('should be able to send files and data in the same request', async () => { + // Leaning on supertest and koa here as multipart form + // data is difficult to send through a node stream. + const router = new Router(); + router.post('/', (ctx) => { + const { originalFilename } = ctx.request.files.file; + ctx.body = { + ...ctx.request.body, + originalFilename, + }; + }); + + const app = new Koa(); + app.use(bodyMiddleware()); + app.use(router.routes()); + + const response = await request( + 'POST', + '/', + { + foo: 'bar', + }, + { app, file } + ); + expect(response.body).toEqual({ + foo: 'bar', + originalFilename: 'test.png', + }); + }); +}); diff --git a/services/api/src/utils/middleware/body.js b/services/api/src/utils/middleware/body.js new file mode 100644 index 000000000..6bf753911 --- /dev/null +++ b/services/api/src/utils/middleware/body.js @@ -0,0 +1,25 @@ +const compose = require('koa-compose'); +const { koaBody } = require('koa-body'); + +module.exports = function () { + return compose([ + koaBody({ + multipart: true, + }), + parseMultipartBody(), + ]); +}; + +// Multipart bodies are assumed to be simple key/value pairs. +// This middleware parses each field as JSON to allow the client +// to send files and JSON data in the same request. +function parseMultipartBody() { + return async (ctx, next) => { + if (ctx.request.files) { + for (let [key, value] of Object.entries(ctx.request.body)) { + ctx.request.body[key] = JSON.parse(value); + } + } + return next(); + }; +} diff --git a/services/api/src/utils/testing/request.js b/services/api/src/utils/testing/request.js index 664007bc0..7becf77c7 100644 --- a/services/api/src/utils/testing/request.js +++ b/services/api/src/utils/testing/request.js @@ -1,5 +1,4 @@ const request = require('supertest'); //eslint-disable-line -const app = require('../../app'); const qs = require('querystring'); const { createAuthTokenPayload, createAuthToken } = require('../tokens'); @@ -21,6 +20,8 @@ module.exports = async function handleRequest(httpMethod, url, bodyOrQuery = {}, headers.organization = options.organization.id; } + const app = options.app || require('../../app'); + let promise; if (options.file) { @@ -30,7 +31,7 @@ module.exports = async function handleRequest(httpMethod, url, bodyOrQuery = {}, promise = promise.attach('file', file); } for (let [key, value] of Object.entries(bodyOrQuery)) { - promise = promise.field(key, value); + promise = promise.field(key, JSON.stringify(value)); } } else { if (httpMethod === 'POST') { diff --git a/services/web/src/utils/api/request.js b/services/web/src/utils/api/request.js index d7c152662..07e3aa782 100644 --- a/services/web/src/utils/api/request.js +++ b/services/web/src/utils/api/request.js @@ -39,7 +39,7 @@ export default async function request(options) { data.append('file', file); }); for (let [key, value] of Object.entries(body || {})) { - data.append(key, value); + data.append(key, JSON.stringify(value)); } body = data; } else if (!(body instanceof FormData)) {