Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
8 changes: 7 additions & 1 deletion src/server/webStandardStreamableHttp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
40 changes: 40 additions & 0 deletions test/server/streamableHttp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading