Skip to content

fix(gateway): propagate AbortController signal on HTTP client disconnect#54388

Merged
obviyus merged 4 commits intoopenclaw:mainfrom
Lellansin:feat/chat-completions-cancel
Apr 7, 2026
Merged

fix(gateway): propagate AbortController signal on HTTP client disconnect#54388
obviyus merged 4 commits intoopenclaw:mainfrom
Lellansin:feat/chat-completions-cancel

Conversation

@Lellansin
Copy link
Copy Markdown
Contributor

@Lellansin Lellansin commented Mar 25, 2026

Summary

  • Problem: The /v1/chat/completions and /v1/responses HTTP endpoints do not cancel the underlying agent execution when a client disconnects. The close handler only sets a closed flag and unsubscribes from agent events, but never aborts the in-flight LLM call.
  • Why it matters: When an HTTP client disconnects mid-request, the backend continues running the full LLM turn, wasting tokens, compute, and holding resources unnecessarily.
  • What changed: Created an AbortController in each handler (both streaming and non-streaming paths) and pass abortController.signal through to agentCommandFromIngress. On client disconnect (res.on("close")), the controller is aborted, which propagates cancellation to the agent runtime via the existing abortSignal plumbing (acpManager.runTurn({ signal })). Used res.on("close") instead of req.on("close") because the request body is fully consumed by handleGatewayPostJsonEndpoint before the abort listener is registered, and IncomingMessage auto-destroys after EOF in modern Node.js.
  • What did NOT change (scope boundary): No changes to the agent runtime, ACP manager, WebSocket handlers, or any other endpoint. The AbortSignal infrastructure was already in place; this PR only wires it into the two HTTP endpoints that were missing it.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor required for the fix
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

  • Related: The WebSocket chat handler (src/gateway/server-methods/chat.ts) already implements this pattern correctly; this PR brings HTTP endpoints to parity.
  • This PR fixes a bug or regression

Root Cause / Regression History (if applicable)

  • Root cause: The /v1/chat/completions and /v1/responses HTTP handlers were originally implemented without AbortController integration. The abortSignal parameter on AgentCommandOpts existed but was never wired in from these HTTP code paths.
  • Missing detection / guardrail: No tests verified that client disconnection propagated an abort signal to the agent command layer for HTTP endpoints.
  • Prior context: The WebSocket chat handler (server-methods/chat.ts:1367) was implemented with proper abort support from the start; the HTTP endpoints were added later without this consideration.
  • Why this regressed now: This was never implemented, not a regression from a prior change.
  • If unknown, what was ruled out: N/A.

Regression Test Plan (if applicable)

  • Coverage level that should have caught this:
    • Unit test
    • Seam / integration test
    • End-to-end test
    • Existing coverage already sufficient
  • Target test or file: src/gateway/openai-http.test.ts, src/gateway/openresponses-http.test.ts
  • Scenario the test should lock in: When an HTTP client disconnects mid-request (both streaming and non-streaming), the abortSignal passed to agentCommandFromIngress must transition to aborted state.
  • Why this is the smallest reliable guardrail: The tests mock the agent command to observe the abort signal directly, using http.request + req.destroy() to simulate client disconnection. This isolates the exact behavior without requiring a full agent runtime.
  • Existing test that already covers this (if any): None for HTTP; WebSocket abort tests exist in server.chat.gateway-server-chat-b.test.ts.
  • If no new test is added, why not: N/A — 4 new tests added.

User-visible / Behavior Changes

When an HTTP client disconnects from /v1/chat/completions or /v1/responses (streaming or non-streaming), the backend now cancels the in-flight LLM call instead of letting it run to completion. This reduces wasted tokens and frees resources faster.

Security Impact (required)

  • New permissions/capabilities? No
  • Secrets/tokens handling changed? No
  • New/changed network calls? No
  • Command/tool execution surface changed? No
  • Data access scope changed? No

