A minimal multipart middleware for @chubbyts/chubbyts-http-types.
It works with any implementation of the types (e.g. @chubbyts/chubbyts-http): the request header gets read and the request gets changed through the injected helpers only.
- node: 22
- @chubbyts/chubbyts-dic-config-factory: ^1.0.1
- @chubbyts/chubbyts-dic-types: ^2.3.1
- @chubbyts/chubbyts-http-types: ^4.0.0
- busboy: ^1.6.0
Through NPM as @chubbyts/chubbyts-http-multipart.
npm i @chubbyts/chubbyts-http-multipart@^3.0.0import { text } from 'node:stream/consumers';
import { createResponseFactory, createServerRequestFactory } from '@chubbyts/chubbyts-http/message-factory';
import { createServerRequestHelpers } from '@chubbyts/chubbyts-http/message-helper';
import type { Handler } from '@chubbyts/chubbyts-http-types/handler';
import {
createMultipartMiddleware,
isMultipartLimitError,
parseMultipartFile,
} from '@chubbyts/chubbyts-http-multipart/multipart-middleware';
const serverRequestFactory = createServerRequestFactory();
const serverRequestHelpers = createServerRequestHelpers();
const responseFactory = createResponseFactory();
const multipartMiddleware = createMultipartMiddleware(
serverRequestHelpers,
{ fileSize: 10 * 1024 * 1024, files: 5 }, // (see the `limits` of busboy)
);
// if the original content-type of the request was multipart/form-data, the content-type is now
// application/x-www-form-urlencoded and the body got replaced
const handler: Handler = async (request) => {
try {
const data = new URLSearchParams(await text(request.body));
// undefined, if the value is not a file written by the middleware for this request
const avatar = parseMultipartFile(request, data.get('avatar'));
// { path: '/tmp/multipart-XXXXXX/...', filename: 'avatar.png', mimeType: 'image/png' }
return responseFactory(200);
} catch (error) {
return responseFactory(isMultipartLimitError(error) ? 413 : 400);
}
};
(async () => {
const serverRequest = serverRequestFactory('POST', 'https://example.com/');
const response = await multipartMiddleware(serverRequest, handler);
})();Only a request with a multipart/form-data content-type including a boundary gets handled, any other request gets
passed to the handler as it is. The content-length header of a handled request gets removed, as it does not match the
new body.
The files get written into a temporary directory per request (<tmpdir>/multipart-XXXXXX/...), accessible by the
owner only. A file is complete as soon as the new body ended, so read the whole body before using a file, removing the
files is up to the application then. The names and filenames are read as UTF-8 (RFC 7578). A file without filename (e.g. a part with content-type
application/octet-stream) gets passed without filename=.
Use parseMultipartFile for the values of files: a client can send a field whose value looks like a file (e.g.
/etc/passwd; filename=avatar.png; mimeType=image/png), only a file within the temporary directory of the request (the
attribute multipartTemporaryPath) got written by the middleware. It reads the filename correctly, even if it contains
; mimeType=.
Configure the limits, there are none by default (beside the fieldSize of 1 MiB of busboy). Reaching a limit gets
thrown as MultipartLimitError (code: 'MULTIPART_LIMIT', limit: 'fileSize' | 'files' | 'fields' | 'fieldSize' | 'parts', see isMultipartLimitError) while iterating the new body, nothing gets truncated or dropped silently. The
fieldNameSize limit does not apply, busboy does not truncate the names of a multipart body.
A malformed multipart body, a failing request body or a failing write of a file gets thrown while iterating the new
body as well (also if the new body gets read after the error happened). On any error, and if the handler is done
(returned or thrown) before the new body got read till its end, no further file gets written and the temporary directory
including the files written so far gets removed by the middleware. The rest of the request body gets still read (and
discarded), so the response can be sent. The fields get read only as fast as the new body gets read, the content of a
file gets written as fast as it arrives (even if nobody reads the new body yet), so limit it by fileSize and files.
----------------------------394107496171408467161617
Content-Disposition: form-data; name="id"
123e4567-e89b-12d3-a456-426655440000
----------------------------394107496171408467161617
Content-Disposition: form-data; name="name"
John Doe
----------------------------394107496171408467161617
Content-Disposition: form-data; name="address"
Content-Type: application/json
{
"street": "3, Garden St",
"city": "Hillsbery, UT"
}
----------------------------394107496171408467161617
Content-Disposition: form-data; name="red"; filename="red.png"
Content-Type: image/png
<binary>
----------------------------394107496171408467161617
Content-Disposition: form-data; name="green"; filename="green.png"
Content-Type: image/png
<binary>
----------------------------394107496171408467161617
Content-Disposition: form-data; name="blue"; filename="blue.png"
Content-Type: image/png
<binary>
----------------------------394107496171408467161617--
Optimized for human readability
id=123e4567-e89b-12d3-a456-426655440000&
name=John Doe&
address={\r\n "street": "3, Garden St",\r\n "city": "Hillsbery, UT"\r\n}&
red=/tmp/multipart-XXXXXX/...; filename=red.png; mimeType=image/png&
green=/tmp/multipart-XXXXXX/...; filename=green.png; mimeType=image/png&
blue=/tmp/multipart-XXXXXX/...; filename=blue.png; mimeType=image/png&
The package ships a service factory (an abstract factory built on chubbyts-dic-config-factory) for a chubbyts-dic-config (or any chubbyts-dic-types compatible) container within @chubbyts/chubbyts-http-multipart/service-factory, configured through config.chubbyts.multipart (optional, see the limits of busboy). The implementation of the types gets read from the service serverRequestHelpers, which has to be registered:
import type { ConfigFactory } from '@chubbyts/chubbyts-dic-config/dist/dic-config';
import { createContainerByConfigFactory } from '@chubbyts/chubbyts-dic-config/dist/dic-config';
import { createServerRequestHelpers } from '@chubbyts/chubbyts-http/message-helper';
import type { MultipartConfig } from '@chubbyts/chubbyts-http-multipart/service-factory';
import { multipartMiddlewareServiceFactory } from '@chubbyts/chubbyts-http-multipart/service-factory';
import type { Middleware } from '@chubbyts/chubbyts-http-types/middleware';
const container = createContainerByConfigFactory({
chubbyts: {
multipart: {
// limits: { fileSize: 10 * 1024 * 1024, files: 5 },
} satisfies MultipartConfig,
},
dependencies: {
factories: new Map<string, ConfigFactory>([
['serverRequestHelpers', () => createServerRequestHelpers()],
['multipartMiddleware', multipartMiddlewareServiceFactory()],
]),
},
})();
const multipartMiddleware = container.get<Middleware>('multipartMiddleware');To use different limits for different parts of an api, the factory can be registered multiple times with a name: the config is then read from config.chubbyts.multipart.<name>.
const container = createContainerByConfigFactory({
chubbyts: {
multipart: {
api: { limits: { fileSize: 1024 * 1024 } },
admin: { limits: { fileSize: 100 * 1024 * 1024 } },
} satisfies Record<string, MultipartConfig>,
},
dependencies: {
factories: new Map<string, ConfigFactory>([
// serverRequestHelpers like above
['multipartMiddlewareapi', multipartMiddlewareServiceFactory('api')],
['multipartMiddlewareadmin', multipartMiddlewareServiceFactory('admin')],
]),
},
})();
const apiMultipartMiddleware = container.get<Middleware>('multipartMiddlewareapi');
const adminMultipartMiddleware = container.get<Middleware>('multipartMiddlewareadmin');2026 Dominik Zogg