mcp: improve error messages from Wait for streamable clients

Previously, the error message received from ClientSession.Wait would
only report the closeErr, which would often be nil even if the client
transport was broken. Wait should return the reason the session
terminated, if abnormal.

I'm not sure of the exact semantics of this, but surely returning nil is
less useful than returning a meaningful non-nil error. We can refine our
handling of errors once we have more feedback.

Also add a test for client termination on HTTP server shutdown,
described in #265. This should work as long as (1) the session is
stateful (with a hanging GET), or (2) the session is stateless but the
client has a keepalive ping.

Also: don't send DELETE if the session was terminated with 404; +test.

Fixes #265
This commit is contained in:
Rob Findley 2025-09-09 14:36:19 +00:00 • committed by Robert Findley
commit 728e0e3ac9

View file

@ -483,10 +483,32 @@ func (c *Connection) Cancel(id ID) {
// Wait blocks until the connection is fully closed, but does not close it.
func (c *Connection) Wait() error {
return c.wait(true)
}
// wait for the connection to close, and aggregates the most cause of its
// termination, if abnormal.
//
// The fromWait argument allows this logic to be shared with Close, where we
// only want to expose the closeErr.
//
// (Previously, Wait also only returned the closeErr, which was misleading if
// the connection was broken for another reason).
func (c *Connection) wait(fromWait bool) error {
var err error
<-c.done
c.updateInFlight(func(s *inFlightState) {
err = s.closeErr
if fromWait {
if !errors.Is(s.readErr, io.EOF) {
err = s.readErr
}
if err == nil && !errors.Is(s.writeErr, io.EOF) {
err = s.writeErr
}
}
if err == nil {
err = s.closeErr
}
})
return err
}
@ -502,8 +524,7 @@ func (c *Connection) Close() error {
// Stop handling new requests, and interrupt the reader (by closing the
// connection) as soon as the active requests finish.
c.updateInFlight(func(s *inFlightState) { s.connClosing = true })
return c.Wait()
return c.wait(false)
}
// readIncoming collects inbound messages from the reader and delivers them, either responding