Skip to main content

agentic_tools_utils/
http.rs

1//! Neutral HTTP error and status classifiers.
2//!
3//! This module provides utilities for classifying HTTP responses
4//! in a consistent, transport-agnostic way.
5
6use reqwest::Error as ReqwestError;
7use reqwest::StatusCode;
8
9/// Classification of HTTP errors.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum HttpKind {
12    /// 401 Unauthorized
13    Unauthorized,
14    /// 403 Forbidden
15    Forbidden,
16    /// 404 Not Found
17    NotFound,
18    /// 429 Too Many Requests
19    RateLimited,
20    /// Other 4xx errors
21    ClientError,
22    /// 5xx errors
23    ServerError,
24    /// Request timeout
25    Timeout,
26    /// Network/connection error
27    Network,
28    /// Unknown or unclassified error
29    Unknown,
30}
31
32/// Summary of an HTTP error for consistent handling.
33#[derive(Debug, Clone)]
34pub struct HttpErrorSummary {
35    /// The classified error kind
36    pub kind: HttpKind,
37    /// HTTP status code if available
38    pub status: Option<u16>,
39    /// Human-readable error message
40    pub message: String,
41}
42
43/// Classify an HTTP status code.
44pub fn classify_status(status: StatusCode) -> HttpKind {
45    match status {
46        StatusCode::UNAUTHORIZED => HttpKind::Unauthorized,
47        StatusCode::FORBIDDEN => HttpKind::Forbidden,
48        StatusCode::NOT_FOUND => HttpKind::NotFound,
49        StatusCode::TOO_MANY_REQUESTS => HttpKind::RateLimited,
50        s if s.is_client_error() => HttpKind::ClientError,
51        s if s.is_server_error() => HttpKind::ServerError,
52        _ => HttpKind::Unknown,
53    }
54}
55
56/// Summarize a reqwest error into a consistent format.
57pub fn summarize_reqwest_error(err: &ReqwestError) -> HttpErrorSummary {
58    if err.is_timeout() {
59        return HttpErrorSummary {
60            kind: HttpKind::Timeout,
61            status: None,
62            message: err.to_string(),
63        };
64    }
65
66    if err.is_connect() {
67        return HttpErrorSummary {
68            kind: HttpKind::Network,
69            status: None,
70            message: err.to_string(),
71        };
72    }
73
74    if let Some(status) = err.status() {
75        let kind = classify_status(status);
76        return HttpErrorSummary {
77            kind,
78            status: Some(status.as_u16()),
79            message: err.to_string(),
80        };
81    }
82
83    HttpErrorSummary {
84        kind: HttpKind::Unknown,
85        status: None,
86        message: err.to_string(),
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    #[test]
95    fn classify_status_unauthorized() {
96        assert_eq!(
97            classify_status(StatusCode::UNAUTHORIZED),
98            HttpKind::Unauthorized
99        );
100    }
101
102    #[test]
103    fn classify_status_forbidden() {
104        assert_eq!(classify_status(StatusCode::FORBIDDEN), HttpKind::Forbidden);
105    }
106
107    #[test]
108    fn classify_status_not_found() {
109        assert_eq!(classify_status(StatusCode::NOT_FOUND), HttpKind::NotFound);
110    }
111
112    #[test]
113    fn classify_status_rate_limited() {
114        assert_eq!(
115            classify_status(StatusCode::TOO_MANY_REQUESTS),
116            HttpKind::RateLimited
117        );
118    }
119
120    #[test]
121    fn classify_status_other_client_error() {
122        assert_eq!(
123            classify_status(StatusCode::BAD_REQUEST),
124            HttpKind::ClientError
125        );
126        assert_eq!(classify_status(StatusCode::CONFLICT), HttpKind::ClientError);
127    }
128
129    #[test]
130    fn classify_status_server_error() {
131        assert_eq!(
132            classify_status(StatusCode::INTERNAL_SERVER_ERROR),
133            HttpKind::ServerError
134        );
135        assert_eq!(
136            classify_status(StatusCode::BAD_GATEWAY),
137            HttpKind::ServerError
138        );
139        assert_eq!(
140            classify_status(StatusCode::SERVICE_UNAVAILABLE),
141            HttpKind::ServerError
142        );
143    }
144
145    #[test]
146    fn classify_status_ok_is_unknown() {
147        // Success codes aren't really "errors" so they classify as Unknown
148        assert_eq!(classify_status(StatusCode::OK), HttpKind::Unknown);
149    }
150}