Repro + Verification

Environment

  • OS: macOS
  • Runtime/container: Node 22+ / pnpm
  • Model/provider: Any (mock in tests)
  • Integration/channel (if any): HTTP gateway endpoints
  • Relevant config (redacted): default local test config

Steps

  1. Start the gateway with HTTP endpoints enabled.
  2. Send a streaming or non-streaming request to /v1/chat/completions or /v1/responses.
  3. Disconnect the client before the response completes.

Expected

  • The agent command receives an aborted signal and stops processing.

Actual

  • Before fix: the agent command continues running to completion, wasting resources.
  • After fix: matches expected.

Evidence

  • Failing test/log before + passing after
  • Trace/log snippets
  • Screenshot/recording
  • Perf numbers (if relevant)

Local verification commands:

  • pnpm test -- src/gateway/openai-http.test.ts (6/6 passed)
  • pnpm test -- src/gateway/openresponses-http.test.ts (14/14 passed)
  • pnpm format (clean)

Human Verification (required)

  • Verified scenarios: streaming and non-streaming abort propagation for both /v1/chat/completions and /v1/responses; all pre-existing tests still pass.
  • Edge cases checked: res.on("close") vs req.on("close") — verified that req.on("close") does NOT fire reliably after body consumption in modern Node.js due to IncomingMessage auto-destroy; switched to res.on("close") which fires when the underlying TCP connection closes.
  • What you did not verify: full pnpm test suite run; live integration with real LLM provider.

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

Compatibility / Migration

  • Backward compatible? Yes
  • Config/env changes? No
  • Migration needed? No

Failure Recovery (if this breaks)

  • How to disable/revert this change quickly: Revert this commit; the abort signal is simply ignored if not passed.
  • Files/config to restore: src/gateway/openai-http.ts, src/gateway/openresponses-http.ts.
  • Known bad symptoms reviewers should watch for: If res.on("close") fires prematurely (before response is sent), it could abort requests that should complete. This would manifest as incomplete or missing responses.

Risks and Mitigations

  • Risk: res.on("close") might fire after a successful response is fully sent, calling abortController.abort() on an already-completed operation.
    • Mitigation: Aborting an already-completed AbortController is a no-op. The agent command has already returned by that point, so the signal is harmless.

@openclaw-barnacle openclaw-barnacle Bot added gateway Gateway runtime size: M labels Mar 25, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Mar 25, 2026

Greptile Summary

