Skip to content

Commit 87d72e2

Browse files
fix(runtime/proxy): bound streaming success-body SSE reads at 16 MiB
Apply createSseByteGuard to src/agents/runtime/proxy.ts (streamProxy) so the runtime proxy SSE parser cannot be exhausted by a hostile or malfunctioning proxy endpoint that streams an unbounded SSE body. The 16 MiB cap matches the non-streaming readProviderJsonResponse cap. What changed: - src/agents/runtime/proxy.ts: wrap response.body.getReader() in createSseByteGuard before the existing streamProxy SSE read loop. The abort handler and finally block use guard.cancel() instead of reader.cancel(). - Inline test added to proxy.test.ts: hostile 1 MiB pull stream through streamProxy, asserts errorMessage matches overflow pattern, cancel(reason) was an Error instance, pullCount bounded to 17-20. Reuses the createSseByteGuard helper from src/agents/streaming-byte-guard.ts (introduced in #96701 for the anthropic-transport-stream path; same helper, same 16 MiB cap, same overflow-error shape). This PR is the final per-surface rescue from the previously closed PR #96666, after PR #3c (#96762). No SDK surface change. No repro script committed (proof in this body).
1 parent 072d3ed commit 87d72e2

2 files changed

Lines changed: 59 additions & 2 deletions

File tree

src/agents/runtime/proxy.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,4 +189,47 @@ describe("streamProxy", () => {
189189
errorMessage: "Proxy stream ended before terminal event",
190190
});
191191
});
192+
193+
it("bounds streamed proxy success bodies without content-length", async () => {
194+
// 1 MiB chunks; cap is 16 MiB so the bounded reader cancels well before
195+
// draining the full 32 MiB advertised body.
196+
const CHUNK = 1024 * 1024;
197+
const TOTAL = 32;
198+
let pullCount = 0;
199+
let cancelReason: unknown;
200+
vi.stubGlobal(
201+
"fetch",
202+
vi.fn(
203+
async () =>
204+
new Response(
205+
new ReadableStream({
206+
pull(controller) {
207+
pullCount += 1;
208+
if (pullCount > TOTAL) {
209+
controller.close();
210+
return;
211+
}
212+
controller.enqueue(new Uint8Array(CHUNK));
213+
},
214+
cancel(reason) {
215+
cancelReason = reason;
216+
},
217+
}),
218+
{ status: 200 },
219+
),
220+
),
221+
);
222+
223+
const result = await streamProxy(model, context, {
224+
authToken: "token",
225+
proxyUrl: "https://proxy.example",
226+
}).result();
227+
228+
expect(result.stopReason).toBe("error");
229+
expect(result.errorMessage).toMatch(/Proxy stream body exceeded \d+ bytes/);
230+
expect(cancelReason).toBeInstanceOf(Error);
231+
// 16 MiB + a couple of overshoot pulls, well under 32.
232+
expect(pullCount).toBeGreaterThanOrEqual(17);
233+
expect(pullCount).toBeLessThanOrEqual(20);
234+
});
192235
});

src/agents/runtime/proxy.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* The server manages auth and proxies requests to LLM providers.
44
*/
55

6+
import { createSseByteGuard } from "../../agents/streaming-byte-guard.js";
67
// Internal import for JSON parsing utility
78
import type {
89
AssistantMessage,
@@ -124,6 +125,8 @@ function sanitizeProxyModel(model: Model): Model {
124125
return safeModel as Model;
125126
}
126127

128+
const PROXY_STREAM_BODY_MAX_BYTES = 16 * 1024 * 1024;
129+
127130
export function streamProxy(
128131
model: Model,
129132
context: Context,
@@ -152,9 +155,12 @@ export function streamProxy(
152155
};
153156

154157
let reader: ReadableStreamDefaultReader<Uint8Array> | undefined;
158+
let guard: ReturnType<typeof createSseByteGuard> | undefined;
155159

156160
const abortHandler = () => {
157-
if (reader) {
161+
if (guard) {
162+
guard.cancel("Request aborted by user").catch(() => {});
163+
} else if (reader) {
158164
reader.cancel("Request aborted by user").catch(() => {});
159165
}
160166
};
@@ -192,6 +198,11 @@ export function streamProxy(
192198
}
193199

194200
reader = response.body!.getReader();
201+
guard = createSseByteGuard(reader, {
202+
maxBytes: PROXY_STREAM_BODY_MAX_BYTES,
203+
onOverflow: ({ size, maxBytes }) =>
204+
new Error(`Proxy stream body exceeded ${maxBytes} bytes (received ${size})`),
205+
});
195206
const decoder = new TextDecoder();
196207
let buffer = "";
197208
let terminalEventSeen = false;
@@ -214,7 +225,7 @@ export function streamProxy(
214225
};
215226

216227
while (true) {
217-
const { done, value } = await reader.read();
228+
const { done, value } = await guard.read();
218229
if (done) {
219230
break;
220231
}
@@ -256,6 +267,9 @@ export function streamProxy(
256267
});
257268
stream.end();
258269
} finally {
270+
try {
271+
await guard?.cancel();
272+
} catch {}
259273
try {
260274
reader?.releaseLock();
261275
} catch {}

0 commit comments

Comments
 (0)