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}