This PR wires AbortController into the /v1/chat/completions and /v1/responses HTTP endpoints so that client disconnection cancels in-flight LLM calls, bringing these handlers to parity with the existing WebSocket path.\n\nKey changes:\n- Creates an AbortController per request in both handleOpenAiHttpRequest and handleOpenResponsesHttpRequest\n- Passes abortController.signal through to agentCommandFromIngress / runResponsesAgentCommand via the existing abortSignal plumbing\n- Switches from req.on(\"close\") to res.on(\"close\") — well-justified: IncomingMessage auto-destroys after body consumption, while ServerResponse reliably fires close on TCP disconnect\n- In the streaming paths, the !closed guard on the close handler correctly prevents a spurious abort when the connection closes after a normal res.end() call\n- In the non-streaming paths, a post-await abortController.signal.aborted check avoids sending a response body to a disconnected client\n- Four new integration-style tests cover streaming and non-streaming disconnects for both endpoints

Confidence Score: 5/5

Safe to merge — the change is strictly additive abort wiring with no behavioral impact on the happy path.

The implementation is logically correct: the closed flag in streaming paths prevents spurious abort on normal completion, the post-await abort checks cleanly suppress responses to disconnected clients, and aborting an already-completed controller is a documented no-op. The res.on("close") choice over req.on("close") is well-justified and matches the existing WebSocket pattern. Four targeted tests verify all abort-propagation scenarios. No changes to the agent runtime, ACP manager, or WebSocket paths.

No files require special attention.

Reviews (3): Last reviewed commit: "fix: use correct 'close' event name for ..." | Re-trigger Greptile

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cfc88cdc57

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/gateway/openai-http.ts Outdated
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3434550b83

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/gateway/openai-http.ts Outdated
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0ff718e5fb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/gateway/openresponses-http.ts Outdated
Comment thread src/gateway/openresponses-http.ts Outdated
@Lellansin Lellansin force-pushed the feat/chat-completions-cancel branch 2 times, most recently from 2ca816b to 2a653d3 Compare March 25, 2026 12:52
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2a653d3262

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/gateway/openai-http.ts Outdated
@Lellansin
Copy link
Copy Markdown
Contributor Author

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 82941d8d1b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/gateway/openai-http.ts Outdated
@Lellansin Lellansin marked this pull request as draft March 25, 2026 15:10
@Lellansin
Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🚀

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@Lellansin Lellansin marked this pull request as ready for review March 26, 2026 01:36
@Lellansin Lellansin marked this pull request as draft March 26, 2026 06:07
@Lellansin Lellansin marked this pull request as ready for review March 26, 2026 06:07
@Lellansin Lellansin requested a review from a team as a code owner April 2, 2026 09:10
@openclaw-barnacle openclaw-barnacle Bot added channel: discord Channel integration: discord channel: line Channel integration: line channel: matrix Channel integration: matrix channel: telegram Channel integration: telegram channel: whatsapp-web Channel integration: whatsapp-web app: web-ui App: web-ui commands Command implementations agents Agent runtime and tooling size: XL and removed size: M labels Apr 2, 2026
@Lellansin Lellansin force-pushed the feat/chat-completions-cancel branch from 6a5ef52 to 6b9d87b Compare April 2, 2026 09:20
@openclaw-barnacle openclaw-barnacle Bot removed the channel: discord Channel integration: discord label Apr 2, 2026
@openclaw-barnacle openclaw-barnacle Bot added size: M and removed channel: line Channel integration: line channel: matrix Channel integration: matrix channel: telegram Channel integration: telegram channel: whatsapp-web Channel integration: whatsapp-web app: web-ui App: web-ui commands Command implementations agents Agent runtime and tooling size: XL labels Apr 2, 2026
@obviyus
Copy link
Copy Markdown
Contributor

obviyus commented Apr 7, 2026

Pushed a small follow-up directly to the PR branch.

What changed:

  • switched HTTP disconnect watching to the underlying socket close path
  • shared that logic in one helper instead of duplicating it in both handlers
  • removed the extra explanatory comment churn around the old res.on("close") branches

Why:

  • the real thing we care about here is connection lifetime, not response-object lifetime
  • this keeps the abort wiring simpler and makes both /v1/chat/completions and /v1/responses use the same disconnect model

Commit: 732c33dd0c

@obviyus obviyus self-assigned this Apr 7, 2026
冰森 and others added 4 commits April 7, 2026 11:12
Abort running agent commands when the HTTP client disconnects for both
/v1/chat/completions and /v1/responses endpoints.

- Listen on res "close" instead of req "close" (the request body is
  already consumed so IncomingMessage auto-destroys before we get here).
- Non-streaming: guard with !signal.aborted so the abort fires on
  genuine disconnects; a spurious abort after sendJson is harmless.
- Streaming: guard with !closed so normal res.end() completions do not
  abort post-turn work still in flight.
- Skip error logging and response writes when the signal is already
  aborted.

Made-with: Cursor
…equests

Updated the event listener for client disconnects to use the correct name and enhanced error handling logic. The changes ensure that abort signals are properly checked before logging errors and returning responses, preventing unnecessary operations on aborted requests.

Made-with: Cursor
@obviyus obviyus force-pushed the feat/chat-completions-cancel branch from 732c33d to 7313421 Compare April 7, 2026 05:44
Copy link
Copy Markdown
Contributor

@obviyus obviyus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed latest changes; landing now.

@obviyus obviyus merged commit aad3bbe into openclaw:main Apr 7, 2026
8 checks passed
@obviyus
Copy link
Copy Markdown
Contributor

obviyus commented Apr 7, 2026

Landed on main.

Thanks @Lellansin.

lovewanwan pushed a commit to lovewanwan/openclaw that referenced this pull request Apr 28, 2026
…thanks @Lellansin)

