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