Skip to content

Commit ee27bac

Browse files
RenKoya1claude
andcommitted
fix(client): lower default SSE event size limit to 4 MiB
Realistic A2A events (Message/Task JSON) are KB-scale and large files should be referenced via FileWithUri parts rather than inlined, so 4 MiB (matching gRPC's default max message size) leaves ample headroom. Also make the error actionable: point at maxEventSizeBytes and FileWithUri. Addresses review feedback from JakubWorek on #582. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 6941823 commit ee27bac

1 file changed

Lines changed: 18 additions & 12 deletions

File tree

src/sse_utils.ts

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,15 @@ export function formatSSEErrorEvent(error: unknown): string {
7171
* Without this cap a malicious or broken server can stream bytes that never
7272
* form a complete line — or `data:` lines whose blank-line terminator never
7373
* arrives — growing an in-memory buffer without limit until the client
74-
* process exhausts memory (CWE-400, uncontrolled resource consumption). The
75-
* default is deliberately generous so that legitimate large JSON events
76-
* (e.g. base64 file parts) still pass; callers handling larger payloads can
77-
* raise it via the `maxEventSizeBytes` argument.
74+
* process exhausts memory (CWE-400, uncontrolled resource consumption).
75+
*
76+
* Realistic A2A events (Message/Task JSON) are KB-scale, and large files
77+
* should be referenced via `FileWithUri` parts rather than inlined, so the
78+
* 4 MiB default (matching gRPC's default max message size) leaves ample
79+
* headroom. Callers that must inline larger payloads can raise it via the
80+
* `maxEventSizeBytes` argument.
7881
*/
79-
export const DEFAULT_MAX_SSE_EVENT_SIZE_BYTES = 20 * 1024 * 1024; // 20 MiB
82+
export const DEFAULT_MAX_SSE_EVENT_SIZE_BYTES = 4 * 1024 * 1024; // 4 MiB
8083

8184
/**
8285
* Parses an SSE stream from a `Response`, yielding events as they arrive.
@@ -107,9 +110,7 @@ export async function* parseSseStream(
107110

108111
while ((lineEndIndex = buffer.indexOf('\n')) >= 0) {
109112
if (lineEndIndex > maxEventSizeBytes) {
110-
throw new Error(
111-
`SSE line exceeded the maximum allowed size of ${maxEventSizeBytes} bytes.`
112-
);
113+
throw sseSizeError('SSE line', maxEventSizeBytes);
113114
}
114115
// Per the SSE spec lines may end with `\r\n`, `\r`, or `\n`. We
115116
// strip a trailing `\r` explicitly rather than calling `.trim()`,
@@ -137,17 +138,15 @@ export async function* parseSseStream(
137138
const fieldValue = stripOptionalLeadingSpace(line.substring('data:'.length));
138139
eventData = eventData === '' ? fieldValue : `${eventData}\n${fieldValue}`;
139140
if (eventData.length > maxEventSizeBytes) {
140-
throw new Error(
141-
`SSE event data exceeded the maximum allowed size of ${maxEventSizeBytes} bytes.`
142-
);
141+
throw sseSizeError('SSE event data', maxEventSizeBytes);
143142
}
144143
}
145144
}
146145

147146
// Same cap for a line that never terminates: the loop above never runs,
148147
// so the residual buffer would otherwise grow without bound.
149148
if (buffer.length > maxEventSizeBytes) {
150-
throw new Error(`SSE line exceeded the maximum allowed size of ${maxEventSizeBytes} bytes.`);
149+
throw sseSizeError('SSE line', maxEventSizeBytes);
151150
}
152151
}
153152

@@ -157,6 +156,13 @@ export async function* parseSseStream(
157156
}
158157
}
159158

159+
function sseSizeError(what: string, maxEventSizeBytes: number): Error {
160+
return new Error(
161+
`${what} exceeded the maximum allowed size of ${maxEventSizeBytes} bytes. ` +
162+
`Pass maxEventSizeBytes to raise the limit, or prefer FileWithUri parts for large payloads.`
163+
);
164+
}
165+
160166
/**
161167
* Per the SSE spec, the optional single leading space after the field-name
162168
* colon is consumed by the parser; embedded and trailing whitespace are

0 commit comments

Comments
 (0)