Skip to main content

nano_get/
lib.rs

1#![warn(missing_docs)]
2
3//! A tiny `HTTP/1.1` client for `GET` and `HEAD`.
4//!
5//! The default build has zero external dependencies. Enable the `https` feature flag to add
6//! HTTPS support via the system OpenSSL library.
7//!
8//! ## Compliance Scope
9//!
10//! The crate’s compliance claim covers all client-applicable RFC 9110, RFC 9111, and RFC 9112
11//! requirements for an HTTP/1.1 `GET`/`HEAD` user agent, within the documented claim boundary.
12//!
13//! Auditable artifacts in the repository:
14//! - `docs/compliance/http11-get-head-rfc-matrix.md`
15//! - `docs/compliance/http11-get-head-requirement-test-index.md`
16//!
17//! ## Helper API
18//!
19//! ```no_run
20//! let body = nano_get::get("http://example.com")?;
21//! # Ok::<(), nano_get::NanoGetError>(())
22//! ```
23//!
24//! ## Advanced API
25//!
26//! ```no_run
27//! let client = nano_get::Client::builder()
28//!     .connection_policy(nano_get::ConnectionPolicy::Reuse)
29//!     .cache_mode(nano_get::CacheMode::Memory)
30//!     .basic_auth("user", "pass")
31//!     .build();
32//! let response = client.execute(
33//!     nano_get::Request::get("http://example.com")?
34//!         .with_redirect_policy(nano_get::RedirectPolicy::follow(5)),
35//! )?;
36//! assert!(response.status_code >= 200);
37//! # Ok::<(), nano_get::NanoGetError>(())
38//! ```
39//!
40//! ## Custom Authentication
41//!
42//! ```no_run
43//! use std::sync::Arc;
44//!
45//! struct TokenAuth;
46//!
47//! impl nano_get::AuthHandler for TokenAuth {
48//!     fn respond(
49//!         &self,
50//!         _target: nano_get::AuthTarget,
51//!         _url: &nano_get::Url,
52//!         challenges: &[nano_get::Challenge],
53//!         _request: &nano_get::Request,
54//!         _response: &nano_get::Response,
55//!     ) -> Result<nano_get::AuthDecision, nano_get::NanoGetError> {
56//!         if challenges
57//!             .iter()
58//!             .any(|challenge| challenge.scheme.eq_ignore_ascii_case("token"))
59//!         {
60//!             return Ok(nano_get::AuthDecision::UseHeaders(vec![
61//!                 nano_get::Header::new("Authorization", "Token secret")?,
62//!             ]));
63//!         }
64//!
65//!         Ok(nano_get::AuthDecision::NoMatch)
66//!     }
67//! }
68//!
69//! let client = nano_get::Client::builder()
70//!     .auth_handler(Arc::new(TokenAuth))
71//!     .build();
72//! let response = client.execute(nano_get::Request::get("http://example.com/protected")?)?;
73//! assert!(response.status_code >= 200);
74//! # Ok::<(), nano_get::NanoGetError>(())
75//! ```
76
77pub use auth::{AuthDecision, AuthHandler, AuthParam, AuthTarget, Challenge};
78pub use client::{
79    CacheMode, Client, ClientBuilder, ConnectionPolicy, ParserStrictness, ProxyConfig, Session,
80};
81pub use errors::NanoGetError;
82pub use request::{Header, Method, RedirectPolicy, Request};
83pub use response::{HttpVersion, Response};
84pub use url::{ToUrl, Url};
85
86mod auth;
87mod client;
88mod date;
89mod errors;
90mod http;
91#[cfg(feature = "https")]
92mod https;
93mod request;
94mod response;
95mod url;
96
97const DEFAULT_REDIRECT_LIMIT: usize = 10;
98
99/// Performs a `GET` request and returns the response body as UTF-8 text.
100///
101/// This helper:
102/// - accepts either `http://` or `https://` URLs
103/// - follows redirects up to 10 hops
104/// - returns `NanoGetError::InvalidUtf8` if the body is not valid UTF-8
105///
106/// For binary payloads, use [`get_bytes`] instead.
107pub fn get<U: ToUrl>(url: U) -> Result<String, NanoGetError> {
108    helper_client().get(url)
109}
110
111/// Performs a `GET` request and returns the response body as raw bytes.
112///
113/// This helper:
114/// - accepts either `http://` or `https://` URLs
115/// - follows redirects up to 10 hops
116pub fn get_bytes<U: ToUrl>(url: U) -> Result<Vec<u8>, NanoGetError> {
117    let request =
118        Request::get(url)?.with_redirect_policy(RedirectPolicy::follow(DEFAULT_REDIRECT_LIMIT));
119    helper_client()
120        .execute(request)
121        .map(|response| response.body)
122}
123
124/// Performs a `HEAD` request and returns the full response metadata.
125///
126/// The returned [`Response`] always has an empty `body` for `HEAD`, even when a server
127/// incorrectly sends bytes on the wire.
128pub fn head<U: ToUrl>(url: U) -> Result<Response, NanoGetError> {
129    let request =
130        Request::head(url)?.with_redirect_policy(RedirectPolicy::follow(DEFAULT_REDIRECT_LIMIT));
131    helper_client().execute(request)
132}
133
134/// Performs a `GET` request using HTTP only and returns UTF-8 text.
135///
136/// Returns [`NanoGetError::UnsupportedScheme`] if the URL is not `http://`.
137pub fn get_http<U: ToUrl>(url: U) -> Result<String, NanoGetError> {
138    get_http_bytes(url)
139        .and_then(|body| String::from_utf8(body).map_err(|error| error.utf8_error().into()))
140}
141
142/// Performs a `GET` request using HTTP only and returns raw bytes.
143///
144/// Returns [`NanoGetError::UnsupportedScheme`] if the URL is not `http://`.
145pub fn get_http_bytes<U: ToUrl>(url: U) -> Result<Vec<u8>, NanoGetError> {
146    let request =
147        Request::get(url)?.with_redirect_policy(RedirectPolicy::follow(DEFAULT_REDIRECT_LIMIT));
148    if !request.url().is_http() {
149        return Err(NanoGetError::UnsupportedScheme(
150            request.url().scheme.clone(),
151        ));
152    }
153    helper_client()
154        .execute(request)
155        .map(|response| response.body)
156}
157
158/// Performs a `HEAD` request using HTTP only.
159///
160/// Returns [`NanoGetError::UnsupportedScheme`] if the URL is not `http://`.
161pub fn head_http<U: ToUrl>(url: U) -> Result<Response, NanoGetError> {
162    let request =
163        Request::head(url)?.with_redirect_policy(RedirectPolicy::follow(DEFAULT_REDIRECT_LIMIT));
164    if !request.url().is_http() {
165        return Err(NanoGetError::UnsupportedScheme(
166            request.url().scheme.clone(),
167        ));
168    }
169    helper_client().execute(request)
170}
171
172/// Performs a `GET` request using HTTPS only and returns UTF-8 text.
173///
174/// Available only with the `https` feature.
175///
176/// Returns [`NanoGetError::UnsupportedScheme`] if the URL is not `https://`.
177#[cfg(feature = "https")]
178pub fn get_https<U: ToUrl>(url: U) -> Result<String, NanoGetError> {
179    get_https_bytes(url)
180        .and_then(|body| String::from_utf8(body).map_err(|error| error.utf8_error().into()))
181}
182
183/// Performs a `GET` request using HTTPS only and returns raw bytes.
184///
185/// Available only with the `https` feature.
186///
187/// Returns [`NanoGetError::UnsupportedScheme`] if the URL is not `https://`.
188#[cfg(feature = "https")]
189pub fn get_https_bytes<U: ToUrl>(url: U) -> Result<Vec<u8>, NanoGetError> {
190    let request =
191        Request::get(url)?.with_redirect_policy(RedirectPolicy::follow(DEFAULT_REDIRECT_LIMIT));
192    if !request.url().is_https() {
193        return Err(NanoGetError::UnsupportedScheme(
194            request.url().scheme.clone(),
195        ));
196    }
197    helper_client()
198        .execute(request)
199        .map(|response| response.body)
200}
201
202/// Performs a `HEAD` request using HTTPS only.
203///
204/// Available only with the `https` feature.
205///
206/// Returns [`NanoGetError::UnsupportedScheme`] if the URL is not `https://`.
207#[cfg(feature = "https")]
208pub fn head_https<U: ToUrl>(url: U) -> Result<Response, NanoGetError> {
209    let request =
210        Request::head(url)?.with_redirect_policy(RedirectPolicy::follow(DEFAULT_REDIRECT_LIMIT));
211    if !request.url().is_https() {
212        return Err(NanoGetError::UnsupportedScheme(
213            request.url().scheme.clone(),
214        ));
215    }
216    helper_client().execute(request)
217}
218
219fn helper_client() -> Client {
220    Client::builder()
221        .redirect_policy(RedirectPolicy::follow(DEFAULT_REDIRECT_LIMIT))
222        .build()
223}
224
225#[cfg(test)]
226mod tests {
227    use crate::client::{CacheMode, Client, ConnectionPolicy, ParserStrictness};
228    use crate::{get_http_bytes, Method, RedirectPolicy, Request, Url};
229
230    #[test]
231    fn request_constructors_work() {
232        let request = Request::new(Method::Get, "http://example.com").unwrap();
233        assert_eq!(request.method(), Method::Get);
234    }
235
236    #[test]
237    fn default_url_scheme_is_http() {
238        let url = Url::parse("example.com").unwrap();
239        assert!(url.is_http());
240    }
241
242    #[test]
243    fn helper_requests_follow_redirects_by_default() {
244        let request = Request::get("http://example.com")
245            .unwrap()
246            .with_redirect_policy(RedirectPolicy::follow(10));
247        assert_eq!(request.redirect_policy(), RedirectPolicy::follow(10));
248    }
249
250    #[test]
251    fn http_only_helpers_reject_https_urls() {
252        let error = get_http_bytes("https://example.com").unwrap_err();
253        assert!(matches!(error, crate::NanoGetError::UnsupportedScheme(_)));
254    }
255
256    #[test]
257    fn http_only_text_and_head_helpers_reject_https_urls() {
258        let error = crate::get_http("https://example.com").unwrap_err();
259        assert!(matches!(error, crate::NanoGetError::UnsupportedScheme(_)));
260
261        let error = crate::head_http("https://example.com").unwrap_err();
262        assert!(matches!(error, crate::NanoGetError::UnsupportedScheme(_)));
263    }
264
265    #[test]
266    fn http_only_helpers_execute_http_paths() {
267        let error = get_http_bytes("http://127.0.0.1:9").unwrap_err();
268        assert!(matches!(
269            error,
270            crate::NanoGetError::Connect(_) | crate::NanoGetError::Io(_)
271        ));
272
273        let error = crate::head_http("http://127.0.0.1:9").unwrap_err();
274        assert!(matches!(
275            error,
276            crate::NanoGetError::Connect(_) | crate::NanoGetError::Io(_)
277        ));
278    }
279
280    #[cfg(feature = "https")]
281    #[test]
282    fn https_only_helpers_reject_http_urls() {
283        let error = crate::get_https("http://example.com").unwrap_err();
284        assert!(matches!(error, crate::NanoGetError::UnsupportedScheme(_)));
285
286        let error = crate::get_https_bytes("http://example.com").unwrap_err();
287        assert!(matches!(error, crate::NanoGetError::UnsupportedScheme(_)));
288
289        let error = crate::head_https("http://example.com").unwrap_err();
290        assert!(matches!(error, crate::NanoGetError::UnsupportedScheme(_)));
291    }
292
293    #[cfg(feature = "https")]
294    #[test]
295    fn https_only_helpers_execute_https_paths() {
296        let error = crate::get_https_bytes("https://127.0.0.1:9").unwrap_err();
297        assert!(matches!(
298            error,
299            crate::NanoGetError::Connect(_)
300                | crate::NanoGetError::Io(_)
301                | crate::NanoGetError::Tls(_)
302        ));
303    }
304
305    #[test]
306    fn client_builder_is_available() {
307        let _client = Client::builder()
308            .connection_policy(ConnectionPolicy::Reuse)
309            .cache_mode(CacheMode::Memory)
310            .parser_strictness(ParserStrictness::Lenient)
311            .build();
312    }
313}