Skip to main content

pray_core/
registry_http.rs

1use crate::resource_limits::MAX_HTTP_RESPONSE_BYTES;
2use crate::{PrayError, PrayResult};
3use std::io::Read;
4use std::sync::OnceLock;
5use std::time::Duration;
6
7pub struct HttpResponse {
8    pub status: u16,
9    pub body: Vec<u8>,
10}
11
12pub fn join_url(base: &str, path: &str) -> String {
13    format!(
14        "{}/{}",
15        base.trim_end_matches('/'),
16        path.trim_start_matches('/')
17    )
18}
19
20fn http_client() -> PrayResult<&'static reqwest::blocking::Client> {
21    static CLIENT: OnceLock<reqwest::blocking::Client> = OnceLock::new();
22    if let Some(client) = CLIENT.get() {
23        return Ok(client);
24    }
25    let client = reqwest::blocking::Client::builder()
26        .timeout(Duration::from_secs(60))
27        .redirect(reqwest::redirect::Policy::limited(5))
28        .build()
29        .map_err(|error| PrayError::Network(error.to_string()))?;
30    let _ = CLIENT.set(client);
31    CLIENT
32        .get()
33        .ok_or_else(|| PrayError::Network("HTTP client unavailable".to_string()))
34}
35
36fn ensure_http_url(url: &str) -> PrayResult<()> {
37    if url.starts_with("http://") || url.starts_with("https://") {
38        Ok(())
39    } else {
40        Err(PrayError::Unsupported(format!(
41            "unsupported URL scheme: {url}"
42        )))
43    }
44}
45
46fn read_response_body(response: reqwest::blocking::Response) -> PrayResult<Vec<u8>> {
47    let length = response.content_length();
48    if length.is_some_and(|value| value > MAX_HTTP_RESPONSE_BYTES) {
49        return Err(PrayError::Network(format!(
50            "HTTP response exceeds {MAX_HTTP_RESPONSE_BYTES} bytes"
51        )));
52    }
53    let mut body = Vec::new();
54    let mut limited = response.take(MAX_HTTP_RESPONSE_BYTES.saturating_add(1));
55    limited
56        .read_to_end(&mut body)
57        .map_err(|error| PrayError::Network(error.to_string()))?;
58    if body.len() as u64 > MAX_HTTP_RESPONSE_BYTES {
59        return Err(PrayError::Network(format!(
60            "HTTP response exceeds {MAX_HTTP_RESPONSE_BYTES} bytes"
61        )));
62    }
63    Ok(body)
64}
65
66pub fn http_get(url: &str) -> PrayResult<Vec<u8>> {
67    let response = http_request("GET", url, None, None, &[])?;
68    if response.status / 100 != 2 {
69        return Err(PrayError::Resolution(format!(
70            "GET {url} failed with HTTP {}",
71            response.status
72        )));
73    }
74    Ok(response.body)
75}
76
77pub fn http_get_with_headers(url: &str, headers: &[(&str, &str)]) -> PrayResult<(Vec<u8>, u16)> {
78    let response = http_request("GET", url, None, None, headers)?;
79    Ok((response.body, response.status))
80}
81
82pub fn http_post(url: &str, content_type: &str, body: &[u8]) -> PrayResult<HttpResponse> {
83    http_request("POST", url, Some(content_type), Some(body), &[])
84}
85
86pub fn http_put(url: &str, content_type: &str, body: &[u8]) -> PrayResult<HttpResponse> {
87    http_request("PUT", url, Some(content_type), Some(body), &[])
88}
89
90fn http_request(
91    method: &str,
92    url: &str,
93    content_type: Option<&str>,
94    body: Option<&[u8]>,
95    headers: &[(&str, &str)],
96) -> PrayResult<HttpResponse> {
97    ensure_http_url(url)?;
98    let client = http_client()?;
99    let mut request = match method {
100        "GET" => client.get(url),
101        "POST" => client.post(url),
102        "PUT" => client.put(url),
103        other => {
104            return Err(PrayError::Unsupported(format!(
105                "unsupported HTTP method: {other}"
106            )))
107        }
108    };
109    for (name, value) in headers {
110        request = request.header(*name, *value);
111    }
112    if let Some(content_type) = content_type {
113        request = request.header(reqwest::header::CONTENT_TYPE, content_type);
114    }
115    if let Some(body) = body {
116        request = request.body(body.to_vec());
117    }
118    let response = request
119        .send()
120        .map_err(|error| PrayError::Network(error.to_string()))?;
121    let status = response.status().as_u16();
122    let body = read_response_body(response)?;
123    Ok(HttpResponse { status, body })
124}