Skip to content

drain-path response-send failure logs at ERROR for a routine client-disconnect race #6

Description

@bug-ops

Description

In crates/rmcp/src/service.rs, the drain phase that runs after the main message loop exits (QuitReason::Closed / QuitReason::Cancelled, ~line 1722) flushes in-flight handler responses before closing the transport. If transport.send(m).await fails there — typically because the peer/session channel is already closed, i.e. the client already disconnected — the failure is logged at tracing::error! (line 1748: "failed to send pending response during drain").

This is inconsistent with the identical failure mode a few lines earlier in the same file (lines 1694 and 1715: a responder send failing because the receiver is gone), which correctly logs at tracing::warn!.

A "channel closed" failure during drain is the expected outcome of the very state transition (Closed/Cancelled) that triggered the drain in the first place — the peer already disconnected, so of course the response can't be delivered. This path fires on completely ordinary production events: a browser tab closing, a client crash, a network drop, or a client cancelling a request. Logging it at ERROR means every downstream server built on this SDK gets false-alarm ERROR log lines for routine disconnects, which is likely to trigger alerting/paging in any ops setup that treats ERROR-level logs as actionable, training on-call to ignore this SDK's real errors.

Reproduction Steps

Reproduced twice, live, both fully consistent (not a one-off):

  1. Conformance suite:

    cargo build -p mcp-conformance
    PORT=8001 ./target/debug/conformance-server &
    npx -y "@modelcontextprotocol/conformance@0.2.0-alpha.10" server \
      --url http://127.0.0.1:8001/mcp --suite all --spec-version 2026-07-28
    

    All 115 scenarios pass (this is not a correctness/functional bug), but the server log shows 3 occurrences of the ERROR line, each immediately after a task cancelled / serve finished quit_reason=Cancelled line — triggered by the conformance client legitimately closing/cancelling connections as part of normal test scenarios.

  2. Plain example pair:

    cargo build --manifest-path examples/servers/Cargo.toml --all-features --example servers_subscriptions_streamhttp
    cargo build --manifest-path examples/clients/Cargo.toml --all-features --example clients_subscriptions_streamhttp
    ./target/debug/examples/servers_subscriptions_streamhttp &
    timeout 20 ./target/debug/examples/clients_subscriptions_streamhttp
    

    Killing the client while a subscription stream is open produces the identical ERROR line on the server immediately after disconnect.

Expected Behavior

The drain-path send failure should log at tracing::warn! (or debug!), matching the identical-purpose logging at service.rs:1694 and service.rs:1715, since "receiver gone during an already-closing/cancelled session" is an expected outcome, not an application error.

Actual Behavior

Logs at tracing::error!:

ERROR serve_inner: rmcp::service: failed to send pending response during drain error=channel closed

on every ordinary client disconnect that races an in-flight response.

Environment

  • Version: v3.1.2 (commit 02c62ae), crates/rmcp/src/service.rs (~line 1748)
  • Features: default conformance-server/conformance-client binaries; examples/servers and examples/clients built with --all-features

Upstream

Checked modelcontextprotocol/rust-sdk open+closed issues and merged/closed PRs for "drain", "channel closed", "log level"/"log noise" — found related-but-distinct history: `modelcontextprotocol/rust-sdk#753` (stdio server drops in-flight tool responses when stdin closes, closed) and its fix `modelcontextprotocol/rust-sdk#759` (fix: drain in-flight responses on stdin EOF, merged) — that PR is what introduced this drain mechanism (and its error! call) in the first place. No existing upstream issue specifically about the log-severity inconsistency found.

Logs / Evidence

2026-08-08T15:01:49.222716Z ERROR serve_inner: rmcp::service: failed to send pending response during drain error=channel closed

(preceded by task cancelled / serve finished quit_reason=Cancelled in both reproductions)

Spec

.local/specs/002-drain-error-log-severity/spec.md

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2High: degraded UX, incorrect non-destructive behaviorbugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions