Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/datahub.rs
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,21 @@ impl DataHubConfig {
}
}

/// Drop the cached token so the next [`Self::get_api_token`] mints a fresh one.
///
/// Called when the server rejects a token with 401. Expiry is not the only way a token stops
/// being usable: an identity provider that is still finishing its own setup can issue one the
/// API refuses — a malformed `organization` claim, say — and that token is unusable for its
/// whole lifetime. Without this the client caches it, `is_expired()` keeps returning false,
/// and every subsequent call re-sends the same rejected token, so a caller that starts a few
/// seconds too early stays broken until the token expires rather than recovering on its next
/// attempt.
pub async fn invalidate_token(&self) {
let mut auth_state = self.auth_state.write().await;
auth_state.token = None;
auth_state.expire_time = None;
}

pub async fn get_api_token(&self) -> Result<String, DataHubError> {
{
// lock scope. read and if expired refresh token
Expand Down
42 changes: 33 additions & 9 deletions src/generic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -700,6 +700,20 @@ pub trait ApiServiceProvider {
self.api_service().upgrade().unwrap()
}

/// Post-process a failed request: drop a rejected token, then explain the failure.
///
/// A 401 means the token just sent is not usable, and expiry is not the only way that
/// happens — an identity provider still finishing its setup can issue one the API refuses
/// for its whole lifetime. Clearing it here means the next call mints a fresh one, so a
/// client that started a few seconds too early recovers on its next attempt instead of
/// re-sending the same rejected credential until it expires.
async fn on_request_error(&self, error: ResponseError, token: &str) -> ResponseError {
if error.get_status() == http::StatusCode::UNAUTHORIZED {
self.get_api_service().config.invalidate_token().await;
}
explain_auth_failure(error, token)
}

async fn get_token(&self) -> Result<String, ResponseError> {
self.get_api_service()
.config
Expand Down Expand Up @@ -744,9 +758,10 @@ pub trait ApiServiceProvider {
ResponseError::from_err(err)
})?
};
process_response::<T>(response, path)
.await
.map_err(|e| explain_auth_failure(e, &token))
match process_response::<T>(response, path).await {
Ok(value) => Ok(value),
Err(e) => Err(self.on_request_error(e, &token).await),
}
}

async fn execute_post_request<
Expand Down Expand Up @@ -780,9 +795,10 @@ pub trait ApiServiceProvider {
}
})
} else {
process_response::<T>(response, path)
.await
.map_err(|e| explain_auth_failure(e, &token))
match process_response::<T>(response, path).await {
Ok(value) => Ok(value),
Err(e) => Err(self.on_request_error(e, &token).await),
}
}
}

Expand Down Expand Up @@ -812,9 +828,10 @@ pub trait ApiServiceProvider {
eprintln!("HTTP file upload request failed: {}", err);
ResponseError::from_err(err)
})?;
process_response::<T>(response, path)
.await
.map_err(|e| explain_auth_failure(e, &token))
match process_response::<T>(response, path).await {
Ok(value) => Ok(value),
Err(e) => Err(self.on_request_error(e, &token).await),
}
}

/// `GET` an endpoint that answers with bytes rather than JSON (currently only
Expand Down Expand Up @@ -846,6 +863,13 @@ pub trait ApiServiceProvider {
if status.is_success() {
return Ok(response);
}
// A 401 means the token we just sent is not usable. Drop it so the next call mints a
// fresh one instead of re-sending the same rejected credential until it expires — the
// difference between a client that recovers on its next attempt and one that stays
// broken for the token's lifetime. See DataHubConfig::invalidate_token.
if status == http::StatusCode::UNAUTHORIZED {
self.get_api_service().config.invalidate_token().await;
}
eprintln!("Request failed with status: {status}");
Err(explain_auth_failure(
ResponseError {
Expand Down