Skip to content

Commit 531d326

Browse files
committed
Configure raw body handling at http foundation layer
1 parent 681c65d commit 531d326

File tree

5 files changed

+86
-9
lines changed

5 files changed

+86
-9
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
"fast-safe-stringify": "^2.1.1",
7373
"fastest-levenshtein": "^1.0.16",
7474
"fastify": "^4.28.1",
75+
"fastify-raw-body": "^4.3.0",
7576
"file-type": "^18.6.0",
7677
"glob": "^10.3.10",
7778
"got": "^14.3.0",

src/components/file/local-bucket.controller.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
Body,
23
Controller,
34
Get,
45
Headers,
@@ -9,9 +10,8 @@ import {
910
} from '@nestjs/common';
1011
import { DateTime } from 'luxon';
1112
import { URL } from 'node:url';
12-
import rawBody from 'raw-body';
1313
import { InputException } from '~/common';
14-
import { HttpAdapter, IRequest, IResponse } from '~/core/http';
14+
import { HttpAdapter, IRequest, IResponse, RawBody } from '~/core/http';
1515
import { FileBucket, InvalidSignedUrlException } from './bucket';
1616

1717
/**
@@ -27,14 +27,13 @@ export class LocalBucketController {
2727
) {}
2828

2929
@Put()
30+
@RawBody({ passthrough: true })
3031
async upload(
3132
@Headers('content-type') contentType: string,
3233
@Request() req: IRequest,
34+
@Body() contents: Buffer,
3335
) {
34-
// Chokes on json files because they are parsed with body-parser.
35-
// Need to disable it for this path or create a workaround.
36-
const contents = await rawBody(req);
37-
if (!contents) {
36+
if (!contents || !Buffer.isBuffer(contents)) {
3837
throw new InputException();
3938
}
4039

src/core/http/decorators.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
FASTIFY_ROUTE_CONFIG_METADATA,
33
FASTIFY_ROUTE_CONSTRAINTS_METADATA,
44
} from '@nestjs/platform-fastify/constants.js';
5+
import { Many } from '@seedcompany/common';
56
import { createMetadataDecorator } from '@seedcompany/nest';
67
import { FastifyContextConfig } from 'fastify';
78
import type { RouteConstraint } from 'fastify/types/route';
@@ -17,3 +18,39 @@ export const RouteConfig = createMetadataDecorator({
1718
types: ['class', 'method'],
1819
setter: (config: FastifyContextConfig) => config,
1920
});
21+
22+
/**
23+
* @example
24+
* ```ts
25+
* @RawBody()
26+
* route(
27+
* @Request('rawBody') raw: string,
28+
* @Body() contents: JSON
29+
* ) {}
30+
* ```
31+
* @example
32+
* ```ts
33+
* @RawBody({ passthrough: true })
34+
* route(
35+
* @Body() contents: Buffer
36+
* ) {}
37+
* ```
38+
*/
39+
export const RawBody = createMetadataDecorator({
40+
types: ['class', 'method'],
41+
setter: (
42+
config: {
43+
/**
44+
* Pass the raw body through to the handler or
45+
* just to keep the raw body in addition to regular content parsing.
46+
*/
47+
passthrough?: boolean;
48+
/**
49+
* The allowed content types.
50+
* Only applicable if passthrough is true.
51+
* Defaults to '*'
52+
*/
53+
allowContentTypes?: Many<string> | RegExp;
54+
} = {},
55+
) => config,
56+
});

src/core/http/http.adapter.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@ import {
1212
NestFastifyApplication,
1313
} from '@nestjs/platform-fastify';
1414
import type { FastifyInstance, HTTPMethods, RouteOptions } from 'fastify';
15+
import rawBody from 'fastify-raw-body';
1516
import * as zlib from 'node:zlib';
1617
import { ConfigService } from '~/core/config/config.service';
17-
import { RouteConfig, RouteConstraints } from './decorators';
18+
import { RawBody, RouteConfig, RouteConstraints } from './decorators';
1819
import type { CookieOptions, CorsOptions, IResponse } from './types';
1920

2021
export type NestHttpApplication = NestFastifyApplication & {
@@ -54,6 +55,9 @@ export class HttpAdapter extends PatchedFastifyAdapter {
5455
});
5556
await app.register(cookieParser);
5657

58+
// Only on routes we've decorated.
59+
await app.register(rawBody, { global: false });
60+
5761
app.setGlobalPrefix(config.hostUrl$.value.pathname.slice(1));
5862

