1use alloc::string::String;
6use alloc::vec::Vec;
7use alloc::format;
8use core::fmt;
9
10use azul_css::{AzString, U8Vec, impl_vec, impl_vec_clone, impl_vec_debug, impl_vec_partialeq, impl_vec_mut, impl_option, impl_option_inner};
11
12#[derive(Debug, Clone, PartialEq, Eq)]
18#[repr(C)]
19pub struct HttpStatusError {
20 pub status_code: u16,
22 pub message: AzString,
24}
25
26#[derive(Copy, Debug, Clone, PartialEq, Eq)]
28#[repr(C)]
29pub struct HttpResponseTooLargeError {
30 pub max_size: u64,
32 pub actual_size: u64,
34}
35
36#[derive(Debug, Clone, PartialEq, Eq)]
38#[repr(C, u8)]
39pub enum HttpError {
40 InvalidUrl(AzString),
42 ConnectionFailed(AzString),
44 Timeout,
46 TlsError(AzString),
48 HttpStatus(HttpStatusError),
50 IoError(AzString),
52 ResponseTooLarge(HttpResponseTooLargeError),
54 Other(AzString),
56}
57
58impl HttpError {
59 #[must_use] pub const fn invalid_url(url: AzString) -> Self {
60 Self::InvalidUrl(url)
61 }
62
63 #[must_use] pub const fn connection_failed(msg: AzString) -> Self {
64 Self::ConnectionFailed(msg)
65 }
66
67 #[must_use] pub const fn tls_error(msg: AzString) -> Self {
68 Self::TlsError(msg)
69 }
70
71 #[must_use] pub const fn http_status(status_code: u16, message: AzString) -> Self {
72 Self::HttpStatus(HttpStatusError {
73 status_code,
74 message,
75 })
76 }
77
78 #[must_use] pub const fn io_error(msg: AzString) -> Self {
79 Self::IoError(msg)
80 }
81
82 #[must_use] pub const fn response_too_large(max_size: u64, actual_size: u64) -> Self {
83 Self::ResponseTooLarge(HttpResponseTooLargeError {
84 max_size,
85 actual_size,
86 })
87 }
88
89 #[must_use] pub const fn other(msg: AzString) -> Self {
90 Self::Other(msg)
91 }
92}
93
94impl fmt::Display for HttpError {
95 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96 match self {
97 Self::InvalidUrl(url) => write!(f, "Invalid URL: {}", url.as_str()),
98 Self::ConnectionFailed(msg) => write!(f, "Connection failed: {}", msg.as_str()),
99 Self::Timeout => write!(f, "Request timed out"),
100 Self::TlsError(msg) => write!(f, "TLS error: {}", msg.as_str()),
101 Self::HttpStatus(e) => write!(f, "HTTP {} - {}", e.status_code, e.message.as_str()),
102 Self::IoError(msg) => write!(f, "I/O error: {}", msg.as_str()),
103 Self::ResponseTooLarge(e) => {
104 write!(f, "Response too large: {} bytes (max: {})", e.actual_size, e.max_size)
105 }
106 Self::Other(msg) => write!(f, "HTTP error: {}", msg.as_str()),
107 }
108 }
109}
110
111#[cfg(feature = "std")]
112impl std::error::Error for HttpError {}
113
114pub type HttpResult<T> = Result<T, HttpError>;
116
117use azul_css::{impl_result, impl_result_inner};
119
120#[derive(Debug, Clone, PartialEq, Eq)]
128#[repr(C)]
129pub struct HttpHeader {
130 pub name: AzString,
132 pub value: AzString,
134}
135
136impl HttpHeader {
137 pub fn new(name: impl Into<String>, value: impl Into<String>) -> Self {
138 Self {
139 name: AzString::from(name.into()),
140 value: AzString::from(value.into()),
141 }
142 }
143}
144
145impl_option!(HttpHeader, OptionHttpHeader, copy = false, [Debug, Clone, PartialEq, Eq]);
146impl_vec!(HttpHeader, HttpHeaderVec, HttpHeaderVecDestructor, HttpHeaderVecDestructorType, HttpHeaderVecSlice, OptionHttpHeader);
147impl_vec_clone!(HttpHeader, HttpHeaderVec, HttpHeaderVecDestructor);
148impl_vec_debug!(HttpHeader, HttpHeaderVec);
149impl_vec_partialeq!(HttpHeader, HttpHeaderVec);
150impl_vec_mut!(HttpHeader, HttpHeaderVec);
151
152#[derive(Debug, Clone)]
154#[repr(C)]
155pub struct HttpRequestConfig {
156 pub timeout_secs: u64,
158 pub max_response_size: u64,
160 pub user_agent: AzString,
162 pub headers: HttpHeaderVec,
164 pub disable_tls_cert_verification: bool,
169}
170
171impl Default for HttpRequestConfig {
172 fn default() -> Self {
173 Self {
174 timeout_secs: 30,
175 max_response_size: 100 * 1024 * 1024, user_agent: AzString::from("azul-http/1.0".to_string()),
177 headers: HttpHeaderVec::from_const_slice(&[]),
178 disable_tls_cert_verification: false,
179 }
180 }
181}
182
183impl HttpRequestConfig {
184 #[must_use] pub fn new() -> Self {
186 Self::default()
187 }
188
189 #[must_use] pub const fn with_timeout(mut self, secs: u64) -> Self {
191 self.timeout_secs = secs;
192 self
193 }
194
195 #[must_use] pub const fn with_max_size(mut self, max_bytes: u64) -> Self {
197 self.max_response_size = max_bytes;
198 self
199 }
200
201 #[must_use]
203 pub fn with_user_agent(mut self, ua: impl Into<String>) -> Self {
204 self.user_agent = AzString::from(ua.into());
205 self
206 }
207
208 #[must_use]
210 pub fn with_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
211 self.headers.push(HttpHeader::new(name, value));
212 self
213 }
214
215 #[cfg(feature = "http")]
223 pub fn http_get_default(url: AzString) -> ResultHttpResponseHttpError {
224 let config = HttpRequestConfig::default();
225 http_get_with_config(url.as_str(), &config).into()
226 }
227
228 #[cfg(not(feature = "http"))]
230 #[must_use] pub fn http_get_default(_url: AzString) -> ResultHttpResponseHttpError {
231 ResultHttpResponseHttpError::Err(HttpError::other("http feature not enabled".into()))
232 }
233
234 #[cfg(feature = "http")]
242 pub fn http_get(&self, url: AzString) -> ResultHttpResponseHttpError {
243 http_get_with_config(url.as_str(), self).into()
244 }
245
246 #[cfg(not(feature = "http"))]
248 #[must_use] pub fn http_get(&self, _url: AzString) -> ResultHttpResponseHttpError {
249 ResultHttpResponseHttpError::Err(HttpError::other("http feature not enabled".into()))
250 }
251
252 #[cfg(feature = "http")]
260 pub fn download_bytes_default(url: AzString) -> ResultU8VecHttpError {
261 download_bytes(url.as_str()).into()
262 }
263
264 #[cfg(not(feature = "http"))]
266 #[must_use] pub fn download_bytes_default(_url: AzString) -> ResultU8VecHttpError {
267 ResultU8VecHttpError::Err(HttpError::other("http feature not enabled".into()))
268 }
269
270 #[cfg(feature = "http")]
278 pub fn download_bytes(&self, url: AzString) -> ResultU8VecHttpError {
279 download_bytes_with_config(url.as_str(), self).into()
280 }
281
282 #[cfg(not(feature = "http"))]
284 #[must_use] pub fn download_bytes(&self, _url: AzString) -> ResultU8VecHttpError {
285 ResultU8VecHttpError::Err(HttpError::other("http feature not enabled".into()))
286 }
287
288 #[cfg(feature = "http")]
296 pub fn is_url_reachable(url: AzString) -> bool {
297 is_url_reachable(url.as_str())
298 }
299
300 #[cfg(not(feature = "http"))]
302 #[must_use] pub fn is_url_reachable(_url: AzString) -> bool {
303 false
304 }
305}
306
307#[derive(Debug, Clone, PartialEq)]
313#[repr(C)]
314pub struct HttpResponse {
315 pub status_code: u16,
317 pub body: U8Vec,
319 pub content_type: AzString,
321 pub content_length: u64,
323 pub headers: HttpHeaderVec,
325}
326
327impl HttpResponse {
328 #[must_use] pub const fn is_success(&self) -> bool {
330 self.status_code >= 200 && self.status_code < 300
331 }
332
333 #[must_use] pub const fn is_redirect(&self) -> bool {
335 self.status_code >= 300 && self.status_code < 400
336 }
337
338 #[must_use] pub const fn is_client_error(&self) -> bool {
340 self.status_code >= 400 && self.status_code < 500
341 }
342
343 #[must_use] pub const fn is_server_error(&self) -> bool {
345 self.status_code >= 500 && self.status_code < 600
346 }
347
348 #[must_use] pub fn body_as_string(&self) -> Option<AzString> {
350 core::str::from_utf8(self.body.as_slice())
351 .ok()
352 .map(|s| AzString::from(s.to_string()))
353 }
354}
355
356impl_result!(
358 HttpResponse,
359 HttpError,
360 ResultHttpResponseHttpError,
361 copy = false,
362 clone = false,
363 [Debug, Clone, PartialEq]
364);
365
366impl_result!(
367 U8Vec,
368 HttpError,
369 ResultU8VecHttpError,
370 copy = false,
371 clone = false,
372 [Debug, Clone, PartialEq, Eq]
373);
374
375#[cfg(feature = "http")]
383pub fn http_get(url: &str) -> HttpResult<HttpResponse> {
384 http_get_with_config(url, &HttpRequestConfig::default())
385}
386
387#[cfg(not(feature = "http"))]
389pub fn http_get(_url: &str) -> HttpResult<HttpResponse> {
393 Err(HttpError::other("http feature not enabled".into()))
394}
395
396#[cfg(feature = "http")]
405fn make_agent(timeout_secs: u64, disable_tls_cert_verification: bool) -> ureq::Agent {
406 use std::time::Duration;
407
408 let mut tls_builder = ureq::tls::TlsConfig::builder()
409 .provider(ureq::tls::TlsProvider::Rustls)
410 .unversioned_rustls_crypto_provider(
411 std::sync::Arc::new(rustls_rustcrypto::provider())
412 );
413
414 if disable_tls_cert_verification {
415 tls_builder = tls_builder.disable_verification(true);
416 } else {
417 tls_builder = tls_builder.root_certs(ureq::tls::RootCerts::WebPki);
418 }
419
420 let tls_config = tls_builder.build();
421
422 ureq::Agent::config_builder()
423 .tls_config(tls_config)
424 .timeout_global(Some(Duration::from_secs(timeout_secs)))
425 .http_status_as_error(false)
426 .build()
427 .new_agent()
428}
429
430#[cfg(feature = "http")]
431pub fn http_get_with_config(url: &str, config: &HttpRequestConfig) -> HttpResult<HttpResponse> {
432 use std::io::Read;
433
434 let agent = make_agent(config.timeout_secs, config.disable_tls_cert_verification);
435
436 let mut request = agent.get(url);
438
439 if !config.user_agent.as_str().is_empty() {
441 request = request.header("User-Agent", config.user_agent.as_str());
442 }
443
444 for header in config.headers.as_slice() {
446 request = request.header(header.name.as_str(), header.value.as_str());
447 }
448
449 let response = request.call().map_err(|e| {
451 match &e {
452 ureq::Error::Timeout(_) => HttpError::Timeout,
453 ureq::Error::HostNotFound => HttpError::connection_failed(
454 format!("DNS resolution failed for {}", url).into(),
455 ),
456 ureq::Error::ConnectionFailed => HttpError::connection_failed(
457 format!("Connection failed: {}", url).into(),
458 ),
459 ureq::Error::Io(io_err) => HttpError::io_error(
460 format!("{}", io_err).into(),
461 ),
462 ureq::Error::BadUri(msg) => HttpError::invalid_url(
463 format!("{}: {}", url, msg).into(),
464 ),
465 ureq::Error::Tls(msg) => HttpError::tls_error(
466 format!("TLS error: {}", msg).into(),
467 ),
468 _ => {
470 let msg = e.to_string();
471 if msg.starts_with("rustls:") || msg.contains("TLS") || msg.contains("certificate") {
472 HttpError::tls_error(msg.into())
473 } else {
474 HttpError::other(msg.into())
475 }
476 }
477 }
478 })?;
479
480 let status_code = response.status().as_u16();
481 let content_type = AzString::from(
482 response.headers().get("Content-Type")
483 .and_then(|v| v.to_str().ok())
484 .unwrap_or("application/octet-stream")
485 .to_string()
486 );
487 let content_length = response.headers().get("Content-Length")
488 .and_then(|v| v.to_str().ok())
489 .and_then(|s| s.parse::<u64>().ok())
490 .unwrap_or(0);
491
492 let mut headers = Vec::new();
494 for (name, value) in response.headers().iter() {
495 if let Ok(v) = value.to_str() {
496 headers.push(HttpHeader::new(name.to_string(), v.to_string()));
497 }
498 }
499
500 if config.max_response_size > 0 && content_length > config.max_response_size {
502 return Err(HttpError::response_too_large(
503 config.max_response_size,
504 content_length,
505 ));
506 }
507
508 let mut body = Vec::new();
510 let limit = if config.max_response_size > 0 {
511 config.max_response_size as usize
512 } else {
513 usize::MAX
514 };
515 let mut body_reader = response.into_body();
516 let mut reader = body_reader.as_reader().take(limit as u64);
517 reader.read_to_end(&mut body).map_err(|e| HttpError::io_error(e.to_string().into()))?;
518
519 Ok(HttpResponse {
520 status_code,
521 body: U8Vec::from(body),
522 content_type,
523 content_length,
524 headers: HttpHeaderVec::from_vec(headers),
525 })
526}
527
528#[cfg(not(feature = "http"))]
530pub fn http_get_with_config(_url: &str, _config: &HttpRequestConfig) -> HttpResult<HttpResponse> {
534 Err(HttpError::other("http feature not enabled".into()))
535}
536
537#[cfg(feature = "http")]
545pub fn download_bytes(url: &str) -> HttpResult<U8Vec> {
546 download_bytes_with_config(url, &HttpRequestConfig::default())
547}
548
549#[cfg(not(feature = "http"))]
551pub fn download_bytes(_url: &str) -> HttpResult<U8Vec> {
555 Err(HttpError::other("http feature not enabled".into()))
556}
557
558#[cfg(feature = "http")]
567pub fn download_bytes_with_config(url: &str, config: &HttpRequestConfig) -> HttpResult<U8Vec> {
568 let response = http_get_with_config(url, config)?;
569
570 if response.status_code >= 400 {
572 return Err(HttpError::http_status(
573 response.status_code,
574 format!("HTTP error {}", response.status_code).into(),
575 ));
576 }
577
578 Ok(response.body)
579}
580
581#[cfg(not(feature = "http"))]
583pub fn download_bytes_with_config(_url: &str, _config: &HttpRequestConfig) -> HttpResult<U8Vec> {
587 Err(HttpError::other("http feature not enabled".into()))
588}
589
590#[cfg(feature = "http")]
598pub fn is_url_reachable(url: &str) -> bool {
599 const REACHABILITY_TIMEOUT_SECS: u64 = 10;
600 let agent = make_agent(REACHABILITY_TIMEOUT_SECS, false);
601 match agent.head(url).call() {
602 Ok(resp) => {
603 let code = resp.status().as_u16();
604 code >= 200 && code < 300
605 }
606 Err(_) => false,
607 }
608}
609
610#[cfg(not(feature = "http"))]
612#[must_use] pub const fn is_url_reachable(_url: &str) -> bool {
613 false
614}
615
616#[cfg(test)]
617mod tests {
618 use super::*;
619
620 #[test]
621 fn test_http_request_config_default() {
622 let config = HttpRequestConfig::default();
623 assert_eq!(config.timeout_secs, 30);
624 assert_eq!(config.max_response_size, 100 * 1024 * 1024);
625 assert!(!config.user_agent.as_str().is_empty());
626 }
627
628 #[test]
629 fn test_http_response_status_checks() {
630 let response = HttpResponse {
631 status_code: 200,
632 body: U8Vec::from(Vec::new()),
633 content_type: AzString::from(String::new()),
634 content_length: 0,
635 headers: HttpHeaderVec::from_const_slice(&[]),
636 };
637 assert!(response.is_success());
638 assert!(!response.is_redirect());
639 assert!(!response.is_client_error());
640 assert!(!response.is_server_error());
641 }
642
643 #[test]
644 fn test_http_error_constructors() {
645 let err = HttpError::http_status(404, "Not Found".into());
646 assert!(err.to_string().contains("404"));
647
648 let err2 = HttpError::response_too_large(100, 200);
649 assert!(err2.to_string().contains("200"));
650 }
651}