Skip to content

fix(agentserver): treat closed response stream as client disconnect - #2251

Merged
chsami merged 1 commit into
developmentfrom
claude/err-e-agentserver-stream-closed-logged-as-error
Nov 12, 2026
Merged

chsami merged 1 commit into
developmentfrom
claude/err-e-agentserver-stream-closed-logged-as-error

Conversation

@chsami

@chsami chsami commented Nov 12, 2026

Copy link
Copy Markdown
Owner

Problem

Client telemetry group 5185 (1 event, 1 report, Microbot 2.6.56, client core, thread AgentServer-1):

java.io.IOException: stream is closed
  sun.net.httpserver.Request$WriteStream.write:413
  sun.net.httpserver.ExchangeImpl.sendResponseHeaders:284
  ...agentserver.handler.AgentHandler.sendJson:165
  ...agentserver.handler.WalkHandler.handleRequest:149
  ...agentserver.handler.AgentHandler.handle:70

Logged by AgentHandler as Error handling {} at error level. An agent caller going away during a waited walk is a normal client disconnect, not an error. There is no functional breakage for the player, but it produces an error-level telemetry report and a second IOException thrown out of handle().

Root cause

When a waited walk (wait=true) has its wait interrupted, WalkHandler (lines 144-150) calls sendJson(exchange, 500, ...) on an exchange that is already closed. exchange.sendResponseHeaders then throws IOException("stream is closed").

AgentHandler.isClientDisconnect did not recognise that JDK httpserver message, so:

  1. handle() fell through to log.error("Error handling {}", ...).
  2. handle() then called sendErrorResponse(exchange, 500, ...) on the same closed exchange, which threw the same unrecognised IOException again and rethrew it out of handle().

The disconnect classification was introduced in #1962; this message variant was not covered.

Change

One line in the shared helper AgentHandler.isClientDisconnect: "stream is closed" is added to the recognised IOException messages.

  • handle() now logs at debug and returns without attempting an error response.
  • sendErrorResponse also swallows it.
  • Every handler is covered through the shared helper; WalkHandler is untouched.

Two things deliberately not done:

  • sendResponseHeaders is not wrapped in ClientDisconnectException. That call also throws IOException for non-disconnect cases (for example headers already sent), which would then be hidden as disconnects.
  • The looser "stream closed" variant is not matched. Telemetry only shows "stream is closed" (the JDK httpserver message), and "Stream closed" is the generic java.io message for unrelated closed file or input streams, which should still be logged as errors.

Validation

./gradlew :client:runUnitTests --tests '*AgentHandlerClientDisconnectTest' -> BUILD SUCCESSFUL, 5/5 passed (2 new tests plus the 3 existing ones).

New tests in AgentHandlerClientDisconnectTest:

  • drives handle() with sendResponseHeaders throwing IOException("stream is closed") and asserts no exception escapes, no 500 retry and no body write;
  • asserts isClientDisconnect recognises the message.

Code review: approved (correct at root).

Known gaps

  • Only the focused test class was run locally, not the full unit suite; CI covers the rest.
  • Not reproduced against a live client; the fix is validated by unit tests against the exact exception and message from telemetry.
  • The pre-auth sendOpaqueNotFound calls in handle() sit outside the try block, so a "stream is closed" failure there still propagates out of handle(). It never reaches log.error in AgentHandler, so it does not produce this telemetry group and is left unchanged.
  • "stream is closed" on an interrupted worker thread can also come from the agent server itself being stopped rather than the caller going away. The debug line "Client disconnected before ... response could be sent" is then slightly inaccurate; behaviour is correct either way because no response can be delivered.

🤖 Generated with Claude Code

A waited walk whose caller went away failed in sendResponseHeaders with
an IOException that isClientDisconnect did not recognise, so it was
logged at error level and the 500 retry threw again on the same
exchange. Classify that message as a disconnect.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

Explain Change →

No actionable comments were generated in the recent review.

Walkthrough

AgentHandler.isClientDisconnect now recognizes IOException messages containing “stream is closed.” A test simulates this exception during response-header sending and verifies that the handler does not attempt a 500 response or access the response body.

Merge Risk: Minimal · up to 1c854

Closed-stream exceptions from a waited walk are now treated as client disconnects, so they are logged at debug level and no longer trigger a failing 500 retry. No actionable merge risk remains.

Pre-merge checks: Passed 4

Check name Status Explanation
Title check Passed The title names the main change: a closed response stream ('stream is closed') is now classified as a client disconnect in the agent server. It matches the one-line change to AgentHandler.isClientDisc…
Description check Passed The description explains the problem, the root cause in WalkHandler and AgentHandler.isClientDisconnect, the one-line change, the handle() and sendErrorResponse behavior, the new tests, and known gaps…
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Title check

Explanation

The title names the main change: a closed response stream ('stream is closed') is now classified as a client disconnect in the agent server. It matches the one-line change to AgentHandler.isClientDisconnect and the new tests. The title is specific and concise, and it follows a conventional commit style.

Full details: Description check

Explanation

The description explains the problem, the root cause in WalkHandler and AgentHandler.isClientDisconnect, the one-line change, the handle() and sendErrorResponse behavior, the new tests, and known gaps. It relates directly to the changeset.

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

Recent review info

Run configuration

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: c5df2583-5568-483c-82e4-31dd228c4886
Commits

Reviewing files that changed from the base of the PR and between ebf0b9c and 1c8544c.

Files selected for processing: 2

  • runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/AgentHandler.java
  • runelite-client/src/test/java/net/runelite/client/plugins/microbot/agentserver/handler/AgentHandlerClientDisconnectTest.java

Included review availability: This review used your included allowance. Your plan provides up to 2 included reviews per hour; 0 remain after this review.

@chsami
chsami merged commit d63ce6a into development Nov 12, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant