Skip to content

Commit 560976f

Browse files
committed
feat(node): Add maxRequestBodySize
1 parent 640b57f commit 560976f

File tree

13 files changed

+448
-7
lines changed

13 files changed

+448
-7
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Payload for requests
2+
export function generatePayload(sizeInBytes: number): { data: string } {
3+
const baseSize = JSON.stringify({ data: '' }).length;
4+
const contentLength = sizeInBytes - baseSize;
5+
6+
return { data: 'x'.repeat(contentLength) };
7+
}
8+
9+
// Generate the "expected" body string
10+
export function generatePayloadString(dataLength: number, truncate?: boolean): string {
11+
const prefix = '{"data":"';
12+
const suffix = truncate ? '...' : '"}';
13+
14+
const baseStructuralLength = prefix.length + suffix.length;
15+
const dataContent = 'x'.repeat(dataLength - baseStructuralLength);
16+
17+
return `${prefix}${dataContent}${suffix}`;
18+
}
19+
20+
// Functions for non-ASCII payloads (e.g. emojis)
21+
export function generateEmojiPayload(sizeInBytes: number): { data: string } {
22+
const baseSize = JSON.stringify({ data: '' }).length;
23+
const contentLength = sizeInBytes - baseSize;
24+
25+
return { data: '👍'.repeat(contentLength) };
26+
}
27+
export function generateEmojiPayloadString(dataLength: number, truncate?: boolean): string {
28+
const prefix = '{"data":"';
29+
const suffix = truncate ? '...' : '"}';
30+
31+
const baseStructuralLength = suffix.length;
32+
const dataContent = '👍'.repeat(dataLength - baseStructuralLength);
33+
34+
return `${prefix}${dataContent}${suffix}`;
35+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import * as Sentry from '@sentry/node';
2+
import { loggingTransport } from '@sentry-internal/node-integration-tests';
3+
4+
Sentry.init({
5+
dsn: 'https://[email protected]/1337',
6+
release: '1.0',
7+
tracesSampleRate: 1.0,
8+
transport: loggingTransport,
9+
integrations: [Sentry.httpIntegration({ maxRequestBodySize: 'always' })],
10+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import * as Sentry from '@sentry/node';
2+
import { loggingTransport } from '@sentry-internal/node-integration-tests';
3+
4+
Sentry.init({
5+
dsn: 'https://[email protected]/1337',
6+
release: '1.0',
7+
tracesSampleRate: 1.0,
8+
transport: loggingTransport,
9+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import * as Sentry from '@sentry/node';
2+
import { loggingTransport } from '@sentry-internal/node-integration-tests';
3+
4+
Sentry.init({
5+
dsn: 'https://[email protected]/1337',
6+
release: '1.0',
7+
tracesSampleRate: 1.0,
8+
transport: loggingTransport,
9+
integrations: [Sentry.httpIntegration({ maxRequestBodySize: 'medium' })],
10+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import * as Sentry from '@sentry/node';
2+
import { loggingTransport } from '@sentry-internal/node-integration-tests';
3+
4+
Sentry.init({
5+
dsn: 'https://[email protected]/1337',
6+
release: '1.0',
7+
tracesSampleRate: 1.0,
8+
transport: loggingTransport,
9+
integrations: [Sentry.httpIntegration({ maxRequestBodySize: 'none' })],
10+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import * as Sentry from '@sentry/node';
2+
import { loggingTransport } from '@sentry-internal/node-integration-tests';
3+
4+
Sentry.init({
5+
dsn: 'https://[email protected]/1337',
6+
release: '1.0',
7+
tracesSampleRate: 1.0,
8+
transport: loggingTransport,
9+
integrations: [Sentry.httpIntegration({ maxRequestBodySize: 'small' })],
10+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import * as Sentry from '@sentry/node';
2+
import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests';
3+
import bodyParser from 'body-parser';
4+
import express from 'express';
5+
6+
const app = express();
7+
8+
// Increase limit for JSON parsing
9+
app.use(bodyParser.json({ limit: '3mb' }));
10+
app.use(express.json({ limit: '3mb' }));
11+
12+
app.post('/test-body-size', (req, res) => {
13+
const receivedSize = JSON.stringify(req.body).length;
14+
res.json({
15+
success: true,
16+
receivedSize,
17+
message: 'Payload processed successfully',
18+
});
19+
});
20+
21+
Sentry.setupExpressErrorHandler(app);
22+
23+
startExpressServerAndSendPortToRunner(app);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
import { afterAll, describe, expect } from 'vitest';
2+
import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../../utils/runner';
3+
import {
4+
generateEmojiPayload,
5+
generateEmojiPayloadString,
6+
generatePayload,
7+
generatePayloadString,
8+
} from './generatePayload';
9+
10+
// Value of MAX_BODY_BYTE_LENGTH in SentryHttpIntegration
11+
const MAX_GENERAL = 1024 * 1024; // 1MB
12+
const MAX_MEDIUM = 10_000;
13+
const MAX_SMALL = 1000;
14+
15+
describe('express with httpIntegration and not defined maxRequestBodySize', () => {
16+
afterAll(() => {
17+
cleanupChildProcesses();
18+
});
19+
20+
createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument-default.mjs', (createRunner, test) => {
21+
test('captures medium request bodies with default setting (medium)', async () => {
22+
const runner = createRunner()
23+
.expect({
24+
transaction: {
25+
transaction: 'POST /test-body-size',
26+
request: {
27+
data: JSON.stringify(generatePayload(MAX_MEDIUM)),
28+
},
29+
},
30+
})
31+
.start();
32+
33+
await runner.makeRequest('post', '/test-body-size', {
34+
headers: { 'Content-Type': 'application/json' },
35+
data: JSON.stringify(generatePayload(MAX_MEDIUM)),
36+
});
37+
38+
await runner.completed();
39+
});
40+
41+
test('truncates large request bodies with default setting (medium)', async () => {
42+
const runner = createRunner()
43+
.expect({
44+
transaction: {
45+
transaction: 'POST /test-body-size',
46+
request: {
47+
data: generatePayloadString(MAX_MEDIUM, true),
48+
},
49+
},
50+
})
51+
.start();
52+
53+
await runner.makeRequest('post', '/test-body-size', {
54+
headers: { 'Content-Type': 'application/json' },
55+
data: JSON.stringify(generatePayload(MAX_MEDIUM + 1)),
56+
});
57+
58+
await runner.completed();
59+
});
60+
});
61+
});
62+
63+
describe('express with httpIntegration and maxRequestBodySize: "none"', () => {
64+
afterAll(() => {
65+
cleanupChildProcesses();
66+
});
67+
68+
createEsmAndCjsTests(
69+
__dirname,
70+
'scenario.mjs',
71+
'instrument-none.mjs',
72+
(createRunner, test) => {
73+
test('does not capture any request bodies with none setting', async () => {
74+
const runner = createRunner()
75+
.expect({
76+
transaction: {
77+
transaction: 'POST /test-body-size',
78+
request: expect.not.objectContaining({
79+
data: expect.any(String),
80+
}),
81+
},
82+
})
83+
.start();
84+
85+
await runner.makeRequest('post', '/test-body-size', {
86+
headers: { 'Content-Type': 'application/json' },
87+
data: JSON.stringify(generatePayload(500)),
88+
});
89+
90+
await runner.completed();
91+
});
92+
},
93+
{ failsOnEsm: false },
94+
);
95+
});
96+
97+
describe('express with httpIntegration and maxRequestBodySize: "always"', () => {
98+
afterAll(() => {
99+
cleanupChildProcesses();
100+
});
101+
102+
createEsmAndCjsTests(
103+
__dirname,
104+
'scenario.mjs',
105+
'instrument-always.mjs',
106+
(createRunner, test) => {
107+
test('captures maximum allowed request body length with "always" setting', async () => {
108+
const runner = createRunner()
109+
.expect({
110+
transaction: {
111+
transaction: 'POST /test-body-size',
112+
request: {
113+
data: JSON.stringify(generatePayload(MAX_GENERAL)),
114+
},
115+
},
116+
})
117+
.start();
118+
119+
await runner.makeRequest('post', '/test-body-size', {
120+
headers: { 'Content-Type': 'application/json' },
121+
data: JSON.stringify(generatePayload(MAX_GENERAL)),
122+
});
123+
124+
await runner.completed();
125+
});
126+
127+
test('captures large request bodies with "always" setting but respects maximum size limit', async () => {
128+
const runner = createRunner()
129+
.expect({
130+
transaction: {
131+
transaction: 'POST /test-body-size',
132+
request: {
133+
data: generatePayloadString(MAX_GENERAL, true),
134+
},
135+
},
136+
})
137+
.start();
138+
139+
await runner.makeRequest('post', '/test-body-size', {
140+
headers: { 'Content-Type': 'application/json' },
141+
data: JSON.stringify(generatePayload(MAX_GENERAL + 1)),
142+
});
143+
144+
await runner.completed();
145+
});
146+
},
147+
{ failsOnEsm: false },
148+
);
149+
});
150+
151+
describe('express with httpIntegration and maxRequestBodySize: "small"', () => {
152+
afterAll(() => {
153+
cleanupChildProcesses();
154+
});
155+
156+
createEsmAndCjsTests(
157+
__dirname,
158+
'scenario.mjs',
159+
'instrument-small.mjs',
160+
(createRunner, test) => {
161+
test('keeps small request bodies with "small" setting', async () => {
162+
const runner = createRunner()
163+
.expect({
164+
transaction: {
165+
transaction: 'POST /test-body-size',
166+
request: {
167+
data: JSON.stringify(generatePayload(MAX_SMALL)),
168+
},
169+
},
170+
})
171+
.start();
172+
173+
await runner.makeRequest('post', '/test-body-size', {
174+
headers: { 'Content-Type': 'application/json' },
175+
data: JSON.stringify(generatePayload(MAX_SMALL)),
176+
});
177+
178+
await runner.completed();
179+
});
180+
181+
test('truncates too large request bodies with "small" setting', async () => {
182+
const runner = createRunner()
183+
.expect({
184+
transaction: {
185+
transaction: 'POST /test-body-size',
186+
request: {
187+
data: generatePayloadString(MAX_SMALL, true),
188+
},
189+
},
190+
})
191+
.start();
192+
193+
await runner.makeRequest('post', '/test-body-size', {
194+
headers: { 'Content-Type': 'application/json' },
195+
data: JSON.stringify(generatePayload(MAX_SMALL + 1)),
196+
});
197+
198+
await runner.completed();
199+
});
200+
201+
test('truncates too large non-ASCII request bodies with "small" setting', async () => {
202+
const runner = createRunner()
203+
.expect({
204+
transaction: {
205+
transaction: 'POST /test-body-size',
206+
request: {
207+
// 250 emojis, each 4 bytes in UTF-8 (resulting in 1000 bytes --> MAX_SMALL)
208+
data: generateEmojiPayloadString(250, true),
209+
},
210+
},
211+
})
212+
.start();
213+
214+
await runner.makeRequest('post', '/test-body-size', {
215+
headers: { 'Content-Type': 'application/json' },
216+
data: JSON.stringify(generateEmojiPayload(MAX_SMALL + 1)),
217+
});
218+
219+
await runner.completed();
220+
});
221+
},
222+
{ failsOnEsm: false },
223+
);
224+
});
225+
226+
describe('express with httpIntegration and maxRequestBodySize: "medium"', () => {
227+
afterAll(() => {
228+
cleanupChildProcesses();
229+
});
230+
231+
createEsmAndCjsTests(
232+
__dirname,
233+
'scenario.mjs',
234+
'instrument-medium.mjs',
235+
(createRunner, test) => {
236+
test('keeps medium request bodies with "medium" setting', async () => {
237+
const runner = createRunner()
238+
.expect({
239+
transaction: {
240+
transaction: 'POST /test-body-size',
241+
request: {
242+
data: JSON.stringify(generatePayload(MAX_MEDIUM)),
243+
},
244+
},
245+
})
246+
.start();
247+
248+
await runner.makeRequest('post', '/test-body-size', {
249+
headers: { 'Content-Type': 'application/json' },
250+
data: JSON.stringify(generatePayload(MAX_MEDIUM)),
251+
});
252+
253+
await runner.completed();
254+
});
255+
256+
test('truncates large request bodies with "medium" setting', async () => {
257+
const runner = createRunner()
258+
.expect({
259+
transaction: {
260+
transaction: 'POST /test-body-size',
261+
request: {
262+
data: generatePayloadString(MAX_MEDIUM, true),
263+
},
264+
},
265+
})
266+
.start();
267+
268+
await runner.makeRequest('post', '/test-body-size', {
269+
headers: { 'Content-Type': 'application/json' },
270+
data: JSON.stringify(generatePayload(MAX_MEDIUM + 1)),
271+
});
272+
273+
await runner.completed();
274+
});
275+
},
276+
{ failsOnEsm: false },
277+
);
278+
});

0 commit comments

Comments
 (0)