fix(client): classify discover outcome at source, not at the error type (#1133)

`ClientLifecycleMode::Auto` only fell back from `server/discover` on
`-32601`, so legacy servers that reject the probe with other codes
(`-32600`, `-32602`, implementation-defined errors) failed to connect
even though `initialize` would have succeeded.

The previous attempt (indicates_legacy_server) classified the failure
after the fact by reverse-engineering the error type. This rewrite moves
the classification into `discover_startup` itself, where the full context
(request id, response correlation, transport state) is still available.

`discover_startup` now returns `DiscoverOutcome`: `Modern` on success,
`Legacy(error)` when the probe received a complete, correlated JSON-RPC
error whose code is not a modern-era rejection. Every other failure
becomes `Err`, so `Auto` simply matches the outcome — no methods on
`ClientInitializeError`, no downcast, no transport-specific types
leaking into the generic lifecycle layer.

Additional fixes that fall out naturally:

- Response correlation is now checked in `expect_response` for both
  success and error branches. Previously error responses skipped id
  correlation entirely. A new `UncorrelatedErrorResponse` variant
  surfaces responses that cannot be tied to the request.

- When both discover and the legacy fallback fail, a
  `LegacyFallbackFailed` compound error preserves both phases instead
  of discarding the discover error.

Fixes #1040.
This commit is contained in:
ip2a 2026-08-10 02:07:50 +08:00 • committed by GitHub
commit 22be2cc5ad
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -125,11 +125,18 @@ async fn test_client_init_handles_jsonrpc_error() {
});
tokio::spawn(async move {
let _init_request = server.receive().await;
let request = server.receive().await;
// Echo the request's own id back on the error so it correlates: an
// uncorrelated id would surface as `UncorrelatedErrorResponse`
// instead of the `JsonRpcError` this test exercises.
let request_id = request
.and_then(|message| message.into_request())
.map(|(_, id)| id)
.expect("client sent an initialize request");
let error_msg = ServerJsonRpcMessage::Error(JsonRpcError {
jsonrpc: JsonRpcVersion2_0,
id: Some(RequestId::Number(1)),
id: Some(request_id),
error: ErrorData {
code: ErrorCode(-32600),
message: Cow::Borrowed("Invalid Request"),