@@ -21,6 +21,62 @@ import {
2121export type StreamId = string ;
2222export type EventId = string ;
2323
24+ const DEFAULT_MAX_BODY_BYTES = 1_000_000 ; // 1MB
25+
26+ class PayloadTooLargeError extends Error {
27+ constructor ( ) {
28+ super ( 'payload_too_large' ) ;
29+ this . name = 'PayloadTooLargeError' ;
30+ }
31+ }
32+
33+ async function readRequestTextWithLimit ( req : Request , maxBytes : number ) : Promise < string > {
34+ const body = req . body ;
35+ if ( ! body ) return '' ;
36+
37+ if ( Number . isFinite ( maxBytes ) ) {
38+ const clRaw = req . headers . get ( 'content-length' ) ?? '' ;
39+ const cl = Number ( clRaw ) ;
40+ if ( Number . isFinite ( cl ) && cl > maxBytes ) {
41+ throw new PayloadTooLargeError ( ) ;
42+ }
43+ }
44+
45+ const reader = body . getReader ( ) ;
46+ const chunks : Uint8Array [ ] = [ ] ;
47+ let total = 0 ;
48+
49+ try {
50+ while ( true ) {
51+ const { value, done } = await reader . read ( ) ;
52+ if ( done ) break ;
53+ if ( ! value ) continue ;
54+
55+ total += value . byteLength ;
56+ if ( Number . isFinite ( maxBytes ) && total > maxBytes ) {
57+ void reader . cancel ( ) . catch ( ( ) => { } ) ;
58+ throw new PayloadTooLargeError ( ) ;
59+ }
60+ chunks . push ( value ) ;
61+ }
62+ } finally {
63+ try {
64+ reader . releaseLock ( ) ;
65+ } catch {
66+ // Ignore.
67+ }
68+ }
69+
70+ const out = new Uint8Array ( total ) ;
71+ let offset = 0 ;
72+ for ( const c of chunks ) {
73+ out . set ( c , offset ) ;
74+ offset += c . byteLength ;
75+ }
76+
77+ return new TextDecoder ( ) . decode ( out ) ;
78+ }
79+
2480/**
2581 * Interface for resumability support via event storage
2682 */
@@ -152,6 +208,19 @@ export interface WebStandardStreamableHTTPServerTransportOptions {
152208 * @default SUPPORTED_PROTOCOL_VERSIONS
153209 */
154210 supportedProtocolVersions ?: string [ ] ;
211+
212+ /**
213+ * Maximum JSON request body size in bytes.
214+ * Used when parsing request bodies to guard against unbounded buffering.
215+ *
216+ * Set to a negative number to disable the limit.
217+ *
218+ * Note: if you pass `parsedBody` to `handleRequest`, this limit is not applied
219+ * (your framework/body parser must enforce its own limit).
220+ *
221+ * @default 1_000_000 (1 MB)
222+ */
223+ maxBodyBytes ?: number ;
155224}
156225
157226/**
@@ -231,6 +300,7 @@ export class WebStandardStreamableHTTPServerTransport implements Transport {
231300 private _enableDnsRebindingProtection : boolean ;
232301 private _retryInterval ?: number ;
233302 private _supportedProtocolVersions : string [ ] ;
303+ private _maxBodyBytes : number ;
234304
235305 sessionId ?: string ;
236306 onclose ?: ( ) => void ;
@@ -248,6 +318,7 @@ export class WebStandardStreamableHTTPServerTransport implements Transport {
248318 this . _enableDnsRebindingProtection = options . enableDnsRebindingProtection ?? false ;
249319 this . _retryInterval = options . retryInterval ;
250320 this . _supportedProtocolVersions = options . supportedProtocolVersions ?? SUPPORTED_PROTOCOL_VERSIONS ;
321+ this . _maxBodyBytes = options . maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES ;
251322 }
252323
253324 /**
@@ -625,8 +696,18 @@ export class WebStandardStreamableHTTPServerTransport implements Transport {
625696
626697 let rawMessage ;
627698 if ( options ?. parsedBody === undefined ) {
699+ const effectiveMaxBodyBytes = this . _maxBodyBytes < 0 ? Number . POSITIVE_INFINITY : this . _maxBodyBytes ;
700+ let text : string ;
701+ try {
702+ text = await readRequestTextWithLimit ( req , effectiveMaxBodyBytes ) ;
703+ } catch ( error ) {
704+ if ( error instanceof PayloadTooLargeError ) {
705+ return this . createJsonErrorResponse ( 413 , - 32_000 , 'Payload too large' ) ;
706+ }
707+ return this . createJsonErrorResponse ( 400 , - 32_700 , 'Parse error: Invalid JSON' ) ;
708+ }
628709 try {
629- rawMessage = await req . json ( ) ;
710+ rawMessage = JSON . parse ( text ) ;
630711 } catch {
631712 return this . createJsonErrorResponse ( 400 , - 32_700 , 'Parse error: Invalid JSON' ) ;
632713 }
0 commit comments