5963
config.applyTimeouts(app.getHttpServer(), config.httpTimeouts);
@@ -71,6 +75,7 @@ export class HttpAdapter extends PatchedFastifyAdapter {
7175

7276
const config = RouteConfig.get(handler) ?? {};
7377
const constraints = RouteConstraints.get(handler) ?? {};
78+
const rawBody = RawBody.get(handler);
7479

7580
let version: VersionValue | undefined = (handler as any).version;
7681
version = version === VERSION_NEUTRAL ? undefined : version;
@@ -79,13 +84,36 @@ export class HttpAdapter extends PatchedFastifyAdapter {
7984
constraints.version = version;
8085
}
8186

87+
// Plugin configured to just add the rawBody property while continuing
88+
// to parse the content type normally.
89+
// Useful for signed webhook payload validation.
90+
if (rawBody && !rawBody.passthrough) {
91+
config.rawBody = true;
92+
}
93+
8294
const route: RouteOptions = {
8395
method,
8496
url,
8597
handler,
8698
...(Object.keys(constraints).length > 0 ? { constraints } : {}),
8799
...(Object.keys(config).length > 0 ? { config } : {}),
88100
};
101+
102+
if (rawBody?.passthrough) {
103+
const { allowContentTypes } = rawBody;
104+
const contentTypes = Array.isArray(allowContentTypes)
105+
? allowContentTypes.slice()
106+
: ((allowContentTypes ?? '*') as string | RegExp);
107+
return this.instance.register(async (child) => {
108+
child.removeAllContentTypeParsers();
109+
child.addContentTypeParser(
110+
contentTypes,
111+
{ parseAs: 'buffer' },
112+
(req, payload, done) => done(null, payload),
113+
);
114+
child.route(route);
115+
});
116+
}
89117
return this.instance.route(route);
90118
}
91119

yarn.lock

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5492,6 +5492,7 @@ __metadata:
54925492
fast-safe-stringify: "npm:^2.1.1"
54935493
fastest-levenshtein: "npm:^1.0.16"
54945494
fastify: "npm:^4.28.1"
5495+
fastify-raw-body: "npm:^4.3.0"
54955496
file-type: "npm:^18.6.0"
54965497
glob: "npm:^10.3.10"
54975498
got: "npm:^14.3.0"
@@ -6966,6 +6967,17 @@ __metadata:
69666967
languageName: node
69676968
linkType: hard
69686969

6970+
"fastify-raw-body@npm:^4.3.0":
6971+
version: 4.3.0
6972+
resolution: "fastify-raw-body@npm:4.3.0"
6973+
dependencies:
6974+
fastify-plugin: "npm:^4.0.0"
6975+
raw-body: "npm:^2.5.1"
6976+
secure-json-parse: "npm:^2.4.0"
6977+
checksum: 10c0/3260ab2fc3483a1668442b0a2b60a3f671948d8fc6e7a811ac782cfc28d31d8f064e7b3835ca21cb542d41c4a2a7bc84dd5c18ef0c38f90d7387dd6bbb83161d
6978+
languageName: node
6979+
linkType: hard
6980+
69696981
"fastify@npm:4.28.1, fastify@npm:^4.28.1":
69706982
version: 4.28.1
69716983
resolution: "fastify@npm:4.28.1"
@@ -11467,7 +11479,7 @@ __metadata:
1146711479
languageName: node
1146811480
linkType: hard
1146911481

11470-
"raw-body@npm:2.5.2":
11482+
"raw-body@npm:2.5.2, raw-body@npm:^2.5.1":
1147111483
version: 2.5.2
1147211484
resolution: "raw-body@npm:2.5.2"
1147311485
dependencies:
@@ -11975,7 +11987,7 @@ __metadata:
1197511987
languageName: node
1197611988
linkType: hard
1197711989

11978-
"secure-json-parse@npm:^2.7.0":
11990+
"secure-json-parse@npm:^2.4.0, secure-json-parse@npm:^2.7.0":
1197911991
version: 2.7.0
1198011992
resolution: "secure-json-parse@npm:2.7.0"
1198111993
checksum: 10c0/f57eb6a44a38a3eeaf3548228585d769d788f59007454214fab9ed7f01fbf2e0f1929111da6db28cf0bcc1a2e89db5219a59e83eeaec3a54e413a0197ce879e4

0 commit comments

Comments
 (0)