From 65d9855475cf116d03a06b1f2eeb04390f938c7a Mon Sep 17 00:00:00 2001 From: luvs01 Date: Fri, 7 Aug 2026 20:14:13 +0900 Subject: [PATCH] fix(grok): keep slow uploads outside toggle flight --- .../management/native-integration-routes.ts | 31 ++++++++++--------- tests/native-grok-toggle.test.ts | 25 +++++++++++++++ 2 files changed, 41 insertions(+), 15 deletions(-) diff --git a/src/server/management/native-integration-routes.ts b/src/server/management/native-integration-routes.ts index 72d0098ec..714a18098 100644 --- a/src/server/management/native-integration-routes.ts +++ b/src/server/management/native-integration-routes.ts @@ -295,28 +295,29 @@ async function handleCodexToggle(ctx: ManagementContext): Promise { async function handleGrokToggle(ctx: ManagementContext): Promise { const { req, config, deps } = ctx; + let body: { enabled?: unknown }; + try { + body = await readManagementJsonBody(req); + } catch (error) { + rethrowManagementBodyTooLarge(error); + return jsonResponse({ error: "invalid JSON body" }, 400); + } + if (typeof body.enabled !== "boolean") { + return jsonResponse({ error: "enabled must be a boolean" }, 400); + } + const enabled = body.enabled; + /* - * The guard stands BEFORE the first await, or two concurrent PUTs could both - * pass it while their bodies are still parsing — the race the guard exists - * to close. + * Parse the request before taking the mutation flight. A client that stalls + * its upload must not prevent a complete request from changing Grok config. + * The check and assignment remain synchronous, so parsed requests cannot + * race into the mutation section. */ if (grokToggleFlight) { return refusal(409, "grok", "config_busy", "Another Grok change is already in flight. Nothing was written — try again in a moment."); } grokToggleFlight = (async (): Promise => { - let body: { enabled?: unknown }; - try { - body = await readManagementJsonBody(req); - } catch (error) { - rethrowManagementBodyTooLarge(error); - return jsonResponse({ error: "invalid JSON body" }, 400); - } - if (typeof body.enabled !== "boolean") { - return jsonResponse({ error: "enabled must be a boolean" }, 400); - } - const enabled = body.enabled; - /* * The inspector runs BEFORE either delegate, in BOTH directions (012 §In * PUT it is the authoritative preflight): an ambiguous fence never reaches diff --git a/tests/native-grok-toggle.test.ts b/tests/native-grok-toggle.test.ts index 4b9326c1b..7b5c92dc0 100644 --- a/tests/native-grok-toggle.test.ts +++ b/tests/native-grok-toggle.test.ts @@ -453,6 +453,31 @@ test("a second concurrent PUT gets 409 config_busy and writes nothing", async () expect(third.status).toBe(200); }); +test("an incomplete request body does not hold the Grok mutation flight", async () => { + writeConfig("# user only\n"); + let finishUpload!: () => void; + const body = new ReadableStream({ + start(controller) { + controller.enqueue(new TextEncoder().encode('{"enabled":')); + finishUpload = () => controller.close(); + }, + }); + const stalled = dispatch(baseConfig(), "/api/native-integrations/grok", { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body, + }); + await new Promise(resolve => setTimeout(resolve, 20)); + + const complete = await put(baseConfig(), true); + expect(complete.status).toBe(200); + expect(complete.body.state).toBe("current"); + + finishUpload(); + const stalledResponse = await stalled; + expect(stalledResponse!.status).toBe(400); +}); + test("no journal row and no snapshot exist for this toggle", () => { // The module must not touch the file-client bookkeeping at all — a static // check that the imports were never widened (012 §OUT).