Skip to content

Commit

Permalink
allow files and data to be send in the same multipart request
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewplummer committed Nov 11, 2023
1 parent 7998250 commit eb4ee00
Show file tree
Hide file tree
Showing 7 changed files with 71 additions and 5 deletions.
1 change: 1 addition & 0 deletions services/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 2 additions & 2 deletions services/api/src/app.js
Original file line number Diff line number Diff line change
@@ -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');
Expand Down Expand Up @@ -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') {
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
39 changes: 39 additions & 0 deletions services/api/src/utils/middleware/__tests__/body.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
const Koa = require('koa');
const Router = require('@koa/router');

const bodyMiddleware = require('../body');
const { request } = require('../../testing');

const file = __dirname + '/../__fixtures__/test.png';

describe('bodyMiddleware', () => {
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',
});
});
});
25 changes: 25 additions & 0 deletions services/api/src/utils/middleware/body.js
Original file line number Diff line number Diff line change
@@ -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();
};
}
5 changes: 3 additions & 2 deletions services/api/src/utils/testing/request.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
const request = require('supertest'); //eslint-disable-line
const app = require('../../app');
const qs = require('querystring');
const { createAuthTokenPayload, createAuthToken } = require('../tokens');

Expand All @@ -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) {
Expand All @@ -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') {
Expand Down
2 changes: 1 addition & 1 deletion services/web/src/utils/api/request.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down

0 comments on commit eb4ee00

Please sign in to comment.