* fix: abort in-flight HTTP requests on client disconnect

Abort running agent commands when the HTTP client disconnects for both
/v1/chat/completions and /v1/responses endpoints.

- Listen on res "close" instead of req "close" (the request body is
  already consumed so IncomingMessage auto-destroys before we get here).
- Non-streaming: guard with !signal.aborted so the abort fires on
  genuine disconnects; a spurious abort after sendJson is harmless.
- Streaming: guard with !closed so normal res.end() completions do not
  abort post-turn work still in flight.
- Skip error logging and response writes when the signal is already
  aborted.

Made-with: Cursor

* fix: correct event listener name and improve error handling in HTTP requests

Updated the event listener for client disconnects to use the correct name and enhanced error handling logic. The changes ensure that abort signals are properly checked before logging errors and returning responses, preventing unnecessary operations on aborted requests.

Made-with: Cursor

* fix: use correct 'close' event name for non-streaming disconnect handler

* fix: watch socket close for HTTP aborts

---------

Co-authored-by: 冰森 <dingheng.huang@urbanic.com>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
ogt-redknie pushed a commit to ogt-redknie/OPENX that referenced this pull request May 2, 2026
…thanks @Lellansin)

* fix: abort in-flight HTTP requests on client disconnect

Abort running agent commands when the HTTP client disconnects for both
/v1/chat/completions and /v1/responses endpoints.

- Listen on res "close" instead of req "close" (the request body is
  already consumed so IncomingMessage auto-destroys before we get here).
- Non-streaming: guard with !signal.aborted so the abort fires on
  genuine disconnects; a spurious abort after sendJson is harmless.
- Streaming: guard with !closed so normal res.end() completions do not
  abort post-turn work still in flight.
- Skip error logging and response writes when the signal is already
  aborted.

Made-with: Cursor

* fix: correct event listener name and improve error handling in HTTP requests

Updated the event listener for client disconnects to use the correct name and enhanced error handling logic. The changes ensure that abort signals are properly checked before logging errors and returning responses, preventing unnecessary operations on aborted requests.

Made-with: Cursor

* fix: use correct 'close' event name for non-streaming disconnect handler

* fix: watch socket close for HTTP aborts

---------

Co-authored-by: 冰森 <dingheng.huang@urbanic.com>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 9, 2026
…thanks @Lellansin)

* fix: abort in-flight HTTP requests on client disconnect

Abort running agent commands when the HTTP client disconnects for both
/v1/chat/completions and /v1/responses endpoints.

- Listen on res "close" instead of req "close" (the request body is
  already consumed so IncomingMessage auto-destroys before we get here).
- Non-streaming: guard with !signal.aborted so the abort fires on
  genuine disconnects; a spurious abort after sendJson is harmless.
- Streaming: guard with !closed so normal res.end() completions do not
  abort post-turn work still in flight.
- Skip error logging and response writes when the signal is already
  aborted.

Made-with: Cursor

* fix: correct event listener name and improve error handling in HTTP requests

Updated the event listener for client disconnects to use the correct name and enhanced error handling logic. The changes ensure that abort signals are properly checked before logging errors and returning responses, preventing unnecessary operations on aborted requests.

Made-with: Cursor

* fix: use correct 'close' event name for non-streaming disconnect handler

* fix: watch socket close for HTTP aborts

---------

Co-authored-by: 冰森 <dingheng.huang@urbanic.com>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gateway Gateway runtime size: M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants