From 83ae63c17a45cc3a6fefd1282dc2a274f56cd757 Mon Sep 17 00:00:00 2001 From: ajianaz Date: Wed, 23 Sep 2026 14:06:50 +0700 Subject: [PATCH] fix(oauth): surface Meta error body in long-lived token exchange failure Step 2 (th_exchange_token) discarded Meta's error object and returned a generic 'No access_token in exchange response', making real causes (invalid secret, expired code, permission scope, etc.) impossible to diagnose from the UI or logs. Step 1 already extracts Meta errors; this brings step 2 to parity and falls back to including the raw body. Refs #281 --- crates/titen-core/src/threads_client.rs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/crates/titen-core/src/threads_client.rs b/crates/titen-core/src/threads_client.rs index a5e83a9..5e8e5bc 100644 --- a/crates/titen-core/src/threads_client.rs +++ b/crates/titen-core/src/threads_client.rs @@ -187,9 +187,20 @@ impl ThreadsClient { .get("access_token") .and_then(|v| v.as_str()) .ok_or_else(|| { - crate::error::TitenError::ThreadsApiError( - "No access_token in exchange response".to_string(), - ) + // Surface Meta's error body instead of a generic message: + // without this, the actual failure reason (invalid secret, + // expired code, permission scope, etc.) is discarded and the + // UI only shows "No access_token in exchange response". + let meta_err = resp.get("error").map(|e| e.to_string()).unwrap_or_default(); + if meta_err.is_empty() { + crate::error::TitenError::ThreadsApiError(format!( + "No access_token in exchange response (HTTP body: {resp})" + )) + } else { + crate::error::TitenError::ThreadsApiError(format!( + "No access_token in exchange response: Meta error: {meta_err}" + )) + } })? .to_string();