2024-09-27 20:09:46 +00:00
|
|
|
//! The BRP transport using JSON-RPC over HTTP.
|
|
|
|
//!
|
|
|
|
//! Adding the [`RemoteHttpPlugin`] to your [`App`] causes Bevy to accept
|
|
|
|
//! connections over HTTP (by default, on port 15702) while your app is running.
|
|
|
|
//!
|
|
|
|
//! Clients are expected to `POST` JSON requests to the root URL; see the `client`
|
|
|
|
//! example for a trivial example of use.
|
|
|
|
|
|
|
|
#![cfg(not(target_family = "wasm"))]
|
|
|
|
|
2024-10-08 16:21:46 +00:00
|
|
|
use crate::{
|
|
|
|
error_codes, BrpBatch, BrpError, BrpMessage, BrpRequest, BrpResponse, BrpResult, BrpSender,
|
|
|
|
};
|
2024-09-27 20:09:46 +00:00
|
|
|
use anyhow::Result as AnyhowResult;
|
2024-10-08 16:21:46 +00:00
|
|
|
use async_channel::{Receiver, Sender};
|
2024-09-27 20:09:46 +00:00
|
|
|
use async_io::Async;
|
|
|
|
use bevy_app::{App, Plugin, Startup};
|
|
|
|
use bevy_ecs::system::{Res, Resource};
|
2024-10-08 16:21:46 +00:00
|
|
|
use bevy_tasks::{futures_lite::StreamExt, IoTaskPool};
|
2024-09-27 20:09:46 +00:00
|
|
|
use core::net::{IpAddr, Ipv4Addr};
|
2024-10-08 16:21:46 +00:00
|
|
|
use core::{
|
|
|
|
convert::Infallible,
|
|
|
|
pin::Pin,
|
|
|
|
task::{Context, Poll},
|
|
|
|
};
|
2024-09-27 20:09:46 +00:00
|
|
|
use http_body_util::{BodyExt as _, Full};
|
2024-10-06 19:06:19 +00:00
|
|
|
use hyper::header::{HeaderName, HeaderValue};
|
2024-09-27 20:09:46 +00:00
|
|
|
use hyper::{
|
2024-10-08 16:21:46 +00:00
|
|
|
body::{Body, Bytes, Frame, Incoming},
|
2024-09-27 20:09:46 +00:00
|
|
|
server::conn::http1,
|
|
|
|
service, Request, Response,
|
|
|
|
};
|
|
|
|
use serde_json::Value;
|
|
|
|
use smol_hyper::rt::{FuturesIo, SmolTimer};
|
2024-10-06 13:21:21 +00:00
|
|
|
use std::collections::HashMap;
|
2024-10-08 16:21:46 +00:00
|
|
|
use std::net::{TcpListener, TcpStream};
|
2024-09-27 20:09:46 +00:00
|
|
|
|
|
|
|
/// The default port that Bevy will listen on.
|
|
|
|
///
|
|
|
|
/// This value was chosen randomly.
|
|
|
|
pub const DEFAULT_PORT: u16 = 15702;
|
|
|
|
|
|
|
|
/// The default host address that Bevy will use for its server.
|
|
|
|
pub const DEFAULT_ADDR: IpAddr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
|
|
|
|
|
2024-10-06 13:21:21 +00:00
|
|
|
/// A struct that holds a collection of HTTP headers.
|
|
|
|
///
|
|
|
|
/// This struct is used to store a set of HTTP headers as key-value pairs, where the keys are
|
|
|
|
/// of type [`HeaderName`] and the values are of type [`HeaderValue`].
|
|
|
|
///
|
|
|
|
#[derive(Debug, Resource, Clone)]
|
|
|
|
pub struct Headers {
|
|
|
|
headers: HashMap<HeaderName, HeaderValue>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Headers {
|
|
|
|
/// Create a new instance of `Headers`.
|
|
|
|
pub fn new() -> Self {
|
|
|
|
Self {
|
|
|
|
headers: HashMap::new(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-10-06 19:06:19 +00:00
|
|
|
/// Insert a key value pair to the `Headers` instance.
|
|
|
|
pub fn insert(
|
|
|
|
mut self,
|
|
|
|
name: impl TryInto<HeaderName>,
|
|
|
|
value: impl TryInto<HeaderValue>,
|
|
|
|
) -> Self {
|
|
|
|
let Ok(header_name) = name.try_into() else {
|
|
|
|
panic!("Invalid header name")
|
|
|
|
};
|
|
|
|
let Ok(header_value) = value.try_into() else {
|
|
|
|
panic!("Invalid header value")
|
|
|
|
};
|
|
|
|
self.headers.insert(header_name, header_value);
|
2024-10-06 13:21:21 +00:00
|
|
|
self
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Default for Headers {
|
|
|
|
fn default() -> Self {
|
|
|
|
Self::new()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-09-27 20:09:46 +00:00
|
|
|
/// Add this plugin to your [`App`] to allow remote connections over HTTP to inspect and modify entities.
|
|
|
|
/// It requires the [`RemotePlugin`](super::RemotePlugin).
|
|
|
|
///
|
|
|
|
/// This BRP transport cannot be used when targeting WASM.
|
|
|
|
///
|
|
|
|
/// The defaults are:
|
|
|
|
/// - [`DEFAULT_ADDR`] : 127.0.0.1.
|
|
|
|
/// - [`DEFAULT_PORT`] : 15702.
|
2024-10-06 13:21:21 +00:00
|
|
|
///
|
2024-09-27 20:09:46 +00:00
|
|
|
pub struct RemoteHttpPlugin {
|
|
|
|
/// The address that Bevy will bind to.
|
|
|
|
address: IpAddr,
|
|
|
|
/// The port that Bevy will listen on.
|
|
|
|
port: u16,
|
2024-10-06 13:21:21 +00:00
|
|
|
/// The headers that Bevy will include in its HTTP responses
|
|
|
|
headers: Headers,
|
2024-09-27 20:09:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl Default for RemoteHttpPlugin {
|
|
|
|
fn default() -> Self {
|
|
|
|
Self {
|
|
|
|
address: DEFAULT_ADDR,
|
|
|
|
port: DEFAULT_PORT,
|
2024-10-06 13:21:21 +00:00
|
|
|
headers: Headers::new(),
|
2024-09-27 20:09:46 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Plugin for RemoteHttpPlugin {
|
|
|
|
fn build(&self, app: &mut App) {
|
|
|
|
app.insert_resource(HostAddress(self.address))
|
|
|
|
.insert_resource(HostPort(self.port))
|
2024-10-06 13:21:21 +00:00
|
|
|
.insert_resource(HostHeaders(self.headers.clone()))
|
2024-09-27 20:09:46 +00:00
|
|
|
.add_systems(Startup, start_http_server);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl RemoteHttpPlugin {
|
|
|
|
/// Set the IP address that the server will use.
|
|
|
|
#[must_use]
|
|
|
|
pub fn with_address(mut self, address: impl Into<IpAddr>) -> Self {
|
|
|
|
self.address = address.into();
|
|
|
|
self
|
|
|
|
}
|
|
|
|
/// Set the remote port that the server will listen on.
|
|
|
|
#[must_use]
|
|
|
|
pub fn with_port(mut self, port: u16) -> Self {
|
|
|
|
self.port = port;
|
|
|
|
self
|
|
|
|
}
|
2024-10-06 13:21:21 +00:00
|
|
|
/// Set the extra headers that the response will include.
|
2024-10-06 19:06:19 +00:00
|
|
|
///
|
|
|
|
/// ////// /// # Example
|
|
|
|
///
|
|
|
|
/// ```ignore
|
|
|
|
///
|
|
|
|
/// // Create CORS headers
|
|
|
|
/// let cors_headers = Headers::new()
|
|
|
|
/// .insert("Access-Control-Allow-Origin", "*")
|
|
|
|
/// .insert("Access-Control-Allow-Headers", "Content-Type");
|
|
|
|
///
|
|
|
|
/// // Create the Bevy app and add the RemoteHttpPlugin with CORS headers
|
|
|
|
/// fn main() {
|
|
|
|
/// App::new()
|
|
|
|
/// .add_plugins(DefaultPlugins)
|
|
|
|
/// .add_plugins(RemotePlugin::default())
|
|
|
|
/// .add_plugins(RemoteHttpPlugin::default()
|
|
|
|
/// .with_headers(cors_headers))
|
|
|
|
/// .run();
|
|
|
|
/// }
|
|
|
|
/// ```
|
2024-10-06 13:21:21 +00:00
|
|
|
#[must_use]
|
|
|
|
pub fn with_headers(mut self, headers: Headers) -> Self {
|
|
|
|
self.headers = headers;
|
|
|
|
self
|
|
|
|
}
|
|
|
|
/// Add a single header to the response headers.
|
|
|
|
#[must_use]
|
|
|
|
pub fn with_header(
|
|
|
|
mut self,
|
|
|
|
name: impl TryInto<HeaderName>,
|
|
|
|
value: impl TryInto<HeaderValue>,
|
|
|
|
) -> Self {
|
2024-10-06 19:06:19 +00:00
|
|
|
self.headers = self.headers.insert(name, value);
|
2024-10-06 13:21:21 +00:00
|
|
|
self
|
|
|
|
}
|
2024-09-27 20:09:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// A resource containing the IP address that Bevy will host on.
|
|
|
|
///
|
|
|
|
/// Currently, changing this while the application is running has no effect; this merely
|
|
|
|
/// reflects the IP address that is set during the setup of the [`RemoteHttpPlugin`].
|
|
|
|
#[derive(Debug, Resource)]
|
|
|
|
pub struct HostAddress(pub IpAddr);
|
|
|
|
|
|
|
|
/// A resource containing the port number that Bevy will listen on.
|
|
|
|
///
|
|
|
|
/// Currently, changing this while the application is running has no effect; this merely
|
|
|
|
/// reflects the host that is set during the setup of the [`RemoteHttpPlugin`].
|
|
|
|
#[derive(Debug, Resource)]
|
|
|
|
pub struct HostPort(pub u16);
|
|
|
|
|
2024-10-06 13:21:21 +00:00
|
|
|
/// A resource containing the headers that Bevy will include in its HTTP responses.
|
|
|
|
///
|
|
|
|
#[derive(Debug, Resource)]
|
|
|
|
struct HostHeaders(pub Headers);
|
|
|
|
|
2024-09-27 20:09:46 +00:00
|
|
|
/// A system that starts up the Bevy Remote Protocol HTTP server.
|
|
|
|
fn start_http_server(
|
|
|
|
request_sender: Res<BrpSender>,
|
|
|
|
address: Res<HostAddress>,
|
|
|
|
remote_port: Res<HostPort>,
|
2024-10-06 13:21:21 +00:00
|
|
|
headers: Res<HostHeaders>,
|
2024-09-27 20:09:46 +00:00
|
|
|
) {
|
|
|
|
IoTaskPool::get()
|
|
|
|
.spawn(server_main(
|
|
|
|
address.0,
|
|
|
|
remote_port.0,
|
|
|
|
request_sender.clone(),
|
2024-10-06 13:21:21 +00:00
|
|
|
headers.0.clone(),
|
2024-09-27 20:09:46 +00:00
|
|
|
))
|
|
|
|
.detach();
|
|
|
|
}
|
|
|
|
|
|
|
|
/// The Bevy Remote Protocol server main loop.
|
|
|
|
async fn server_main(
|
|
|
|
address: IpAddr,
|
|
|
|
port: u16,
|
|
|
|
request_sender: Sender<BrpMessage>,
|
2024-10-06 13:21:21 +00:00
|
|
|
headers: Headers,
|
2024-09-27 20:09:46 +00:00
|
|
|
) -> AnyhowResult<()> {
|
|
|
|
listen(
|
|
|
|
Async::<TcpListener>::bind((address, port))?,
|
|
|
|
&request_sender,
|
2024-10-06 13:21:21 +00:00
|
|
|
&headers,
|
2024-09-27 20:09:46 +00:00
|
|
|
)
|
|
|
|
.await
|
|
|
|
}
|
|
|
|
|
|
|
|
async fn listen(
|
|
|
|
listener: Async<TcpListener>,
|
|
|
|
request_sender: &Sender<BrpMessage>,
|
2024-10-06 13:21:21 +00:00
|
|
|
headers: &Headers,
|
2024-09-27 20:09:46 +00:00
|
|
|
) -> AnyhowResult<()> {
|
|
|
|
loop {
|
|
|
|
let (client, _) = listener.accept().await?;
|
|
|
|
|
|
|
|
let request_sender = request_sender.clone();
|
2024-10-06 13:21:21 +00:00
|
|
|
let headers = headers.clone();
|
2024-09-27 20:09:46 +00:00
|
|
|
IoTaskPool::get()
|
|
|
|
.spawn(async move {
|
2024-10-06 13:21:21 +00:00
|
|
|
let _ = handle_client(client, request_sender, headers).await;
|
2024-09-27 20:09:46 +00:00
|
|
|
})
|
|
|
|
.detach();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async fn handle_client(
|
|
|
|
client: Async<TcpStream>,
|
|
|
|
request_sender: Sender<BrpMessage>,
|
2024-10-06 13:21:21 +00:00
|
|
|
headers: Headers,
|
2024-09-27 20:09:46 +00:00
|
|
|
) -> AnyhowResult<()> {
|
|
|
|
http1::Builder::new()
|
|
|
|
.timer(SmolTimer::new())
|
|
|
|
.serve_connection(
|
|
|
|
FuturesIo::new(client),
|
2024-10-06 13:21:21 +00:00
|
|
|
service::service_fn(|request| {
|
|
|
|
process_request_batch(request, &request_sender, &headers)
|
|
|
|
}),
|
2024-09-27 20:09:46 +00:00
|
|
|
)
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
/// A helper function for the Bevy Remote Protocol server that handles a batch
|
|
|
|
/// of requests coming from a client.
|
|
|
|
async fn process_request_batch(
|
|
|
|
request: Request<Incoming>,
|
|
|
|
request_sender: &Sender<BrpMessage>,
|
2024-10-06 13:21:21 +00:00
|
|
|
headers: &Headers,
|
2024-10-08 16:21:46 +00:00
|
|
|
) -> AnyhowResult<Response<BrpHttpBody>> {
|
2024-09-27 20:09:46 +00:00
|
|
|
let batch_bytes = request.into_body().collect().await?.to_bytes();
|
|
|
|
let batch: Result<BrpBatch, _> = serde_json::from_slice(&batch_bytes);
|
|
|
|
|
2024-10-08 16:21:46 +00:00
|
|
|
let result = match batch {
|
2024-09-27 20:09:46 +00:00
|
|
|
Ok(BrpBatch::Single(request)) => {
|
2024-10-08 16:21:46 +00:00
|
|
|
let response = process_single_request(request, request_sender).await?;
|
|
|
|
match response {
|
|
|
|
BrpHttpResponse::Complete(res) => {
|
|
|
|
BrpHttpResponse::Complete(serde_json::to_string(&res)?)
|
|
|
|
}
|
|
|
|
BrpHttpResponse::Stream(stream) => BrpHttpResponse::Stream(stream),
|
|
|
|
}
|
2024-09-27 20:09:46 +00:00
|
|
|
}
|
|
|
|
Ok(BrpBatch::Batch(requests)) => {
|
|
|
|
let mut responses = Vec::new();
|
|
|
|
|
|
|
|
for request in requests {
|
2024-10-08 16:21:46 +00:00
|
|
|
let response = process_single_request(request, request_sender).await?;
|
|
|
|
match response {
|
|
|
|
BrpHttpResponse::Complete(res) => responses.push(res),
|
|
|
|
BrpHttpResponse::Stream(BrpStream { id, .. }) => {
|
|
|
|
responses.push(BrpResponse::new(
|
|
|
|
id,
|
|
|
|
Err(BrpError {
|
|
|
|
code: error_codes::INVALID_REQUEST,
|
|
|
|
message: "Streaming can not be used in batch requests".to_string(),
|
|
|
|
data: None,
|
|
|
|
}),
|
|
|
|
));
|
|
|
|
}
|
|
|
|
}
|
2024-09-27 20:09:46 +00:00
|
|
|
}
|
|
|
|
|
2024-10-08 16:21:46 +00:00
|
|
|
BrpHttpResponse::Complete(serde_json::to_string(&responses)?)
|
2024-09-27 20:09:46 +00:00
|
|
|
}
|
|
|
|
Err(err) => {
|
|
|
|
let err = BrpResponse::new(
|
|
|
|
None,
|
|
|
|
Err(BrpError {
|
|
|
|
code: error_codes::INVALID_REQUEST,
|
|
|
|
message: err.to_string(),
|
|
|
|
data: None,
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
|
2024-10-08 16:21:46 +00:00
|
|
|
BrpHttpResponse::Complete(serde_json::to_string(&err)?)
|
2024-09-27 20:09:46 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2024-10-08 16:21:46 +00:00
|
|
|
let mut response = match result {
|
|
|
|
BrpHttpResponse::Complete(serialized) => {
|
|
|
|
let mut response = Response::new(BrpHttpBody::Complete(Full::new(Bytes::from(
|
|
|
|
serialized.as_bytes().to_owned(),
|
|
|
|
))));
|
|
|
|
response.headers_mut().insert(
|
|
|
|
hyper::header::CONTENT_TYPE,
|
|
|
|
HeaderValue::from_static("application/json"),
|
|
|
|
);
|
|
|
|
response
|
|
|
|
}
|
|
|
|
BrpHttpResponse::Stream(stream) => {
|
|
|
|
let mut response = Response::new(BrpHttpBody::Stream(stream));
|
|
|
|
response.headers_mut().insert(
|
|
|
|
hyper::header::CONTENT_TYPE,
|
|
|
|
HeaderValue::from_static("text/event-stream"),
|
|
|
|
);
|
|
|
|
response
|
|
|
|
}
|
|
|
|
};
|
2024-10-06 13:21:21 +00:00
|
|
|
for (key, value) in &headers.headers {
|
|
|
|
response.headers_mut().insert(key, value.clone());
|
|
|
|
}
|
2024-09-30 21:23:55 +00:00
|
|
|
Ok(response)
|
2024-09-27 20:09:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// A helper function for the Bevy Remote Protocol server that processes a single
|
|
|
|
/// request coming from a client.
|
|
|
|
async fn process_single_request(
|
|
|
|
request: Value,
|
|
|
|
request_sender: &Sender<BrpMessage>,
|
2024-10-08 16:21:46 +00:00
|
|
|
) -> AnyhowResult<BrpHttpResponse<BrpResponse, BrpStream>> {
|
2024-09-27 20:09:46 +00:00
|
|
|
// Reach in and get the request ID early so that we can report it even when parsing fails.
|
|
|
|
let id = request.as_object().and_then(|map| map.get("id")).cloned();
|
|
|
|
|
|
|
|
let request: BrpRequest = match serde_json::from_value(request) {
|
|
|
|
Ok(v) => v,
|
|
|
|
Err(err) => {
|
2024-10-08 16:21:46 +00:00
|
|
|
return Ok(BrpHttpResponse::Complete(BrpResponse::new(
|
2024-09-27 20:09:46 +00:00
|
|
|
id,
|
|
|
|
Err(BrpError {
|
|
|
|
code: error_codes::INVALID_REQUEST,
|
|
|
|
message: err.to_string(),
|
|
|
|
data: None,
|
|
|
|
}),
|
2024-10-08 16:21:46 +00:00
|
|
|
)));
|
2024-09-27 20:09:46 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
if request.jsonrpc != "2.0" {
|
2024-10-08 16:21:46 +00:00
|
|
|
return Ok(BrpHttpResponse::Complete(BrpResponse::new(
|
2024-09-27 20:09:46 +00:00
|
|
|
id,
|
|
|
|
Err(BrpError {
|
|
|
|
code: error_codes::INVALID_REQUEST,
|
|
|
|
message: String::from("JSON-RPC request requires `\"jsonrpc\": \"2.0\"`"),
|
|
|
|
data: None,
|
|
|
|
}),
|
2024-10-08 16:21:46 +00:00
|
|
|
)));
|
2024-09-27 20:09:46 +00:00
|
|
|
}
|
|
|
|
|
2024-10-08 16:21:46 +00:00
|
|
|
let watch = request.method.contains("+watch");
|
|
|
|
let size = if watch { 8 } else { 1 };
|
|
|
|
let (result_sender, result_receiver) = async_channel::bounded(size);
|
2024-09-27 20:09:46 +00:00
|
|
|
|
|
|
|
let _ = request_sender
|
|
|
|
.send(BrpMessage {
|
|
|
|
method: request.method,
|
|
|
|
params: request.params,
|
|
|
|
sender: result_sender,
|
|
|
|
})
|
|
|
|
.await;
|
|
|
|
|
2024-10-08 16:21:46 +00:00
|
|
|
if watch {
|
|
|
|
Ok(BrpHttpResponse::Stream(BrpStream {
|
|
|
|
id: request.id,
|
|
|
|
rx: Box::pin(result_receiver),
|
|
|
|
}))
|
|
|
|
} else {
|
|
|
|
let result = result_receiver.recv().await?;
|
|
|
|
Ok(BrpHttpResponse::Complete(BrpResponse::new(
|
|
|
|
request.id, result,
|
|
|
|
)))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
struct BrpStream {
|
|
|
|
id: Option<Value>,
|
|
|
|
rx: Pin<Box<Receiver<BrpResult>>>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Body for BrpStream {
|
|
|
|
type Data = Bytes;
|
|
|
|
type Error = Infallible;
|
|
|
|
|
|
|
|
fn poll_frame(
|
|
|
|
mut self: Pin<&mut Self>,
|
|
|
|
cx: &mut Context<'_>,
|
|
|
|
) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
|
|
|
|
match self.as_mut().rx.poll_next(cx) {
|
|
|
|
Poll::Ready(result) => match result {
|
|
|
|
Some(result) => {
|
|
|
|
let response = BrpResponse::new(self.id.clone(), result);
|
|
|
|
let serialized = serde_json::to_string(&response).unwrap();
|
|
|
|
let bytes =
|
|
|
|
Bytes::from(format!("data: {serialized}\n\n").as_bytes().to_owned());
|
|
|
|
let frame = Frame::data(bytes);
|
|
|
|
Poll::Ready(Some(Ok(frame)))
|
|
|
|
}
|
|
|
|
None => Poll::Ready(None),
|
|
|
|
},
|
|
|
|
Poll::Pending => Poll::Pending,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn is_end_stream(&self) -> bool {
|
|
|
|
dbg!(self.rx.is_closed())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
enum BrpHttpResponse<C, S> {
|
|
|
|
Complete(C),
|
|
|
|
Stream(S),
|
|
|
|
}
|
|
|
|
|
|
|
|
enum BrpHttpBody {
|
|
|
|
Complete(Full<Bytes>),
|
|
|
|
Stream(BrpStream),
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Body for BrpHttpBody {
|
|
|
|
type Data = Bytes;
|
|
|
|
type Error = Infallible;
|
|
|
|
|
|
|
|
fn poll_frame(
|
|
|
|
self: Pin<&mut Self>,
|
|
|
|
cx: &mut Context<'_>,
|
|
|
|
) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
|
|
|
|
match &mut *self.get_mut() {
|
|
|
|
BrpHttpBody::Complete(body) => Body::poll_frame(Pin::new(body), cx),
|
|
|
|
BrpHttpBody::Stream(body) => Body::poll_frame(Pin::new(body), cx),
|
|
|
|
}
|
|
|
|
}
|
2024-09-27 20:09:46 +00:00
|
|
|
}
|