use std::time::Duration;
#[must_use]
pub fn retry_after(status: &tonic::Status) -> Option<Duration> {
let raw = status.metadata().get("retry-after")?;
match raw.to_str().ok().and_then(|s| s.parse::<u64>().ok()) {
Some(secs) => Some(Duration::from_secs(secs)),
None => {
tracing::warn!(
retry_after = ?raw,
"gRPC `retry-after` metadata present but unparseable; \
falling back to default backoff"
);
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use tonic::metadata::MetadataValue;
fn status_with_retry_after(value: &str) -> tonic::Status {
let mut status = tonic::Status::resource_exhausted("rate limited");
if let Ok(v) = MetadataValue::try_from(value) {
status.metadata_mut().insert("retry-after", v);
}
status
}
#[test]
fn present_value_60_returns_duration() {
let status = status_with_retry_after("60");
assert_eq!(retry_after(&status), Some(Duration::from_secs(60)));
}
#[test]
fn missing_metadata_returns_none() {
let status = tonic::Status::resource_exhausted("no metadata");
assert_eq!(retry_after(&status), None);
}
#[test]
fn malformed_ascii_returns_none() {
let status = status_with_retry_after("abc");
assert_eq!(retry_after(&status), None);
}
#[test]
fn negative_string_returns_none() {
let status = status_with_retry_after("-5");
assert_eq!(retry_after(&status), None);
}
#[test]
fn zero_returns_duration_zero() {
let status = status_with_retry_after("0");
assert_eq!(retry_after(&status), Some(Duration::ZERO));
}
#[test]
fn large_value_86400_returns_duration() {
let status = status_with_retry_after("86400");
assert_eq!(retry_after(&status), Some(Duration::from_secs(86_400)));
}
}