Skip to content

Commit bd692d0

Browse files
committed
fix(server): allow application/json-only Accept in JSON response mode (closes #1944)
`StreamableHTTPServerTransport` rejected `Accept: application/json` with 406 even when the transport was built with `enableJsonResponse: true` (i.e. pure request/response, no SSE). Branch the Accept check on `_enableJsonResponse`: in JSON-only mode, require `application/json`; in streaming mode, continue to require both `application/json` and `text/event-stream` (on-wire spec remains unchanged for that path). GET (SSE subscription) is unaffected. Two regression tests added in the JSON Response Mode suite: 1. POST with `Accept: application/json` is now 200. 2. POST with an incompatible Accept (e.g. `text/plain`) is still 406. Closes #1944.
1 parent 48251fe commit bd692d0

2 files changed

Lines changed: 38 additions & 3 deletions

File tree

packages/server/src/server/streamableHttp.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -617,10 +617,21 @@ export class WebStandardStreamableHTTPServerTransport implements Transport {
617617
*/
618618
private async handlePostRequest(req: Request, options?: HandleRequestOptions): Promise<Response> {
619619
try {
620-
// Validate the Accept header
620+
// Validate the Accept header.
621+
//
622+
// When the transport is in pure request/response mode (\`enableJsonResponse: true\`)
623+
// we never open an SSE stream, so we only require \`application/json\`. In streaming
624+
// mode the spec requires the client to accept both content types so the server can
625+
// upgrade to SSE.
621626
const acceptHeader = req.headers.get('accept');
622-
// The client MUST include an Accept header, listing both application/json and text/event-stream as supported content types.
623-
if (!acceptHeader?.includes('application/json') || !acceptHeader.includes('text/event-stream')) {
627+
const acceptsJson = acceptHeader?.includes('application/json') ?? false;
628+
const acceptsEventStream = acceptHeader?.includes('text/event-stream') ?? false;
629+
if (this._enableJsonResponse) {
630+
if (!acceptsJson) {
631+
this.onerror?.(new Error('Not Acceptable: Client must accept application/json'));
632+
return this.createJsonErrorResponse(406, -32_000, 'Not Acceptable: Client must accept application/json');
633+
}
634+
} else if (!acceptsJson || !acceptsEventStream) {
624635
this.onerror?.(new Error('Not Acceptable: Client must accept both application/json and text/event-stream'));
625636
return this.createJsonErrorResponse(
626637
406,

packages/server/test/server/streamableHttp.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -559,6 +559,30 @@ describe('Zod v4', () => {
559559
id: 'call-1'
560560
});
561561
});
562+
563+
it('should accept application/json-only Accept header in JSON response mode (regression for #1944)', async () => {
564+
// The strict Accept check used to require text/event-stream even when
565+
// enableJsonResponse: true (i.e. the transport will never open an SSE stream).
566+
// After the fix, application/json alone is sufficient.
567+
sessionId = await initializeServer();
568+
const request = createRequest('POST', TEST_MESSAGES.toolsList, {
569+
sessionId,
570+
accept: 'application/json'
571+
});
572+
const response = await transport.handleRequest(request);
573+
expect(response.status).toBe(200);
574+
expect(response.headers.get('content-type')).toBe('application/json');
575+
});
576+
577+
it('should still reject Accept that does not include application/json (JSON response mode)', async () => {
578+
sessionId = await initializeServer();
579+
const request = createRequest('POST', TEST_MESSAGES.toolsList, {
580+
sessionId,
581+
accept: 'text/plain'
582+
});
583+
const response = await transport.handleRequest(request);
584+
expect(response.status).toBe(406);
585+
});
562586
});
563587

564588
describe('HTTPServerTransport - Session Callbacks', () => {

0 commit comments

Comments
 (0)