mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-08 00:02:20 +03:00
fix(sse): sanitize non-ok Antigravity streaming error body (port from 9router#2461) (#7106)
Root cause: the STREAMING branch of AntigravityExecutor.executeOnce() had no
!response.ok check at all — it unconditionally wrapped the upstream response
body in a pass-through TransformStream, unlike the sibling non-streaming
branch which already built a sanitized error via buildAntigravityUpstreamError.
When Google's 403 error body was binary/non-UTF8 (observed: gzip-magic-byte
payloads), those raw bytes were forwarded verbatim, corrupting the
client-visible error message ('[ERROR] [403]: <control-byte garbage>').
Fix: add the same !response.ok guard to the streaming branch, routing through
buildAntigravityUpstreamError()/buildErrorBody() (hard rule #12) instead of
piping unknown bytes through as if they were an SSE stream.
Reported-by: Duongkhanhtool (https://github.com/decolua/9router/issues/2461)
This commit is contained in:
committed by
GitHub
parent
7123236012
commit
bc6cd2a806
@@ -0,0 +1 @@
|
||||
- **fix(sse):** Antigravity streaming requests that hit a non-ok upstream response (e.g. a 403) no longer pipe the raw upstream bytes straight through to the client — a binary/non-UTF8 error body (observed as gzip-magic-byte garbage) is now routed through the same sanitized `buildAntigravityUpstreamError()` path the non-streaming branch already used, instead of corrupting the client-visible error message. Regression guard: `tests/unit/antigravity-streaming-error-body-sanitized-2461.test.ts` — thanks @Duongkhanhtool
|
||||
@@ -1591,6 +1591,34 @@ export class AntigravityExecutor extends BaseExecutor {
|
||||
};
|
||||
}
|
||||
|
||||
// #2461: a non-ok upstream response (e.g. 403) must never be piped through the
|
||||
// streaming pass-through below as if it were an SSE body. Google occasionally
|
||||
// returns non-UTF8/binary error bodies (observed: gzip-magic-byte payloads) for
|
||||
// 403s on this endpoint; reading/forwarding those raw bytes corrupts the
|
||||
// client-visible error message. Mirror the non-streaming branch above and build
|
||||
// a sanitized JSON error via buildAntigravityUpstreamError (hard rule #12)
|
||||
// instead of streaming unknown bytes straight through.
|
||||
if (!response.ok) {
|
||||
const rawBody = await response
|
||||
.clone()
|
||||
.text()
|
||||
.catch(() => "");
|
||||
const errorBody = buildAntigravityUpstreamError(
|
||||
response.status,
|
||||
response.statusText,
|
||||
rawBody
|
||||
);
|
||||
return {
|
||||
response: new Response(JSON.stringify(errorBody), {
|
||||
status: response.status,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}),
|
||||
url,
|
||||
headers: finalHeaders,
|
||||
transformedBody: attachToolNameMap(transformedBody, requestToolNameMap),
|
||||
};
|
||||
}
|
||||
|
||||
// Streaming path: wrap the response body in a pass-through TransformStream
|
||||
// that extracts remainingCredits from the final SSE chunk(s) without
|
||||
// consuming the stream. The client receives the unmodified SSE data.
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { AntigravityExecutor } from "../../open-sse/executors/antigravity.ts";
|
||||
import {
|
||||
clearAntigravityVersionCache,
|
||||
seedAntigravityVersionCache,
|
||||
} from "../../open-sse/services/antigravityVersion.ts";
|
||||
|
||||
// Ports decolua/9router#2461: a non-ok (e.g. 403) Antigravity upstream response in the
|
||||
// STREAMING path was piped straight through to the client via a raw pass-through
|
||||
// TransformStream, with no `response.ok` check at all — unlike the non-streaming path,
|
||||
// which already builds a sanitized error via buildAntigravityUpstreamError. When the
|
||||
// upstream 403 body is gzip-compressed (or otherwise binary/non-UTF8), those raw bytes
|
||||
// end up surfaced verbatim in the client-visible error message, corrupting it (reporters
|
||||
// saw literal control-byte garbage after "[ERROR] [403]:").
|
||||
test.afterEach(() => {
|
||||
clearAntigravityVersionCache();
|
||||
});
|
||||
|
||||
test("AntigravityExecutor.execute (stream=true) sanitizes a non-ok upstream body instead of piping raw bytes", async () => {
|
||||
const executor = new AntigravityExecutor();
|
||||
const originalFetch = globalThis.fetch;
|
||||
seedAntigravityVersionCache("2026.04.17-test");
|
||||
|
||||
// Simulate a gzip-compressed 403 body (magic bytes 0x1f 0x8b), the exact shape
|
||||
// reported upstream — reading it as text without decoding produces garbage.
|
||||
const binaryBody = new Uint8Array([0x1f, 0x8b, 0x08, 0x00, 0x02, 0xff, 0x52, 0x41, 0x4e]);
|
||||
|
||||
globalThis.fetch = async () =>
|
||||
new Response(binaryBody, {
|
||||
status: 403,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await executor.execute({
|
||||
model: "antigravity/gemini-2.5-flash",
|
||||
body: { request: { contents: [] } },
|
||||
stream: true,
|
||||
credentials: { accessToken: "token", projectId: "project-1" },
|
||||
log: { debug() {}, warn() {} },
|
||||
});
|
||||
|
||||
assert.equal(result.response.status, 403);
|
||||
|
||||
const bodyText = await result.response.text();
|
||||
|
||||
// The raw gzip magic bytes must never reach the client-visible error text.
|
||||
assert.ok(
|
||||
!bodyText.includes("\x1f\x8b"),
|
||||
`expected sanitized error body, got raw bytes leaking through: ${JSON.stringify(bodyText)}`
|
||||
);
|
||||
|
||||
// Must be routed through buildErrorBody()/buildAntigravityUpstreamError() — a clean,
|
||||
// parseable JSON error shape (hard rule #12), not an arbitrary pass-through stream.
|
||||
const parsed = JSON.parse(bodyText) as { error?: { message?: string } };
|
||||
assert.ok(parsed.error?.message, "expected a structured error.message");
|
||||
assert.match(parsed.error.message, /Antigravity upstream error \(403\)/);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user