fix(core): send unchanged hooked requests without copying the body (#59097)

This commit is contained in:
Luke Parker
2026-10-26 09:34:34 +10:00
committed by GitHub
Unverified
parent 6f861e2623
commit d1cf7dd04a
2 changed files with 62 additions and 11 deletions
+14 -11
View File
@@ -321,17 +321,20 @@ export const layer = Layer.effect(
const http: StreamOptions["http"] = hasHttpHooks
? (req, handler) =>
Effect.gen(function* () {
const before = yield* hooks.trigger("session", "http.request", {
...scope,
request: yield* HttpClientRequest.toWeb(req),
})
let sent = HttpClientRequest.fromWeb(before.request)
if (before.request.body)
sent = HttpClientRequest.bodyUint8Array(
sent,
new Uint8Array(yield* Effect.promise(() => before.request.clone().arrayBuffer())),
before.request.headers.get("content-type") ?? undefined,
)
const request = yield* HttpClientRequest.toWeb(req)
const before = yield* hooks.trigger("session", "http.request", { ...scope, request })
// A hook that kept the request object cannot have replaced its body, so the original body is sent as is.
// Reading it back would hold a second full copy of the request, screenshots included, for the whole call.
const sent =
before.request === request
? HttpClientRequest.setBody(HttpClientRequest.fromWeb(request), req.body)
: before.request.body
? HttpClientRequest.bodyUint8Array(
HttpClientRequest.fromWeb(before.request),
new Uint8Array(yield* Effect.promise(() => before.request.clone().arrayBuffer())),
before.request.headers.get("content-type") ?? undefined,
)
: HttpClientRequest.fromWeb(before.request)
const res = yield* handler(sent)
const after = yield* hooks.trigger("session", "http.response", {
...scope,
@@ -81,6 +81,54 @@ describe("SessionModelRequest HTTP hooks", () => {
}).pipe(Effect.provideService(SessionModelTransport.Service, transport)),
)
it.effect("sends a kept request's original body and a replaced request's new body", () =>
Effect.gen(function* () {
const hooks = yield* PluginHooks.Service
let replace = false
yield* hooks.register("session", "http.request", (event) =>
Effect.sync(() => {
if (replace) {
event.request = new Request(event.request.url, { method: "POST", body: '{"replaced":true}' })
return
}
event.request.headers.set("x-plugin", "kept")
}),
)
const requests = yield* SessionModelRequest.Service.pipe(Effect.provide(SessionModelRequest.layer))
const prepared = yield* requests.primary({
session,
agent: Agent.ID.make("build"),
model,
system: [],
messages: [],
})
const http = prepared.options.http
if (!http) throw new Error("Expected HTTP middleware")
const original = HttpClientRequest.post("https://example.test/v1/chat/completions").pipe(
HttpClientRequest.bodyText('{"messages":[]}', "application/json"),
)
const send = () => {
const sent: HttpClientRequest.HttpClientRequest[] = []
return http(original, (request) =>
Effect.sync(() => {
sent.push(request)
return HttpClientResponse.fromWeb(request, new Response("{}", { status: 200 }))
}),
).pipe(Effect.map(() => sent[0]!))
}
const kept = yield* send()
expect(kept.body).toBe(original.body)
expect(kept.headers["x-plugin"]).toBe("kept")
replace = true
const replaced = yield* send()
expect(replaced.body._tag).toBe("Uint8Array")
if (replaced.body._tag !== "Uint8Array") return
expect(new TextDecoder().decode(replaced.body.body)).toBe('{"replaced":true}')
}).pipe(Effect.provideService(SessionModelTransport.Service, transport)),
)
it.effect("keeps hook edits to a tool schema out of later requests", () =>
Effect.gen(function* () {
const hooks = yield* PluginHooks.Service