Skip to content

feat(node): Add maxIncomingRequestBodySize #16225

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
May 15, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Payload for requests
export function generatePayload(sizeInBytes: number): { data: string } {
const baseSize = JSON.stringify({ data: '' }).length;
const contentLength = sizeInBytes - baseSize;

return { data: 'x'.repeat(contentLength) };
}

// Generate the "expected" body string
export function generatePayloadString(dataLength: number, truncate?: boolean): string {
const prefix = '{"data":"';
const suffix = truncate ? '...' : '"}';

const baseStructuralLength = prefix.length + suffix.length;
const dataContent = 'x'.repeat(dataLength - baseStructuralLength);

return `${prefix}${dataContent}${suffix}`;
}

// Functions for non-ASCII payloads (e.g. emojis)
export function generateEmojiPayload(sizeInBytes: number): { data: string } {
const baseSize = JSON.stringify({ data: '' }).length;
const contentLength = sizeInBytes - baseSize;

return { data: '👍'.repeat(contentLength) };
}
export function generateEmojiPayloadString(dataLength: number, truncate?: boolean): string {
const prefix = '{"data":"';
const suffix = truncate ? '...' : '"}';

const baseStructuralLength = suffix.length;
const dataContent = '👍'.repeat(dataLength - baseStructuralLength);

return `${prefix}${dataContent}${suffix}`;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import * as Sentry from '@sentry/node';
import { loggingTransport } from '@sentry-internal/node-integration-tests';

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
tracesSampleRate: 1.0,
transport: loggingTransport,
integrations: [Sentry.httpIntegration({ maxRequestBodySize: 'always' })],
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import * as Sentry from '@sentry/node';
import { loggingTransport } from '@sentry-internal/node-integration-tests';

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
tracesSampleRate: 1.0,
transport: loggingTransport,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import * as Sentry from '@sentry/node';
import { loggingTransport } from '@sentry-internal/node-integration-tests';

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
tracesSampleRate: 1.0,
transport: loggingTransport,
integrations: [Sentry.httpIntegration({ maxRequestBodySize: 'medium' })],
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import * as Sentry from '@sentry/node';
import { loggingTransport } from '@sentry-internal/node-integration-tests';

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
tracesSampleRate: 1.0,
transport: loggingTransport,
integrations: [Sentry.httpIntegration({ maxRequestBodySize: 'none' })],
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import * as Sentry from '@sentry/node';
import { loggingTransport } from '@sentry-internal/node-integration-tests';

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
tracesSampleRate: 1.0,
transport: loggingTransport,
integrations: [Sentry.httpIntegration({ maxRequestBodySize: 'small' })],
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import * as Sentry from '@sentry/node';
import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests';
import bodyParser from 'body-parser';
import express from 'express';

const app = express();

// Increase limit for JSON parsing
app.use(bodyParser.json({ limit: '3mb' }));
app.use(express.json({ limit: '3mb' }));

app.post('/test-body-size', (req, res) => {
const receivedSize = JSON.stringify(req.body).length;
res.json({
success: true,
receivedSize,
message: 'Payload processed successfully',
});
});

Sentry.setupExpressErrorHandler(app);

startExpressServerAndSendPortToRunner(app);
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
import { afterAll, describe, expect } from 'vitest';
import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../../utils/runner';
import {
generateEmojiPayload,
generateEmojiPayloadString,
generatePayload,
generatePayloadString,
} from './generatePayload';

// Value of MAX_BODY_BYTE_LENGTH in SentryHttpIntegration
const MAX_GENERAL = 1024 * 1024; // 1MB
const MAX_MEDIUM = 10_000;
const MAX_SMALL = 1000;

describe('express with httpIntegration and not defined maxRequestBodySize', () => {
afterAll(() => {
cleanupChildProcesses();
});

createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument-default.mjs', (createRunner, test) => {
test('captures medium request bodies with default setting (medium)', async () => {
const runner = createRunner()
.expect({
transaction: {
transaction: 'POST /test-body-size',
request: {
data: JSON.stringify(generatePayload(MAX_MEDIUM)),
},
},
})
.start();

await runner.makeRequest('post', '/test-body-size', {
headers: { 'Content-Type': 'application/json' },
data: JSON.stringify(generatePayload(MAX_MEDIUM)),
});

await runner.completed();
});

test('truncates large request bodies with default setting (medium)', async () => {
const runner = createRunner()
.expect({
transaction: {
transaction: 'POST /test-body-size',
request: {
data: generatePayloadString(MAX_MEDIUM, true),
},
},
})
.start();

await runner.makeRequest('post', '/test-body-size', {
headers: { 'Content-Type': 'application/json' },
data: JSON.stringify(generatePayload(MAX_MEDIUM + 1)),
});

await runner.completed();
});
});
});

