Skip to content

Commit aec6f6c

Browse files
committed
fix(auth): surface 'No token' dispatch failure as AuthError::NoToken
When a session expires mid-chat, the AWS SDK returns a dispatch failure with 'No token' in the error chain. This was converted to ChatError::Client and displayed as an opaque multi-line error instead of the clear 'Your session has expired. Run q login' message. Add is_no_token_error() to walk the error source chain and detect this case, converting it to ChatError::Auth(AuthError::NoToken) so the existing handler in handle_chat_error displays the actionable message. Fixes #3173
1 parent e14ea18 commit aec6f6c

1 file changed

Lines changed: 63 additions & 0 deletions

File tree

  • crates/chat-cli/src/cli/chat

crates/chat-cli/src/cli/chat/mod.rs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -624,10 +624,73 @@ impl ReasonCode for ChatError {
624624

625625
impl From<ApiClientError> for ChatError {
626626
fn from(value: ApiClientError) -> Self {
627+
// A dispatch failure with "No token" in the error chain means the session has
628+
// expired. Surface this as AuthError::NoToken so the handler in handle_chat_error
629+
// can display a clear "run `q login`" message instead of an opaque SDK error.
630+
// See: #3173
631+
if is_no_token_error(&value) {
632+
return Self::Auth(crate::auth::AuthError::NoToken);
633+
}
627634
Self::Client(Box::new(value))
628635
}
629636
}
630637

638+
/// Returns true if the error chain contains a "No token" dispatch failure,
639+
/// which indicates the user's session has expired.
640+
fn is_no_token_error(err: &ApiClientError) -> bool {
641+
use std::error::Error;
642+
let mut source: Option<&dyn Error> = Some(err);
643+
while let Some(e) = source {
644+
if e.to_string().contains("No token") {
645+
return true;
646+
}
647+
source = e.source();
648+
}
649+
false
650+
}
651+
652+
#[cfg(test)]
653+
mod is_no_token_error_tests {
654+
use crate::api_client::error::ApiClientError;
655+
use crate::auth::AuthError;
656+
657+
use super::{ChatError, is_no_token_error};
658+
659+
#[test]
660+
fn detects_auth_no_token() {
661+
let err = ApiClientError::AuthError(AuthError::NoToken);
662+
assert!(is_no_token_error(&err));
663+
}
664+
665+
#[test]
666+
fn ignores_unrelated_errors() {
667+
let err = ApiClientError::DefaultModelNotFound;
668+
assert!(!is_no_token_error(&err));
669+
}
670+
671+
#[test]
672+
fn from_api_client_error_converts_no_token_to_auth_error() {
673+
let err = ApiClientError::AuthError(AuthError::NoToken);
674+
let chat_err = ChatError::from(err);
675+
assert!(
676+
matches!(chat_err, ChatError::Auth(AuthError::NoToken)),
677+
"expected ChatError::Auth(NoToken), got: {:?}",
678+
chat_err
679+
);
680+
}
681+
682+
#[test]
683+
fn from_api_client_error_keeps_other_errors_as_client() {
684+
let err = ApiClientError::DefaultModelNotFound;
685+
let chat_err = ChatError::from(err);
686+
assert!(
687+
matches!(chat_err, ChatError::Client(_)),
688+
"expected ChatError::Client, got: {:?}",
689+
chat_err
690+
);
691+
}
692+
}
693+
631694
impl From<parser::SendMessageError> for ChatError {
632695
fn from(value: parser::SendMessageError) -> Self {
633696
Self::SendMessage(Box::new(value))

0 commit comments

Comments
 (0)