n0-computer/iroh · error · ConnectError
UnexpectedUpgradeStatus
UnexpectedUpgradeStatus
Error message
Unexpected status during upgrade: {code} What it means
ConnectError::UnexpectedUpgradeStatus from iroh-relay's websocket-based client connect. After issuing the HTTP upgrade request over the established stream, the client requires hyper's 101 SWITCHING_PROTOCOLS status; any other status (e.g. 4xx/5xx from an intermediary) fails the websocket upgrade and this error carries the actual status code.
Solutions
- Inspect the status code in the error: 401/403 means auth is missing/invalid; 404 means wrong URL/path; 502/503 means a proxy or relay is down.
- Verify the relay URL, scheme (wss vs ws), and that the server supports the websocket transport.
- Bypass or correctly configure intermediate proxies to pass Upgrade/Connection headers.
- Update the iroh-relay client/server so protocol versions match.
Example fix
match relay_client.connect().await {
Ok(conn) => /* use conn */,
Err(ConnectError::UnexpectedUpgradeStatus { code }) if code == 401 => {
// refresh auth token then retry
}
Err(e) => return Err(e.into()),
} Defensive patterns
Strategy: try-catch
Validate before calling
// Pre-flight: confirm the relay URL answers an HTTP request before upgrading
let probe = reqwest::get(relay_url.join("ping")?).await?;
ensure!(probe.status().is_success(), "relay unreachable: {}", probe.status()); Try / catch
match client.connect().await {
Err(e) if format!("{e}").contains("UnexpectedUpgradeStatus") => {
// log status, check proxy/auth, retry with backoff
}
other => other,
} Prevention
- Verify relay URL, scheme (wss/ws), and reachability before connecting
- Ensure proxies pass Upgrade/Connection headers for websocket traffic
- Send auth headers (CLIENT_AUTH_HEADER) when the relay requires them
- Match client/server iroh versions to avoid protocol mismatches
When it happens
Trigger: Calling connect() on the relay websocket client when response.status() != 101 — e.g. the relay or a proxy replies with 400/401/403/404/500, or an HTTP/1.0 proxy mangles the Upgrade request.
Common situations: Corporate proxies or load balancers stripping WebSocket Upgrade headers; relays requiring auth returning 401/403; wrong relay URL or TLS termination returning 404/502; relay running an incompatible protocol version.
Related errors
- The relay is rate-limiting this endpoint; outbound relay…
- Invalid transport configuration
- ExceedsMaxPacketSize
- EmptyPacket
- InvalidHeader
AI-assisted analysis of n0-computer/iroh@2b4de030ce (2026-09-08).
Data as JSON: /api/errors/9242dc8ee8d2223e.
Report an issue: GitHub.
Appendix: source
Thrown at iroh-relay/src/client.rs:338
if let Some(token) = self.auth_token.as_ref() {
let value = HeaderValue::from_str(&format!("Bearer {token}"))
.map_err(|_| e!(ConnectError::InvalidAuthToken))?;
builder = builder
.add_header(AUTHORIZATION, value)
.expect("valid header name");
}
if let Some(client_auth) = KeyMaterialClientAuth::new(&self.secret_key, &stream) {
debug!("Using TLS key export for relay client authentication");
builder = builder
.add_header(CLIENT_AUTH_HEADER, client_auth.into_header_value())
.expect(
"impossible: CLIENT_AUTH_HEADER isn't a disallowed header value for websockets",
);
}
let (conn, response) = builder.connect_on(stream).await.anyerr()?;
n0_error::ensure!(
response.status() == hyper::StatusCode::SWITCHING_PROTOCOLS,
ConnectError::UnexpectedUpgradeStatus {
code: response.status()
}
);
let protocol_version_str = response
.headers()
.get(SEC_WEBSOCKET_PROTOCOL)
.and_then(|s| s.to_str().ok());
let protocol_version = protocol_version_str
.and_then(ProtocolVersion::match_from_str)
.ok_or_else(|| {
e!(ConnectError::BadVersionHeader {
server_version: protocol_version_str.map(ToOwned::to_owned)
})
})?;
View on GitHub (pinned to 2b4de030ce)