describe('express with httpIntegration and maxRequestBodySize: "none"', () => {
afterAll(() => {
cleanupChildProcesses();
});

createEsmAndCjsTests(
__dirname,
'scenario.mjs',
'instrument-none.mjs',
(createRunner, test) => {
test('does not capture any request bodies with none setting', async () => {
const runner = createRunner()
.expect({
transaction: {
transaction: 'POST /test-body-size',
request: expect.not.objectContaining({
data: expect.any(String),
}),
},
})
.start();

await runner.makeRequest('post', '/test-body-size', {
headers: { 'Content-Type': 'application/json' },
data: JSON.stringify(generatePayload(500)),
});

await runner.completed();
});
},
{ failsOnEsm: false },
);
});

describe('express with httpIntegration and maxRequestBodySize: "always"', () => {
afterAll(() => {
cleanupChildProcesses();
});

createEsmAndCjsTests(
__dirname,
'scenario.mjs',
'instrument-always.mjs',
(createRunner, test) => {
test('captures maximum allowed request body length with "always" setting', async () => {
const runner = createRunner()
.expect({
transaction: {
transaction: 'POST /test-body-size',
request: {
data: JSON.stringify(generatePayload(MAX_GENERAL)),
},
},
})
.start();

await runner.makeRequest('post', '/test-body-size', {
headers: { 'Content-Type': 'application/json' },
data: JSON.stringify(generatePayload(MAX_GENERAL)),
});

await runner.completed();
});

test('captures large request bodies with "always" setting but respects maximum size limit', async () => {
const runner = createRunner()
.expect({
transaction: {
transaction: 'POST /test-body-size',
request: {
data: generatePayloadString(MAX_GENERAL, true),
},
},
})
.start();

await runner.makeRequest('post', '/test-body-size', {
headers: { 'Content-Type': 'application/json' },
data: JSON.stringify(generatePayload(MAX_GENERAL + 1)),
});

await runner.completed();
});
},
{ failsOnEsm: false },
);
});

describe('express with httpIntegration and maxRequestBodySize: "small"', () => {
afterAll(() => {
cleanupChildProcesses();
});

createEsmAndCjsTests(
__dirname,
'scenario.mjs',
'instrument-small.mjs',
(createRunner, test) => {
test('keeps small request bodies with "small" setting', async () => {
const runner = createRunner()
.expect({
transaction: {
transaction: 'POST /test-body-size',
request: {
data: JSON.stringify(generatePayload(MAX_SMALL)),
},
},
})
.start();

await runner.makeRequest('post', '/test-body-size', {
headers: { 'Content-Type': 'application/json' },
data: JSON.stringify(generatePayload(MAX_SMALL)),
});

await runner.completed();
});

test('truncates too large request bodies with "small" setting', async () => {
const runner = createRunner()
.expect({
transaction: {
transaction: 'POST /test-body-size',
request: {
data: generatePayloadString(MAX_SMALL, true),
},
},
})
.start();

await runner.makeRequest('post', '/test-body-size', {
headers: { 'Content-Type': 'application/json' },
data: JSON.stringify(generatePayload(MAX_SMALL + 1)),
});

await runner.completed();
});

test('truncates too large non-ASCII request bodies with "small" setting', async () => {
const runner = createRunner()
.expect({
transaction: {
transaction: 'POST /test-body-size',
request: {
// 250 emojis, each 4 bytes in UTF-8 (resulting in 1000 bytes --> MAX_SMALL)
data: generateEmojiPayloadString(250, true),
},
},
})
.start();

await runner.makeRequest('post', '/test-body-size', {
headers: { 'Content-Type': 'application/json' },
data: JSON.stringify(generateEmojiPayload(MAX_SMALL + 1)),
});

await runner.completed();
});
},
{ failsOnEsm: false },
);
});

describe('express with httpIntegration and maxRequestBodySize: "medium"', () => {
afterAll(() => {
cleanupChildProcesses();
});

createEsmAndCjsTests(
__dirname,
'scenario.mjs',
'instrument-medium.mjs',
(createRunner, test) => {
test('keeps medium request bodies with "medium" setting', async () => {
const runner = createRunner()
.expect({
transaction: {
transaction: 'POST /test-body-size',
request: {
data: JSON.stringify(generatePayload(MAX_MEDIUM)),
},
},
})
.start();

await runner.makeRequest('post', '/test-body-size', {
headers: { 'Content-Type': 'application/json' },
data: JSON.stringify(generatePayload(MAX_MEDIUM)),
});

await runner.completed();
});

test('truncates large request bodies with "medium" setting', async () => {
const runner = createRunner()
.expect({
transaction: {
transaction: 'POST /test-body-size',
request: {
data: generatePayloadString(MAX_MEDIUM, true),
},
},
})
.start();

await runner.makeRequest('post', '/test-body-size', {
headers: { 'Content-Type': 'application/json' },
data: JSON.stringify(generatePayload(MAX_MEDIUM + 1)),
});

await runner.completed();
});
},
{ failsOnEsm: false },
);
});
Loading
Loading