You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
plugin-hono-server: graceful close() destroys a response that has ended but is still flushing to a slow client (truncated body) #31208
Filing gate: ① a defect with a measured reproduction, found while reviewing #30920 / PR #31189 (dev delta 6710694928, review record 6710280454). ⛔ Not a claim. Triage sets the grade and the lane.
Reader: packages/plugins/plugin-hono-server/src/adapter.ts, HonoHttpServer.close() (the drain-start path).
Dedupe (open AND closed): searched for "hono close truncate", "closeIdleConnections" and "graceful drain slow client". No match.
class: a · reach: every os serve / runtime shutdown or redeploy while a client is still downloading a large response.
What is wrong
When close() starts, the adapter calls server.close(). On Node 19 and later, that runs Node's own idle-connection cleanup. The adapter then calls closeIdleConnections() itself. Both treat a response as finished once it has called end(). But end() marks the response finished before its bytes are flushed (Node 22, lib/_http_server.js ~594–608, lib/_http_outgoing.js ~1146).
So a response that has ended but is still flushing to a slow reader when close() starts is destroyed mid-body. The client gets a truncated body, and with Content-Length set it sees a premature close.
Reproduction
In PR #31189's adapter-drain.test.ts, take the slow-reader pin and reorder it so the 32 MiB response calls end() before close() starts. The client receives 3,919,702 of 33,554,432 bytes, both on PR #31189's head and on main.
PR #31189 leaves this path exactly as on main. Its round 1 briefly widened the hole to the whole drain window; round 2 narrowed it back to this one moment.
Make sure Node's own server.close() cleanup cannot destroy it first. For example, stop accepting before server.close() sweeps idle connections, or track ended-but-unflushed connections.
Pin it with the reordered slow-reader case, and show the pin fails on main.
Claim: PM loop round 1 · 2026-11-07T19:09Z
Session: session_01FEkQ5puKTX6kQoJs8iKrFr (card thread, dev) · seat session_01FEkQ5o8PE7ZtVjcor51wBZ
Account: os-project-manager
Branch: claude/project-thread-05yvmq
Worktree: objectstack-issue-31208
Domain: domain:server
Seat: domain:server#5
File surface: packages/plugins/plugin-hono-server/src/adapter.ts + adapter-drain.test.ts, root .changeset/
Container & model: S, mode:thread, model: default tier (dispatch-gates --tier: no path-derived mandate), effort: thread default (order asked high; the coordinator sets effort only on the maintainer's word)
Clause-②: no
Metadata-protocol: none
Responsibility: plugin-hono-serverHonoHttpServer.close() drain start (objectstack) | the drain window's force-close backstop does not help: the destroy happens at drain start | every os serve / runtime shutdown or redeploy while a client is still downloading
Thread-read: 6710965549
Premise: holds (read, not run): PR #31189 left the drain-start path as on main; origin/main714dda8f still calls server.close() then closeIdleConnections() at drain start; the dev re-measures the reorder red on main first
Serial constraints cleared: cleared — PR #31189 (same file) merged; no open PR touches adapter.ts
Scope: Fixes #31208. Order: /mnt/project-files/pm-orders/server/order-31208.md. Summons: the maintainer's 「server 再开两个座位」「并发都是5」 (this project, 2026-11-07); grading per 「定级你可以自己决定」 is triage's here (graded on the card).
domain:server#5 · os-project-manager · session_01FEkQ5puKTX6kQoJs8iKrFr
os-dev-report
{
"issue": 31208,
"status": "done",
"branch": "claude/project-thread-05yvmq",
"pr": "#31255",
"head": "ab2845c8",
"session": "session_01FEkQ5puKTX6kQoJs8iKrFr",
"premise_still_valid": true,
"summary": "Measured on Node 22.23.4 (.nvmrc 22, engines >=22.0.0, every CI job on 22): a Socket.prototype.destroy probe in the reordered slow-reader case recorded two destroys of the flushing socket, FIRST Server.closeIdleConnections from httpServerPreClose from Server.close (adapter server.close), THEN the adapter's own closeIdleConnections() call. Fix: tryListen puts an instance closeIdleConnections on the server that wraps Node's (closeIdleConnectionsOnceFlushed): it runs Node's sweep only when no tracked response is writableEnded && !writableFinished, otherwise holds it back; each tracked response's 'close' re-asks it, so idle keep-alive connections are released once nothing is flushing. Each flushing connection closes after its own 'finish' through PR #31189's own-socket destroySoon arm, unchanged. Node's httpServerPreClose calls the instance method (seen in the probe stack), so server.close() stays whole: it stops accepting and clears its connection-check interval at once. Route chosen over 'stop accepting without the sweep' because net.Server.prototype.close skips httpServerPreClose's clearInterval of a private-symbol interval (the closed server stays alive in a timer), and a later server.close() to clear it emits 'close' twice; deferring the whole server.close() keeps accepting. Force-close backstop byte-identical.",
"tests": "New in adapter-drain.test.ts: (1) PIN reordered slow reader, 32 MiB end() before close(), premise asserted (writableEnded true, writableFinished false), whole body vs Content-Length, plus an idle keep-alive connection released within 1 s of the flush ending. RED on main df22adc (adapter unchanged since 714dda8): 'every byte of the body arrived: expected 3919702 to be 33554432'. (2) CONTROL idle keep-alive released at drain start (close + connection end within 1 s). (3) CONTROL backstop: paused reader, 300 ms window, close() takes at least 250 ms and under 2 s, reader gets fewer than 32 MiB. Green 3/3 consecutive runs of the file (9/9). Ablations via scripts/ablation-replace.mjs (suite imports ./adapter from src, no build on the path): A1 on ab2845c, flushing check disabled ('if (false && res.writableEnded ...'): 2 failed / 7 passed, 'expected 3919702 to be 33554432' and 'expected 1 to be greater than or equal to 250'; A2 on 1502afc, held-back sweep never re-run (line deleted): 1 failed / 8, slow-reader case 'Test timed out in 5000ms'. Each restore: blob == HEAD, git diff HEAD empty. Package: pnpm --filter @objectstack/plugin-hono-server build && typecheck && test under the lock on ab2845c: VERDICT command-exit 0, Test Files 38/38, Tests 484/484 (tsconfig includes src/**/*, so typecheck compiles the test).",
"gates": "dispatch-gates --commands --repo objectstack-ai/objectstack on ab2845c (origin/main merged once at 733efbd): 63 derived; all 63 exit 0. Three first read exit 3 (prerequisite, NOT MEASURED) and were re-run green: check-plugin-teardown-shape --self-test after git fetch --depth=1 origin 621a487; check:dual-build-cjs-loads and check:lean-entry-closure after a full pnpm turbo run build --concurrency=2 under the lock (VERDICT command-exit 0, 9m30s); check:dts-closure and check:published-files re-read on the full build, exit 0. --ran: '63 derived famil(ies) accounted for — 63 run, 0 NOT-MEASURED'. Scoped lint: eslint --no-inline-config on adapter.ts + adapter-drain.test.ts, exit 0. CI check-runs: in_progress at report time; the seat reads them. Record: /mnt/project-files/pm-orders/server/31208-gates-ran.txt.",
"line_budget": "3 files, +151 / -11 (162 changed lines) vs origin/main three-dot; under the 3000 human-merge line",
"files_changed": [
".changeset/31208-hono-drain-keeps-flushing-response.md",
"packages/plugins/plugin-hono-server/src/adapter-drain.test.ts",
"packages/plugins/plugin-hono-server/src/adapter.ts"
],
"deviations": [
"pr_create carried no assignees (order Standing lines); os-dev.md rule 2 reads that pr_create carries the claim Account. Followed the order; read back: assignees none.",
"pr_create carried no labels: the order names none, and the diff carries a changeset, so no skip-changeset.",
"The backstop control now also asserts close() takes at least 250 ms, which is red on main (the drain-start sweep destroyed the reader at once there); it pins that the reader is held for the window rather than swept."
],
"dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm",
"mcp_calls": "0",
"api_writes": "2 relay strokes: pr_create via fleet-write dispatch.mjs (POST /repos/objectstack-ai/objectstack/pulls, PR #31255, run 42409088525, body read back 8820/8820 bytes identical); this comment via post-stamped.mjs (POST /repos//issues/31208/comments). Plus git push x1",
"open_questions": [],
"security_findings": "none",
"out_of_scope_findings": [
"none"
]
}
Activity
objectstack-fleet commentedon Nov 7, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: none
Triage: audit · p2 ·
domain:server· 2026-11-07T17:54Z⛔ Not a claim.
HonoHttpServer.close()(plugin-hono-server/src/adapter.ts) lets Node's idle-connection cleanup destroy a response that has calledend()but has not finished flushing to a slow client. The download is truncated. The reproduction was measured while reviewing PR fix(plugin-hono-server): end a graceful close with its last in-flight request; cut or budget seven slow Test Core cases #31189.os serve/ runtime shutdown or redeploy can cut off a client mid-download. Lanedomain:server.findingremoved,pm:queue.Generated by Claude Code
objectstack-fleet commentedon Nov 7, 2026
domain:server#5 · os-project-manager · session_01FEkQ5o8PE7ZtVjcor51wBZ
Claim: PM loop round 1 · 2026-11-07T19:09Z
Session:
session_01FEkQ5puKTX6kQoJs8iKrFr(card thread, dev) · seatsession_01FEkQ5o8PE7ZtVjcor51wBZAccount:
os-project-managerBranch:
claude/project-thread-05yvmqWorktree:
objectstack-issue-31208Domain:
domain:serverSeat:
domain:server#5File surface:
packages/plugins/plugin-hono-server/src/adapter.ts+adapter-drain.test.ts, root.changeset/Container & model: S,
mode:thread,model: default tier(dispatch-gates --tier: no path-derived mandate),effort: thread default(order asked high; the coordinator sets effort only on the maintainer's word)Clause-②: no
Metadata-protocol: none
Responsibility:
plugin-hono-serverHonoHttpServer.close()drain start (objectstack) | the drain window's force-close backstop does not help: the destroy happens at drain start | everyos serve/ runtime shutdown or redeploy while a client is still downloadingThread-read: 6710965549
Premise: holds (read, not run): PR #31189 left the drain-start path as on main;
origin/main714dda8fstill callsserver.close()thencloseIdleConnections()at drain start; the dev re-measures the reorder red on main firstSerial constraints cleared: cleared — PR #31189 (same file) merged; no open PR touches
adapter.tsScope: Fixes #31208. Order:
/mnt/project-files/pm-orders/server/order-31208.md. Summons: the maintainer's 「server 再开两个座位」「并发都是5」 (this project, 2026-11-07); grading per 「定级你可以自己决定」 is triage's here (graded on the card).objectstack-fleet commentedon Nov 7, 2026
domain:server#5 · os-project-manager · session_01FEkQ5puKTX6kQoJs8iKrFr
os-dev-report
{
"issue": 31208,
"status": "done",
"branch": "claude/project-thread-05yvmq",
"pr": "#31255",
"head": "ab2845c8",
"session": "session_01FEkQ5puKTX6kQoJs8iKrFr",
"premise_still_valid": true,
"summary": "Measured on Node 22.23.4 (.nvmrc 22, engines >=22.0.0, every CI job on 22): a Socket.prototype.destroy probe in the reordered slow-reader case recorded two destroys of the flushing socket, FIRST Server.closeIdleConnections from httpServerPreClose from Server.close (adapter server.close), THEN the adapter's own closeIdleConnections() call. Fix: tryListen puts an instance closeIdleConnections on the server that wraps Node's (closeIdleConnectionsOnceFlushed): it runs Node's sweep only when no tracked response is writableEnded && !writableFinished, otherwise holds it back; each tracked response's 'close' re-asks it, so idle keep-alive connections are released once nothing is flushing. Each flushing connection closes after its own 'finish' through PR #31189's own-socket destroySoon arm, unchanged. Node's httpServerPreClose calls the instance method (seen in the probe stack), so server.close() stays whole: it stops accepting and clears its connection-check interval at once. Route chosen over 'stop accepting without the sweep' because net.Server.prototype.close skips httpServerPreClose's clearInterval of a private-symbol interval (the closed server stays alive in a timer), and a later server.close() to clear it emits 'close' twice; deferring the whole server.close() keeps accepting. Force-close backstop byte-identical.",
"tests": "New in adapter-drain.test.ts: (1) PIN reordered slow reader, 32 MiB end() before close(), premise asserted (writableEnded true, writableFinished false), whole body vs Content-Length, plus an idle keep-alive connection released within 1 s of the flush ending. RED on main df22adc (adapter unchanged since 714dda8): 'every byte of the body arrived: expected 3919702 to be 33554432'. (2) CONTROL idle keep-alive released at drain start (close + connection end within 1 s). (3) CONTROL backstop: paused reader, 300 ms window, close() takes at least 250 ms and under 2 s, reader gets fewer than 32 MiB. Green 3/3 consecutive runs of the file (9/9). Ablations via scripts/ablation-replace.mjs (suite imports ./adapter from src, no build on the path): A1 on ab2845c, flushing check disabled ('if (false && res.writableEnded ...'): 2 failed / 7 passed, 'expected 3919702 to be 33554432' and 'expected 1 to be greater than or equal to 250'; A2 on 1502afc, held-back sweep never re-run (line deleted): 1 failed / 8, slow-reader case 'Test timed out in 5000ms'. Each restore: blob == HEAD, git diff HEAD empty. Package: pnpm --filter @objectstack/plugin-hono-server build && typecheck && test under the lock on ab2845c: VERDICT command-exit 0, Test Files 38/38, Tests 484/484 (tsconfig includes src/**/*, so typecheck compiles the test).",
"gates": "dispatch-gates --commands --repo objectstack-ai/objectstack on ab2845c (origin/main merged once at 733efbd): 63 derived; all 63 exit 0. Three first read exit 3 (prerequisite, NOT MEASURED) and were re-run green: check-plugin-teardown-shape --self-test after git fetch --depth=1 origin 621a487; check:dual-build-cjs-loads and check:lean-entry-closure after a full pnpm turbo run build --concurrency=2 under the lock (VERDICT command-exit 0, 9m30s); check:dts-closure and check:published-files re-read on the full build, exit 0. --ran: '63 derived famil(ies) accounted for — 63 run, 0 NOT-MEASURED'. Scoped lint: eslint --no-inline-config on adapter.ts + adapter-drain.test.ts, exit 0. CI check-runs: in_progress at report time; the seat reads them. Record: /mnt/project-files/pm-orders/server/31208-gates-ran.txt.",
"line_budget": "3 files, +151 / -11 (162 changed lines) vs origin/main three-dot; under the 3000 human-merge line",
"files_changed": [
".changeset/31208-hono-drain-keeps-flushing-response.md",
"packages/plugins/plugin-hono-server/src/adapter-drain.test.ts",
"packages/plugins/plugin-hono-server/src/adapter.ts"
],
"deviations": [
"pr_create carried no assignees (order Standing lines); os-dev.md rule 2 reads that pr_create carries the claim Account. Followed the order; read back: assignees none.",
"pr_create carried no labels: the order names none, and the diff carries a changeset, so no skip-changeset.",
"The backstop control now also asserts close() takes at least 250 ms, which is red on main (the drain-start sweep destroyed the reader at once there); it pins that the reader is held for the window rather than swept."
],
"dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm",
"mcp_calls": "0",
"api_writes": "2 relay strokes: pr_create via fleet-write dispatch.mjs (POST /repos/objectstack-ai/objectstack/pulls, PR #31255, run 42409088525, body read back 8820/8820 bytes identical); this comment via post-stamped.mjs (POST /repos//issues/31208/comments). Plus git push x1",
"open_questions": [],
"security_findings": "none",
"out_of_scope_findings": [
"none"
]
}
Generated by Claude Code
objectstack-fleet commentedon Nov 7, 2026
domain:server#5 · os-project-manager · session_01FEkQ5o8PE7ZtVjcor51wBZ
ACCEPT → PR #31255 comment 6712667832 (Head-sha
ab2845c820). Ready, auto-merge armed; the card closes on merge.