Skip to content

Commit 198e4f5

Browse files
committed
fix: treat parsedBody = null as no pre-parsed body (v1.x)
Some body parsers — notably the synthetic objects produced by serverless-express on AWS Lambda Function URLs — set req.body = null after draining the request stream. The existing guard only checked for undefined, so null fell through to JSONRPCMessageSchema.parse and broke every Lambda-hosted MCP server. Treat null and undefined alike and fall through to req.json(). Fixes #1417.
1 parent e7ee57c commit 198e4f5

2 files changed

Lines changed: 47 additions & 1 deletion

File tree

src/server/webStandardStreamableHttp.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -621,7 +621,13 @@ export class WebStandardStreamableHTTPServerTransport implements Transport {
621621
};
622622

623623
let rawMessage;
624-
if (options?.parsedBody !== undefined) {
624+
// Treat both `undefined` and `null` as "no pre-parsed body". Some
625+
// body parsers (notably the synthetic req/res objects produced by
626+
// serverless-express on AWS Lambda Function URLs) set `req.body`
627+
// to `null` after draining the request stream. Without this guard
628+
// the `null` would be passed straight to JSONRPCMessageSchema.parse
629+
// and fail with an opaque Zod error. See issue #1417.
630+
if (options?.parsedBody !== undefined && options.parsedBody !== null) {
625631
rawMessage = options.parsedBody;
626632
} else {
627633
try {

test/server/streamableHttp.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1292,6 +1292,46 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => {
12921292
});
12931293
});
12941294

1295+
// Regression test for #1417: serverless-express on AWS Lambda Function URLs
1296+
// drains the request stream and sets req.body to `null` rather than leaving
1297+
// it undefined. The transport should treat this the same as "no pre-parsed
1298+
// body" and fall through to reading the body itself, not pass `null` to the
1299+
// JSON-RPC parser.
1300+
describe('StreamableHTTPServerTransport with parsedBody = null (Lambda regression #1417)', () => {
1301+
let server: Server;
1302+
let transport: StreamableHTTPServerTransport;
1303+
let baseUrl: URL;
1304+
1305+
beforeEach(async () => {
1306+
const result = await createTestServer({
1307+
customRequestHandler: async (req, res) => {
1308+
// Mimic serverless-express: pass `null` explicitly as parsedBody.
1309+
await transport.handleRequest(req, res, null);
1310+
},
1311+
sessionIdGenerator: () => randomUUID()
1312+
});
1313+
1314+
server = result.server;
1315+
transport = result.transport;
1316+
baseUrl = result.baseUrl;
1317+
});
1318+
1319+
afterEach(async () => {
1320+
await stopTestServer({ server, transport });
1321+
});
1322+
1323+
it('parses the request body when parsedBody is null', async () => {
1324+
const response = await sendPostRequest(baseUrl, TEST_MESSAGES.initialize);
1325+
1326+
expect(response.status).toBe(200);
1327+
expect(response.headers.get('mcp-session-id')).toBeTruthy();
1328+
1329+
const text = await readSSEEvent(response);
1330+
expect(text).toContain('"id":"init-1"');
1331+
expect(text).toContain('"protocolVersion"');
1332+
});
1333+
});
1334+
12951335
// Test resumability support
12961336
describe('StreamableHTTPServerTransport with resumability', () => {
12971337
let server: Server;

0 commit comments

Comments
 (0)