bevy_http_client/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use bevy_app::{App, Plugin, Update};
4use bevy_derive::Deref;
5use bevy_ecs::{prelude::*, world::CommandQueue};
6use bevy_tasks::IoTaskPool;
7use crossbeam_channel::{Receiver, Sender};
8use ehttp::{Headers, Request, Response};
9
10use crate::{prelude::TypedRequest, typed::HttpObserved};
11
12pub mod prelude;
13mod typed;
14
15/// JSON serialization fallback strategy when serialization fails
16#[derive(Debug, Clone, Default)]
17pub enum JsonFallback {
18    /// Use empty object {} as fallback
19    #[default]
20    EmptyObject,
21    /// Use empty array [] as fallback  
22    EmptyArray,
23    /// Use null as fallback
24    Null,
25    /// Use custom data as fallback
26    Custom(Vec<u8>),
27}
28
29/// JSON serialization error type
30#[derive(Debug, Clone)]
31pub enum JsonSerializationError {
32    SerializationFailed {
33        message: String,
34        fallback_used: JsonFallback,
35    },
36}
37
38impl std::fmt::Display for JsonSerializationError {
39    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40        match self {
41            JsonSerializationError::SerializationFailed {
42                message,
43                fallback_used,
44            } => {
45                write!(
46                    f,
47                    "JSON serialization failed: {}, fallback: {:?}",
48                    message, fallback_used
49                )
50            }
51        }
52    }
53}
54
55impl std::error::Error for JsonSerializationError {}
56
57/// HTTP client builder error type
58#[derive(Debug, Clone)]
59pub enum HttpClientBuilderError {
60    MissingMethod,
61    MissingUrl,
62    MissingHeaders,
63}
64
65impl std::fmt::Display for HttpClientBuilderError {
66    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67        match self {
68            HttpClientBuilderError::MissingMethod => write!(f, "HTTP method is required"),
69            HttpClientBuilderError::MissingUrl => write!(f, "URL is required"),
70            HttpClientBuilderError::MissingHeaders => write!(f, "Headers are required"),
71        }
72    }
73}
74
75impl std::error::Error for HttpClientBuilderError {}
76
77/// Plugin that provides support for send http request and handle response.
78///
79/// # Example
80/// ```
81/// use bevy::prelude::*;
82/// use bevy_http_client::prelude::*;
83///
84/// let mut app = App::new();
85/// app.add_plugins(MinimalPlugins)
86///    .add_plugins(HttpClientPlugin);
87/// // Note: Don't call .run() in doctests as it starts the event loop
88/// ```
89#[derive(Default)]
90pub struct HttpClientPlugin;
91
92impl Plugin for HttpClientPlugin {
93    fn build(&self, app: &mut App) {
94        if !app.world().contains_resource::<HttpClientSetting>() {
95            app.init_resource::<HttpClientSetting>();
96        }
97        app.add_message::<HttpRequest>();
98        app.add_message::<HttpResponse>();
99        app.add_message::<HttpResponseError>();
100        app.add_systems(Update, (handle_request, handle_tasks));
101    }
102}
103
104/// The setting of http client.
105/// can set the max concurrent request.
106#[derive(Resource, Debug)]
107pub struct HttpClientSetting {
108    /// max concurrent request
109    pub client_limits: usize,
110    current_clients: usize,
111}
112
113impl Default for HttpClientSetting {
114    fn default() -> Self {
115        Self {
116            client_limits: 5,
117            current_clients: 0,
118        }
119    }
120}
121
122impl HttpClientSetting {
123    /// create a new http client setting
124    pub fn new(max_concurrent: usize) -> Self {
125        Self {
126            client_limits: max_concurrent,
127            current_clients: 0,
128        }
129    }
130
131    /// check if the client is available
132    #[inline]
133    pub fn is_available(&self) -> bool {
134        self.current_clients < self.client_limits
135    }
136}
137
138#[derive(Event, Message, Debug, Clone)]
139pub struct HttpRequest {
140    pub from_entity: Option<Entity>,
141    pub request: Request,
142}
143
144/// builder  for ehttp request
145#[derive(Component, Debug, Clone)]
146pub struct HttpClient {
147    /// The entity that the request is associated with.
148    from_entity: Option<Entity>,
149    /// "GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", …
150    method: Option<String>,
151
152    /// https://…
153    url: Option<String>,
154
155    /// The data you send with e.g. "POST".
156    body: Vec<u8>,
157
158    /// ("Accept", "*/*"), …
159    headers: Option<Headers>,
160
161    /// Request mode used on fetch. Only available on wasm builds
162    #[cfg(target_arch = "wasm32")]
163    pub mode: ehttp::Mode,
164}
165
166impl Default for HttpClient {
167    fn default() -> Self {
168        Self {
169            from_entity: None,
170            method: None,
171            url: None,
172            body: vec![],
173            headers: Some(Headers::new(&[("Accept", "*/*")])),
174            #[cfg(target_arch = "wasm32")]
175            mode: ehttp::Mode::default(),
176        }
177    }
178}
179
180impl HttpClient {
181    /// This method is used to create a new `HttpClient` instance.
182    ///
183    /// # Returns
184    ///
185    /// * `Self` - Returns the instance of the `HttpClient` struct.
186    ///
187    /// # Examples
188    ///
189    /// ```
190    /// use bevy_http_client::HttpClient;
191    /// let http_client = HttpClient::new();
192    /// ```
193    pub fn new() -> Self {
194        Self::default()
195    }
196
197    /// his method is used to create a new `HttpClient` instance with `Entity`.
198    ///
199    /// # Arguments
200    ///
201    /// * `entity`: Target Entity
202    ///
203    /// returns: HttpClient
204    ///
205    /// # Examples
206    ///
207    /// ```
208    /// use bevy_http_client::HttpClient;
209    /// use bevy_ecs::entity::Entity;
210    ///
211    /// let entity = Entity::from_raw_u32(42).unwrap(); // Example entity
212    /// let http_client = HttpClient::new_with_entity(entity);
213    /// ```
214    pub fn new_with_entity(entity: Entity) -> Self {
215        Self {
216            from_entity: Some(entity),
217            ..Default::default()
218        }
219    }
220
221    /// This method is used to create a `GET` HTTP request.
222    ///
223    /// # Arguments
224    ///
225    /// * `url` - A value that can be converted into a string. This is the URL to which the HTTP
226    ///   request will be sent.
227    ///
228    /// # Returns
229    ///
230    /// * `Self` - Returns the instance of the `HttpClient` struct, allowing for method chaining.
231    ///
232    /// # Examples
233    ///
234    /// ```
235    /// use bevy_http_client::HttpClient;
236    /// let http_client = HttpClient::new().get("http://example.com");
237    /// ```
238    pub fn get(mut self, url: impl ToString) -> Self {
239        self.method = Some("GET".to_string());
240        self.url = Some(url.to_string());
241        self
242    }
243
244    /// This method is used to create a `POST` HTTP request.
245    ///
246    /// # Arguments
247    ///
248    /// * `url` - A value that can be converted into a string. This is the URL to which the HTTP
249    ///   request will be sent.
250    ///
251    /// # Returns
252    ///
253    /// * `Self` - Returns the instance of the `HttpClient` struct, allowing for method chaining.
254    ///
255    /// # Examples
256    ///
257    /// ```
258    /// use bevy_http_client::HttpClient;
259    /// let http_client = HttpClient::new().post("http://example.com");
260    /// ```
261    pub fn post(mut self, url: impl ToString) -> Self {
262        self.method = Some("POST".to_string());
263        self.url = Some(url.to_string());
264        self
265    }
266
267    /// This method is used to create a `PUT` HTTP request.
268    ///
269    /// # Arguments
270    ///
271    /// * `url` - A value that can be converted into a string. This is the URL to which the HTTP
272    ///   request will be sent.
273    ///
274    /// # Returns
275    ///
276    /// * `Self` - Returns the instance of the `HttpClient` struct, allowing for method chaining.
277    ///
278    /// # Examples
279    ///
280    /// ```
281    /// use bevy_http_client::HttpClient;
282    /// let http_client = HttpClient::new().put("http://example.com");
283    /// ```
284    pub fn put(mut self, url: impl ToString) -> Self {
285        self.method = Some("PUT".to_string());
286        self.url = Some(url.to_string());
287        self
288    }
289
290    /// This method is used to create a `PATCH` HTTP request.
291    ///
292    /// # Arguments
293    ///
294    /// * `url` - A value that can be converted into a string. This is the URL to which the HTTP
295    ///   request will be sent.
296    ///
297    /// # Returns
298    ///
299    /// * `Self` - Returns the instance of the `HttpClient` struct, allowing for method chaining.
300    ///
301    /// # Examples
302    ///
303    /// ```
304    /// use bevy_http_client::HttpClient;
305    /// let http_client = HttpClient::new().patch("http://example.com");
306    /// ```
307    pub fn patch(mut self, url: impl ToString) -> Self {
308        self.method = Some("PATCH".to_string());
309        self.url = Some(url.to_string());
310        self
311    }
312
313    /// This method is used to create a `DELETE` HTTP request.
314    ///
315    /// # Arguments
316    ///
317    /// * `url` - A value that can be converted into a string. This is the URL to which the HTTP
318    ///   request will be sent.
319    ///
320    /// # Returns
321    ///
322    /// * `Self` - Returns the instance of the `HttpClient` struct, allowing for method chaining.
323    ///
324    /// # Examples
325    ///
326    /// ```
327    /// use bevy_http_client::HttpClient;
328    /// let http_client = HttpClient::new().delete("http://example.com");
329    /// ```
330    pub fn delete(mut self, url: impl ToString) -> Self {
331        self.method = Some("DELETE".to_string());
332        self.url = Some(url.to_string());
333        self
334    }
335
336    /// This method is used to create a `HEAD` HTTP request.
337    ///
338    /// # Arguments
339    ///
340    /// * `url` - A value that can be converted into a string. This is the URL to which the HTTP
341    ///   request will be sent.
342    ///
343    /// # Returns
344    ///
345    /// * `Self` - Returns the instance of the `HttpClient` struct, allowing for method chaining.
346    ///
347    /// # Examples
348    ///
349    /// ```
350    /// use bevy_http_client::HttpClient;
351    /// let http_client = HttpClient::new().head("http://example.com");
352    /// ```
353    pub fn head(mut self, url: impl ToString) -> Self {
354        self.method = Some("HEAD".to_string());
355        self.url = Some(url.to_string());
356        self
357    }
358
359    /// This method is used to set the headers of the HTTP request.
360    ///
361    /// # Arguments
362    ///
363    /// * `headers` - A slice of tuples where each tuple represents a header. The first element of
364    ///   the tuple is the header name and the second element is the header value.
365    ///
366    /// # Returns
367    ///
368    /// * `Self` - Returns the instance of the `HttpClient` struct, allowing for method chaining.
369    ///
370    /// # Examples
371    ///
372    /// ```
373    /// use bevy_http_client::HttpClient;
374    /// let http_client = HttpClient::new().post("http://example.com")
375    ///     .headers(&[("Content-Type", "application/json"), ("Accept", "*/*")]);
376    /// ```
377    pub fn headers(mut self, headers: &[(&str, &str)]) -> Self {
378        self.headers = Some(Headers::new(headers));
379        self
380    }
381
382    /// Safe JSON serialization method with fallback strategy
383    ///
384    /// This method safely serializes the body to JSON and sets the Content-Type header.
385    /// If serialization fails, it uses a fallback strategy instead of panicking.
386    ///
387    /// # Arguments
388    /// * `body` - Data to serialize to JSON
389    /// * `fallback` - Fallback strategy when serialization fails
390    ///
391    /// # Returns
392    /// Returns HttpClient instance for method chaining
393    ///
394    /// # Examples
395    /// ```
396    /// use bevy_http_client::{HttpClient, JsonFallback};
397    /// use serde::Serialize;
398    ///
399    /// #[derive(Serialize)]
400    /// struct MyData { name: String }
401    /// let data = MyData { name: "test".to_string() };
402    ///
403    /// let client = HttpClient::new()
404    ///     .post("http://example.com")
405    ///     .json_with_fallback(&data, JsonFallback::EmptyObject);
406    /// ```
407    pub fn json_with_fallback(
408        mut self,
409        body: &impl serde::Serialize,
410        fallback: JsonFallback,
411    ) -> Self {
412        // Set Content-Type header
413        if let Some(headers) = self.headers.as_mut() {
414            headers.insert("Content-Type".to_string(), "application/json".to_string());
415        } else {
416            self.headers = Some(Headers::new(&[
417                ("Content-Type", "application/json"),
418                ("Accept", "*/*"),
419            ]));
420        }
421
422        // Safe serialization with fallback strategy
423        self.body = match serde_json::to_vec(body) {
424            Ok(bytes) => {
425                // Check for unreasonably large payloads
426                if bytes.len() > 50 * 1024 * 1024 {
427                    // 50MB limit
428                    bevy_log::warn!(
429                        "JSON payload is very large ({} bytes), this might cause performance issues",
430                        bytes.len()
431                    );
432                }
433                bytes
434            }
435            Err(e) => {
436                // Get fallback data
437                let fallback_data = match &fallback {
438                    JsonFallback::EmptyObject => b"{}".to_vec(),
439                    JsonFallback::EmptyArray => b"[]".to_vec(),
440                    JsonFallback::Null => b"null".to_vec(),
441                    JsonFallback::Custom(data) => data.clone(),
442                };
443
444                // Log error using bevy's logging system
445                bevy_log::error!(
446                    "JSON serialization failed: {}. Using fallback: {:?}",
447                    e,
448                    fallback
449                );
450
451                fallback_data
452            }
453        };
454
455        self
456    }
457
458    /// Result-returning safe JSON serialization method
459    ///
460    /// # Arguments
461    /// * `body` - Data to serialize to JSON
462    ///
463    /// # Returns
464    /// * `Ok(HttpClient)` - Serialization successful
465    /// * `Err(JsonSerializationError)` - Serialization failed
466    ///
467    /// # Examples
468    /// ```
469    /// use bevy_http_client::HttpClient;
470    /// use serde::Serialize;
471    ///
472    /// #[derive(Serialize)]
473    /// struct MyData { name: String }
474    /// let data = MyData { name: "test".to_string() };
475    ///
476    /// match HttpClient::new().post("http://example.com").json_safe(&data) {
477    ///     Ok(client) => { /* use client */ },
478    ///     Err(e) => { /* handle error */ },
479    /// }
480    /// ```
481    pub fn json_safe(
482        mut self,
483        body: &impl serde::Serialize,
484    ) -> Result<Self, JsonSerializationError> {
485        // Set Content-Type header
486        if let Some(headers) = self.headers.as_mut() {
487            headers.insert("Content-Type".to_string(), "application/json".to_string());
488        } else {
489            self.headers = Some(Headers::new(&[
490                ("Content-Type", "application/json"),
491                ("Accept", "*/*"),
492            ]));
493        }
494
495        // Try serialization
496        self.body = serde_json::to_vec(body).map_err(|e| {
497            JsonSerializationError::SerializationFailed {
498                message: e.to_string(),
499                fallback_used: JsonFallback::EmptyObject, // Record intended fallback
500            }
501        })?;
502
503        Ok(self)
504    }
505
506    /// Improved json method with safe fallback - maintains backward compatibility
507    ///
508    /// This method will automatically use empty object {} as fallback when serialization fails,
509    /// instead of panicking. This maintains backward compatibility while providing better error handling.
510    ///
511    /// # Arguments
512    /// * `body` - Data to serialize to JSON
513    ///
514    /// # Returns
515    /// Returns HttpClient instance for method chaining
516    ///
517    /// # Examples
518    /// ```
519    /// use bevy_http_client::HttpClient;
520    /// use serde::Serialize;
521    ///
522    /// #[derive(Serialize)]
523    /// struct MyData { name: String }
524    /// let my_data = MyData { name: "test".to_string() };
525    ///
526    /// let client = HttpClient::new()
527    ///     .post("http://example.com")
528    ///     .json(&my_data);  // Now safe, won't panic
529    /// ```
530    pub fn json(self, body: &impl serde::Serialize) -> Self {
531        // Use default fallback strategy (empty object)
532        self.json_with_fallback(body, JsonFallback::default())
533    }
534
535    /// This method is used to set the properties of the `HttpClient` instance using an `Request`
536    /// instance. This version of the method is used when the target architecture is not
537    /// `wasm32`.
538    ///
539    /// # Arguments
540    ///
541    /// * `request` - An instance of `Request` which includes the HTTP method, URL, body, and
542    ///   headers.
543    ///
544    /// # Returns
545    ///
546    /// * `Self` - Returns the instance of the `HttpClient` struct, allowing for method chaining.
547    ///
548    /// # Examples
549    ///
550    /// ```
551    /// use bevy_http_client::HttpClient;
552    /// use ehttp::{Request, Headers};
553    ///
554    /// let request = Request {
555    ///     method: "POST".to_string(),
556    ///     url: "http://example.com".to_string(),
557    ///     body: vec![],
558    ///     headers: Headers::new(&[("Content-Type", "application/json"), ("Accept", "*/*")]),
559    /// };
560    /// let http_client = HttpClient::new().request(request);
561    /// ```
562    #[cfg(not(target_arch = "wasm32"))]
563    pub fn request(mut self, request: Request) -> Self {
564        self.method = Some(request.method);
565        self.url = Some(request.url);
566        self.body = request.body;
567        self.headers = Some(request.headers);
568
569        self
570    }
571
572    /// Associates an `Entity` with the `HttpClient`.
573    ///
574    /// This method is used to associate an `Entity` with the `HttpClient`. This can be useful when
575    /// you want to track which entity initiated the HTTP request.
576    ///
577    /// # Parameters
578    ///
579    /// * `entity`: The `Entity` that you want to associate with the `HttpClient`.
580    ///
581    /// # Returns
582    ///
583    /// A mutable reference to the `HttpClient`. This is used to allow method chaining.
584    ///
585    /// # Examples
586    ///
587    /// ```
588    /// use bevy_http_client::HttpClient;
589    /// use bevy_ecs::entity::Entity;
590    ///
591    /// let entity = Entity::from_raw_u32(42).unwrap(); // Example entity
592    /// let http_client = HttpClient::new().entity(entity);
593    /// ```
594    pub fn entity(mut self, entity: Entity) -> Self {
595        self.from_entity = Some(entity);
596        self
597    }
598
599    /// This method is used to set the properties of the `HttpClient` instance using an `Request`
600    /// instance. This version of the method is used when the target architecture is `wasm32`.
601    ///
602    /// # Arguments
603    ///
604    /// * `request` - An instance of `Request` which includes the HTTP method, URL, body, headers,
605    ///   and mode.
606    ///
607    /// # Returns
608    ///
609    /// * `Self` - Returns the instance of the `HttpClient` struct, allowing for method chaining.
610    ///
611    /// # Examples
612    ///
613    /// ```
614    /// use bevy_http_client::HttpClient;
615    /// use ehttp::{Request, Headers, Mode};
616    ///
617    /// let request = Request {
618    ///     method: "POST".to_string(),
619    ///     url: "http://example.com".to_string(),
620    ///     body: vec![],
621    ///     headers: Headers::new(&[("Content-Type", "application/json"), ("Accept", "*/*")]),
622    ///     mode: Mode::Cors,
623    /// };
624    /// let http_client = HttpClient::new().request(request);
625    /// ```
626    #[cfg(target_arch = "wasm32")]
627    pub fn request(mut self, request: Request) -> Self {
628        self.method = Some(request.method);
629        self.url = Some(request.url);
630        self.body = request.body;
631        self.headers = Some(request.headers);
632        self.mode = request.mode;
633
634        self
635    }
636
637    /// Builds an `HttpRequest` from the `HttpClient` instance.
638    ///
639    /// This method is used to construct an `HttpRequest` from the current state of the `HttpClient`
640    /// instance. The resulting `HttpRequest` includes the HTTP method, URL, body, headers, and mode
641    /// (only available on wasm builds).
642    ///
643    /// # Returns
644    ///
645    /// An `HttpRequest` instance which includes the HTTP method, URL, body, headers, and mode (only
646    /// available on wasm builds).
647    ///
648    /// # Panics
649    ///
650    /// This method will panic if the HTTP method, URL, or headers are not set in the `HttpClient`
651    /// instance.
652    ///
653    /// # Examples
654    ///
655    /// ```
656    /// use bevy_http_client::HttpClient;
657    /// use serde::Serialize;
658    ///
659    /// #[derive(Serialize)]
660    /// struct MyData { name: String }
661    /// let data = MyData { name: "test".to_string() };
662    ///
663    /// let http_request = HttpClient::new().post("http://example.com")
664    ///     .headers(&[("Content-Type", "application/json"), ("Accept", "*/*")])
665    ///     .json(&data)
666    ///     .build();
667    /// ```
668    ///
669    /// # Note
670    ///
671    /// This method consumes the `HttpClient` instance, meaning it can only be called once per
672    /// instance.
673    #[deprecated(
674        since = "0.8.3",
675        note = "Use `try_build()` instead for better error handling"
676    )]
677    pub fn build(self) -> HttpRequest {
678        HttpRequest {
679            from_entity: self.from_entity,
680            request: Request {
681                method: self.method.expect("method is required"),
682                url: self.url.expect("url is required"),
683                body: self.body,
684                headers: self.headers.expect("headers is required"),
685                #[cfg(target_arch = "wasm32")]
686                mode: self.mode,
687            },
688        }
689    }
690
691    /// Safe version of build() that returns a Result instead of panicking
692    ///
693    /// This method safely builds an `HttpRequest` from the `HttpClient` instance.
694    /// Returns an error if required fields (method, url, headers) are missing.
695    ///
696    /// # Returns
697    ///
698    /// * `Ok(HttpRequest)` - Successfully built HTTP request
699    /// * `Err(HttpClientBuilderError)` - Missing required fields
700    ///
701    /// # Examples
702    ///
703    /// ```
704    /// use bevy_http_client::HttpClient;
705    ///
706    /// let result = HttpClient::new().post("http://example.com")
707    ///     .headers(&[("Content-Type", "application/json")])
708    ///     .try_build();
709    ///
710    /// match result {
711    ///     Ok(request) => { /* use request */ },
712    ///     Err(e) => eprintln!("Build failed: {}", e),
713    /// }
714    /// ```
715    pub fn try_build(self) -> Result<HttpRequest, HttpClientBuilderError> {
716        let method = self.method.ok_or(HttpClientBuilderError::MissingMethod)?;
717        let url = self
718            .url
719            .filter(|u| !u.trim().is_empty())
720            .ok_or(HttpClientBuilderError::MissingUrl)?;
721        let headers = self.headers.ok_or(HttpClientBuilderError::MissingHeaders)?;
722
723        Ok(HttpRequest {
724            from_entity: self.from_entity,
725            request: Request {
726                method,
727                url,
728                body: self.body,
729                headers,
730                #[cfg(target_arch = "wasm32")]
731                mode: self.mode,
732            },
733        })
734    }
735
736    #[deprecated(
737        since = "0.8.3",
738        note = "Use `try_with_type()` instead for better error handling"
739    )]
740    pub fn with_type<T: Send + Sync + 'static + for<'a> serde::Deserialize<'a>>(
741        self,
742    ) -> TypedRequest<T> {
743        TypedRequest::new(
744            Request {
745                method: self.method.expect("method is required"),
746                url: self.url.expect("url is required"),
747                body: self.body,
748                headers: self.headers.expect("headers is required"),
749                #[cfg(target_arch = "wasm32")]
750                mode: self.mode,
751            },
752            self.from_entity,
753        )
754    }
755
756    /// Safe version of with_type() that returns a Result instead of panicking
757    ///
758    /// This method safely creates a typed request from the `HttpClient` instance.
759    /// Returns an error if required fields (method, url, headers) are missing.
760    ///
761    /// # Type Parameters
762    ///
763    /// * `T` - The expected response type that implements Deserialize
764    ///
765    /// # Returns
766    ///
767    /// * `Ok(TypedRequest<T>)` - Successfully built typed request
768    /// * `Err(HttpClientBuilderError)` - Missing required fields
769    ///
770    /// # Examples
771    ///
772    /// ```
773    /// use bevy_http_client::HttpClient;
774    /// use serde::Deserialize;
775    ///
776    /// #[derive(Deserialize)]
777    /// struct MyResponseType { id: u32, name: String }
778    ///
779    /// let result = HttpClient::new().get("https://api.example.com")
780    ///     .try_with_type::<MyResponseType>();
781    ///
782    /// match result {
783    ///     Ok(request) => { /* use typed request */ },
784    ///     Err(e) => eprintln!("Build failed: {}", e),
785    /// }
786    /// ```
787    pub fn try_with_type<T: Send + Sync + 'static + for<'a> serde::Deserialize<'a>>(
788        self,
789    ) -> Result<TypedRequest<T>, HttpClientBuilderError> {
790        let method = self.method.ok_or(HttpClientBuilderError::MissingMethod)?;
791        let url = self
792            .url
793            .filter(|u| !u.trim().is_empty())
794            .ok_or(HttpClientBuilderError::MissingUrl)?;
795        let headers = self.headers.ok_or(HttpClientBuilderError::MissingHeaders)?;
796
797        Ok(TypedRequest::new(
798            Request {
799                method,
800                url,
801                body: self.body,
802                headers,
803                #[cfg(target_arch = "wasm32")]
804                mode: self.mode,
805            },
806            self.from_entity,
807        ))
808    }
809}
810
811/// wrap for ehttp response
812#[derive(Event, Message, Debug, Clone, Deref)]
813pub struct HttpResponse(pub Response);
814
815/// wrap for ehttp error
816#[derive(Event, Message, Debug, Clone, Deref)]
817pub struct HttpResponseError {
818    pub err: String,
819}
820
821impl HttpResponseError {
822    pub fn new(err: String) -> Self {
823        Self { err }
824    }
825}
826
827/// task for ehttp response result
828#[derive(Component, Debug)]
829pub struct RequestTask {
830    tx: Sender<CommandQueue>,
831    rx: Receiver<CommandQueue>,
832}
833
834fn handle_request(
835    mut commands: Commands,
836    mut req_res: ResMut<HttpClientSetting>,
837    mut requests: MessageReader<HttpRequest>,
838    q_tasks: Query<&RequestTask>,
839) {
840    let thread_pool = IoTaskPool::get();
841    for request in requests.read() {
842        if req_res.is_available() {
843            let req = request.clone();
844            let (entity, has_from_entity) = if let Some(entity) = req.from_entity {
845                (entity, true)
846            } else {
847                (commands.spawn_empty().id(), false)
848            };
849
850            let tx = get_channel(&mut commands, q_tasks, entity);
851
852            thread_pool
853                .spawn(async move {
854                    let mut command_queue = CommandQueue::default();
855
856                    let response = ehttp::fetch_async(req.request).await;
857                    command_queue.push(move |world: &mut World| {
858                        match response {
859                            Ok(res) => {
860                                if let Some(mut events) =
861                                    world.get_resource_mut::<Messages<HttpResponse>>()
862                                {
863                                    events.write(HttpResponse(res.clone()));
864                                } else {
865                                    bevy_log::error!("HttpResponse events resource not found");
866                                }
867                                world.trigger(HttpObserved::new(entity, HttpResponse(res)));
868                            }
869                            Err(e) => {
870                                if let Some(mut events) =
871                                    world.get_resource_mut::<Messages<HttpResponseError>>()
872                                {
873                                    events.write(HttpResponseError::new(e.to_string()));
874                                } else {
875                                    bevy_log::error!("HttpResponseError events resource not found");
876                                }
877                                world.trigger(HttpObserved::new(
878                                    entity,
879                                    HttpResponseError::new(e.to_string()),
880                                ));
881                            }
882                        }
883
884                        if !has_from_entity {
885                            world.entity_mut(entity).despawn();
886                        }
887                    });
888
889                    if let Err(e) = tx.send(command_queue) {
890                        bevy_log::error!("Failed to send command queue: {}", e);
891                    }
892                })
893                .detach();
894
895            req_res.current_clients += 1;
896        }
897    }
898}
899
900fn get_channel(
901    commands: &mut Commands,
902    q_tasks: Query<&RequestTask>,
903    entity: Entity,
904) -> Sender<CommandQueue> {
905    if let Ok(task) = q_tasks.get(entity) {
906        task.tx.clone()
907    } else {
908        let (tx, rx) = crossbeam_channel::bounded(5);
909
910        commands.entity(entity).insert(RequestTask {
911            tx: tx.clone(),
912            rx: rx.clone(),
913        });
914
915        tx
916    }
917}
918
919fn handle_tasks(
920    mut commands: Commands,
921    mut req_res: ResMut<HttpClientSetting>,
922    mut request_tasks: Query<&RequestTask>,
923) {
924    for task in request_tasks.iter_mut() {
925        if let Ok(mut command_queue) = task.rx.try_recv() {
926            commands.append(&mut command_queue);
927            req_res.current_clients -= 1;
928        }
929    }
930}