pray_core/
registry_http.rs1use 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_put_with_headers(url, content_type, body, &[])
88}
89
90pub fn http_put_with_headers(
91 url: &str,
92 content_type: &str,
93 body: &[u8],
94 headers: &[(&str, &str)],
95) -> PrayResult<HttpResponse> {
96 http_request("PUT", url, Some(content_type), Some(body), headers)
97}
98
99fn http_request(
100 method: &str,
101 url: &str,
102 content_type: Option<&str>,
103 body: Option<&[u8]>,
104 headers: &[(&str, &str)],
105) -> PrayResult<HttpResponse> {
106 ensure_http_url(url)?;
107 let client = http_client()?;
108 let mut request = match method {
109 "GET" => client.get(url),
110 "POST" => client.post(url),
111 "PUT" => client.put(url),
112 other => {
113 return Err(PrayError::Unsupported(format!(
114 "unsupported HTTP method: {other}"
115 )))
116 }
117 };
118 for (name, value) in headers {
119 request = request.header(*name, *value);
120 }
121 if let Some(content_type) = content_type {
122 request = request.header(reqwest::header::CONTENT_TYPE, content_type);
123 }
124 if let Some(body) = body {
125 request = request.body(body.to_vec());
126 }
127 let response = request
128 .send()
129 .map_err(|error| PrayError::Network(error.to_string()))?;
130 let status = response.status().as_u16();
131 let body = read_response_body(response)?;
132 Ok(HttpResponse { status, body })
133}