feat(qconnect): surface missing AUTHENTICATE JWT as a hard credential error
Add `require_jwt` (default false) to `WsTransportConfig` and a `MissingAuthenticateJwt` error variant. When `require_jwt` is set but no `jwt_qws` is present, the connect attempt now emits a `TransportError`, closes the socket, and routes through the existing bounded reconnect path instead of silently skipping the AUTHENTICATE frame (gap #12). The default stays false so the InMemory / test transport path keeps working without a JWT; `resolve_transport_config` flips it on for the real Qobuz endpoint. Reuses the increment-only `handle_reconnect_delay`, so the issue #358 latch is untouched. cc
This commit is contained in:
4 files changed
+52
No files matched your search
@@ -2,6 +2,11 @@
|
||||
pub struct WsTransportConfig {
|
||||
pub endpoint_url: String,
|
||||
pub jwt_qws: Option<String>,
|
||||
/// When `true`, a connect attempt with `jwt_qws == None` is a hard
|
||||
/// credential error instead of silently skipping the AUTHENTICATE frame
|
||||
/// (gap #12). Defaults to `false` so the InMemory / test transport path
|
||||
/// keeps working without a JWT.
|
||||
pub require_jwt: bool,
|
||||
pub reconnect_backoff_ms: u64,
|
||||
pub reconnect_backoff_max_ms: u64,
|
||||
/// Maximum number of consecutive reconnect attempts before the transport
|
||||
@@ -25,6 +30,7 @@ impl Default for WsTransportConfig {
|
||||
Self {
|
||||
endpoint_url: String::new(),
|
||||
jwt_qws: None,
|
||||
require_jwt: false,
|
||||
reconnect_backoff_ms: 2_000,
|
||||
reconnect_backoff_max_ms: 30_000,
|
||||
reconnect_max_attempts: Some(10),
|
||||
|
||||
@@ -20,4 +20,6 @@ pub enum WsTransportError {
|
||||
Serialization(String),
|
||||
#[error("internal transport error: {0}")]
|
||||
Internal(String),
|
||||
#[error("missing AUTHENTICATE JWT for an endpoint that requires it")]
|
||||
MissingAuthenticateJwt,
|
||||
}
|
||||
@@ -421,6 +421,38 @@ async fn run_native_transport_loop(
|
||||
}
|
||||
}
|
||||
emit(&events_tx, TransportEvent::Authenticated);
|
||||
} else if config.require_jwt {
|
||||
// gap #12: the real Qobuz endpoint requires AUTHENTICATE. Silently
|
||||
// skipping it leaves the session un-joinable; surface it as a hard
|
||||
// credential error and route it through the existing bounded
|
||||
// reconnect path (increment-only — issue #358 latch unaffected).
|
||||
emit(
|
||||
&events_tx,
|
||||
TransportEvent::TransportError {
|
||||
stage: "authenticate".to_string(),
|
||||
message: "missing jwt_qws for endpoint that requires AUTHENTICATE".to_string(),
|
||||
},
|
||||
);
|
||||
let _ = ws.close(None).await;
|
||||
{
|
||||
let mut guard = state.lock().await;
|
||||
guard.connected = false;
|
||||
}
|
||||
emit(&events_tx, TransportEvent::Disconnected);
|
||||
match handle_reconnect_delay(
|
||||
&events_tx,
|
||||
&mut shutdown_rx,
|
||||
&mut reconnect_attempt,
|
||||
&mut backoff,
|
||||
max_backoff,
|
||||
max_attempts,
|
||||
"missing_authenticate_jwt".to_string(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
ReconnectOutcome::Continue => continue,
|
||||
ReconnectOutcome::Shutdown | ReconnectOutcome::Exhausted => break,
|
||||
}
|
||||
}
|
||||
|
||||
if config.auto_subscribe {
|
||||
@@ -964,6 +996,15 @@ struct CloudPayload {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// gap #12: `require_jwt` must default to `false` so the InMemory / test
|
||||
/// transport path keeps working without a JWT. Only the real Qobuz endpoint
|
||||
/// (set in `resolve_transport_config`) flips it on.
|
||||
#[test]
|
||||
fn config_require_jwt_defaults_false_for_test_path() {
|
||||
let cfg = WsTransportConfig::default();
|
||||
assert!(!cfg.require_jwt);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn qcloud_frame_roundtrip() {
|
||||
let payload = vec![0x01, 0x02, 0x03, 0x04];
|
||||
|
||||
Reference in new issue
Block a user