diff --git a/src/server/webStandardStreamableHttp.ts b/src/server/webStandardStreamableHttp.ts index 1f528427c8..c37e0854fd 100644 --- a/src/server/webStandardStreamableHttp.ts +++ b/src/server/webStandardStreamableHttp.ts @@ -621,7 +621,13 @@ export class WebStandardStreamableHTTPServerTransport implements Transport { }; let rawMessage; - if (options?.parsedBody !== undefined) { + // Treat both `undefined` and `null` as "no pre-parsed body". Some + // body parsers (notably the synthetic req/res objects produced by + // serverless-express on AWS Lambda Function URLs) set `req.body` + // to `null` after draining the request stream. Without this guard + // the `null` would be passed straight to JSONRPCMessageSchema.parse + // and fail with an opaque Zod error. See issue #1417. + if (options?.parsedBody !== undefined && options.parsedBody !== null) { rawMessage = options.parsedBody; } else { try { diff --git a/test/server/streamableHttp.test.ts b/test/server/streamableHttp.test.ts index 4a4f7d8248..3862fdf282 100644 --- a/test/server/streamableHttp.test.ts +++ b/test/server/streamableHttp.test.ts @@ -1292,6 +1292,46 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { }); }); + // Regression test for #1417: serverless-express on AWS Lambda Function URLs + // drains the request stream and sets req.body to `null` rather than leaving + // it undefined. The transport should treat this the same as "no pre-parsed + // body" and fall through to reading the body itself, not pass `null` to the + // JSON-RPC parser. + describe('StreamableHTTPServerTransport with parsedBody = null (Lambda regression #1417)', () => { + let server: Server; + let transport: StreamableHTTPServerTransport; + let baseUrl: URL; + + beforeEach(async () => { + const result = await createTestServer({ + customRequestHandler: async (req, res) => { + // Mimic serverless-express: pass `null` explicitly as parsedBody. + await transport.handleRequest(req, res, null); + }, + sessionIdGenerator: () => randomUUID() + }); + + server = result.server; + transport = result.transport; + baseUrl = result.baseUrl; + }); + + afterEach(async () => { + await stopTestServer({ server, transport }); + }); + + it('parses the request body when parsedBody is null', async () => { + const response = await sendPostRequest(baseUrl, TEST_MESSAGES.initialize); + + expect(response.status).toBe(200); + expect(response.headers.get('mcp-session-id')).toBeTruthy(); + + const text = await readSSEEvent(response); + expect(text).toContain('"id":"init-1"'); + expect(text).toContain('"protocolVersion"'); + }); + }); + // Test resumability support describe('StreamableHTTPServerTransport with resumability', () => { let server: Server;