@@ -21,6 +21,18 @@ import {
2121export type StreamId = string ;
2222export type EventId = string ;
2323
24+ const DEFAULT_MAX_BODY_BYTES = 1_000_000 ;
25+
26+ class PayloadTooLargeError extends Error {
27+ readonly maxBodyBytes : number ;
28+
29+ constructor ( maxBodyBytes : number ) {
30+ super ( 'Payload too large' ) ;
31+ this . name = 'PayloadTooLargeError' ;
32+ this . maxBodyBytes = maxBodyBytes ;
33+ }
34+ }
35+
2436/**
2537 * Interface for resumability support via event storage
2638 */
@@ -107,6 +119,16 @@ export interface WebStandardStreamableHTTPServerTransportOptions {
107119 */
108120 enableJsonResponse ?: boolean ;
109121
122+ /**
123+ * Maximum size in bytes that this transport will read when parsing an `application/json` request body.
124+ * This is a basic DoS guard for servers that call `transport.handleRequest(req)` without an upstream body-size limit.
125+ *
126+ * Set to `0` (or any non-finite value like `Infinity`) to disable the limit (not recommended).
127+ *
128+ * @default 1_000_000
129+ */
130+ maxBodyBytes ?: number ;
131+
110132 /**
111133 * Event store for resumability support
112134 * If provided, resumability will be enabled, allowing clients to reconnect and resume messages
@@ -222,6 +244,7 @@ export class WebStandardStreamableHTTPServerTransport implements Transport {
222244 private _requestResponseMap : Map < RequestId , JSONRPCMessage > = new Map ( ) ;
223245 private _initialized : boolean = false ;
224246 private _enableJsonResponse : boolean = false ;
247+ private _maxBodyBytes ?: number ;
225248 private _standaloneSseStreamId : string = '_GET_stream' ;
226249 private _eventStore ?: EventStore ;
227250 private _onsessioninitialized ?: ( sessionId : string ) => void | Promise < void > ;
@@ -240,6 +263,8 @@ export class WebStandardStreamableHTTPServerTransport implements Transport {
240263 constructor ( options : WebStandardStreamableHTTPServerTransportOptions = { } ) {
241264 this . sessionIdGenerator = options . sessionIdGenerator ;
242265 this . _enableJsonResponse = options . enableJsonResponse ?? false ;
266+ const maxBodyBytes = options . maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES ;
267+ this . _maxBodyBytes = Number . isFinite ( maxBodyBytes ) && maxBodyBytes > 0 ? maxBodyBytes : undefined ;
243268 this . _eventStore = options . eventStore ;
244269 this . _onsessioninitialized = options . onsessioninitialized ;
245270 this . _onsessionclosed = options . onsessionclosed ;
@@ -298,6 +323,64 @@ export class WebStandardStreamableHTTPServerTransport implements Transport {
298323 ) ;
299324 }
300325
326+ private async parseJsonRequestBody ( req : Request ) : Promise < unknown > {
327+ if ( this . _maxBodyBytes === undefined ) {
328+ return req . json ( ) ;
329+ }
330+
331+ const maxBodyBytes = this . _maxBodyBytes ;
332+
333+ // Quick reject when content-length is present and exceeds the limit.
334+ const contentLengthHeader = req . headers . get ( 'content-length' ) ;
335+ if ( contentLengthHeader ) {
336+ const contentLength = Number ( contentLengthHeader ) ;
337+ if ( Number . isFinite ( contentLength ) && contentLength > maxBodyBytes ) {
338+ throw new PayloadTooLargeError ( maxBodyBytes ) ;
339+ }
340+ }
341+
342+ const reader = req . body ?. getReader ( ) ;
343+ if ( ! reader ) {
344+ // Fall back to the platform JSON parsing if the body stream is unavailable.
345+ return req . json ( ) ;
346+ }
347+
348+ const chunks : Uint8Array [ ] = [ ] ;
349+ let totalBytes = 0 ;
350+
351+ while ( true ) {
352+ const { done, value } = await reader . read ( ) ;
353+ if ( done ) {
354+ break ;
355+ }
356+ if ( ! value ) {
357+ continue ;
358+ }
359+
360+ totalBytes += value . byteLength ;
361+ if ( totalBytes > maxBodyBytes ) {
362+ try {
363+ await reader . cancel ( ) ;
364+ } catch {
365+ // Best-effort.
366+ }
367+ throw new PayloadTooLargeError ( maxBodyBytes ) ;
368+ }
369+
370+ chunks . push ( value ) ;
371+ }
372+
373+ const bodyBytes = new Uint8Array ( totalBytes ) ;
374+ let offset = 0 ;
375+ for ( const chunk of chunks ) {
376+ bodyBytes . set ( chunk , offset ) ;
377+ offset += chunk . byteLength ;
378+ }
379+
380+ const bodyText = new TextDecoder ( ) . decode ( bodyBytes ) ;
381+ return JSON . parse ( bodyText ) as unknown ;
382+ }
383+
301384 /**
302385 * Validates request headers for DNS rebinding protection.
303386 * @returns Error response if validation fails, undefined if validation passes.
@@ -626,8 +709,11 @@ export class WebStandardStreamableHTTPServerTransport implements Transport {
626709 let rawMessage ;
627710 if ( options ?. parsedBody === undefined ) {
628711 try {
629- rawMessage = await req . json ( ) ;
630- } catch {
712+ rawMessage = await this . parseJsonRequestBody ( req ) ;
713+ } catch ( error ) {
714+ if ( error instanceof PayloadTooLargeError ) {
715+ return this . createJsonErrorResponse ( 413 , - 32_000 , 'Payload too large' ) ;
716+ }
631717 return this . createJsonErrorResponse ( 400 , - 32_700 , 'Parse error: Invalid JSON' ) ;
632718 }
633719 } else {
0 commit comments