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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# Changelog
All notable changes to this project will be documented in this file.

## [Unreleased]
### Added
- `ErrorResponse::with_retry_after_duration` helper for specifying retry advice via `Duration`.

## [0.3.3] - 2025-09-11
### Added
- `ErrorResponse::status_code()` exposing validated `StatusCode`.
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,10 +99,11 @@ fn do_work(flag: bool) -> AppResult<()> {

~~~rust
use masterror::{AppError, AppErrorKind, AppCode, ErrorResponse};
use std::time::Duration;

let app_err = AppError::new(AppErrorKind::Unauthorized, "Token expired");
let resp: ErrorResponse = (&app_err).into()
.with_retry_after_secs(30)
.with_retry_after_duration(Duration::from_secs(30))
.with_www_authenticate(r#"Bearer realm="api", error="invalid_token""#);

assert_eq!(resp.status, 401);
Expand Down Expand Up @@ -244,7 +245,7 @@ assert_eq!(app.kind, AppErrorKind::RateLimited);
<summary><b>Migration 0.2 → 0.3</b></summary>

- Use `ErrorResponse::new(status, AppCode::..., "msg")` instead of legacy
- New helpers: `.with_retry_after_secs`, `.with_www_authenticate`
- New helpers: `.with_retry_after_secs`, `.with_retry_after_duration`, `.with_www_authenticate`
- `ErrorResponse::new_legacy` is temporary shim

</details>
Expand Down
49 changes: 45 additions & 4 deletions src/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
//! (`serde_json::Value` if the `serde_json` feature is enabled, otherwise
//! plain text)
//! - [`retry`](ErrorResponse::retry): optional retry advice, rendered as the
//! `Retry-After` header in HTTP adapters
//! `Retry-After` header in HTTP adapters; set via
//! [`with_retry_after_secs`](ErrorResponse::with_retry_after_secs) or
//! [`with_retry_after_duration`](ErrorResponse::with_retry_after_duration)
//! - [`www_authenticate`](ErrorResponse::www_authenticate): optional
//! authentication challenge string, rendered as the `WWW-Authenticate` header
//!
Expand All @@ -27,11 +29,13 @@
//! # Example
//!
//! ```rust
//! use std::time::Duration;
//!
//! use masterror::{AppCode, ErrorResponse};
//!
//! let resp = ErrorResponse::new(404, AppCode::NotFound, "User not found")
//! .expect("status")
//! .with_retry_after_secs(30);
//! .with_retry_after_duration(Duration::from_secs(30));
//! ```
//!
//! With `serde_json` enabled:
Expand All @@ -55,7 +59,10 @@
//! stable machine-readable code. A temporary [`ErrorResponse::new_legacy`] is
//! provided as a deprecated shim.

use std::fmt::{Display, Formatter, Result as FmtResult};
use std::{
fmt::{Display, Formatter, Result as FmtResult},
time::Duration
};

use http::StatusCode;
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -155,7 +162,9 @@ impl ErrorResponse {

/// Attach retry advice (number of seconds).
///
/// When present, integrations set the `Retry-After` header automatically.
/// See [`with_retry_after_duration`](Self::with_retry_after_duration) for
/// using a [`Duration`]. When present, integrations set the `Retry-After`
/// header automatically.
#[must_use]
pub fn with_retry_after_secs(mut self, secs: u64) -> Self {
self.retry = Some(RetryAdvice {
Expand All @@ -164,6 +173,28 @@ impl ErrorResponse {
self
}

/// Attach retry advice as a [`Duration`].
///
/// Equivalent to [`with_retry_after_secs`](Self::with_retry_after_secs).
/// When present, integrations set the `Retry-After` header automatically.
///
/// # Examples
///
/// ```rust
/// use std::time::Duration;
///
/// use masterror::{AppCode, ErrorResponse};
///
/// let resp = ErrorResponse::new(503, AppCode::Internal, "retry later")
/// .expect("status")
/// .with_retry_after_duration(Duration::from_secs(60));
/// assert_eq!(resp.retry.expect("retry").after_seconds, 60);
/// ```
#[must_use]
pub fn with_retry_after_duration(self, dur: Duration) -> Self {
self.with_retry_after_secs(dur.as_secs())
}

/// Attach an authentication challenge string.
///
/// When present, integrations set the `WWW-Authenticate` header
Expand Down Expand Up @@ -368,6 +399,16 @@ mod tests {
assert_eq!(e.www_authenticate.as_deref(), Some(r#"Bearer realm="api""#));
}

#[test]
fn with_retry_after_duration_attaches_advice() {
use std::time::Duration;

let e = ErrorResponse::new(429, AppCode::RateLimited, "slow down")
.expect("status")
.with_retry_after_duration(Duration::from_secs(42));
assert_eq!(e.retry.unwrap().after_seconds, 42);
}

#[test]
fn status_code_maps_invalid_to_internal_server_error() {
use http::StatusCode;
Expand Down
Loading