use std::time::UNIX_EPOCH;
use reqwest::Response;
use serde_json::Value;
use tracing::debug;
use tracing::warn;
use super::constants::BRP_DEFAULT_HOST;
use super::constants::BRP_HTTP_PROTOCOL;
use super::constants::BRP_JSONRPC_PATH;
use super::constants::ERROR_BODY_PREVIEW_CHARS;
use super::constants::HTTP_CONTENT_TYPE_JSON;
use super::constants::HTTP_HEADER_CONTENT_TYPE;
use super::constants::HTTP_REQUEST_TIMEOUT;
use super::json_rpc_builder::BrpJsonRpcBuilder;
use crate::brp_tools::Port;
use crate::error::Error;
use crate::error::Result;
use crate::support::JsonObjectAccess;
use crate::tool::ParameterName;
pub(super) struct BrpHttpClient<'method> {
brp_method: &'method str,
port: Port,
params: Option<Value>,
}
enum ReqwestErrorKind {
Timeout,
Connection,
Request,
Body,
Decode,
Other,
}
impl ReqwestErrorKind {
fn classify(error: &reqwest::Error) -> Self {
if error.is_timeout() {
Self::Timeout
} else if error.is_connect() {
Self::Connection
} else if error.is_request() {
Self::Request
} else if error.is_body() {
Self::Body
} else if error.is_decode() {
Self::Decode
} else {
Self::Other
}
}
const fn label(self) -> &'static str {
match self {
Self::Timeout => "Timeout",
Self::Connection => "Connection failed",
Self::Request => "Request error",
Self::Body => "Body error",
Self::Decode => "Decode error",
Self::Other => "Unknown error type",
}
}
}
impl<'method> BrpHttpClient<'method> {
pub(super) const fn new(brp_method: &'method str, port: Port, params: Option<Value>) -> Self {
Self {
brp_method,
port,
params,
}
}
fn build_url(&self) -> String {
format!(
"{BRP_HTTP_PROTOCOL}://{BRP_DEFAULT_HOST}:{}{BRP_JSONRPC_PATH}",
self.port
)
}
fn build_request_body(&self) -> String {
let mut brp_json_rpc_builder = BrpJsonRpcBuilder::new(self.brp_method);
if let Some(ref params) = self.params {
debug!(
"BRP execute_brp_method: Added params - {}",
serde_json::to_string(params)
.unwrap_or_else(|_| "Failed to serialize params".to_string())
);
brp_json_rpc_builder = brp_json_rpc_builder.params(params.clone());
}
brp_json_rpc_builder.build().to_string()
}
pub(super) async fn send_request(&self) -> Result<Response> {
let url = self.build_url();
let body = self.build_request_body();
let client = reqwest::Client::new();
let response = client
.post(&url)
.header(HTTP_HEADER_CONTENT_TYPE, HTTP_CONTENT_TYPE_JSON)
.body(body.clone())
.timeout(HTTP_REQUEST_TIMEOUT)
.send()
.await;
let response = match response {
Ok(resp) => resp,
Err(e) => self.handle_error(e, &url, &body)?,
};
self.check_status(&response)?;
Ok(response)
}
pub(super) async fn send_streaming_request(&self) -> Result<Response> {
let url = self.build_url();
let body = self.build_request_body();
let client = reqwest::Client::new();
let response = client
.post(&url)
.header(HTTP_HEADER_CONTENT_TYPE, HTTP_CONTENT_TYPE_JSON)
.body(body.clone())
.send()
.await;
let response = match response {
Ok(resp) => resp,
Err(e) => self.handle_error(e, &url, &body)?,
};
self.check_status(&response)?;
Ok(response)
}
fn check_status(&self, response: &Response) -> Result<()> {
if !response.status().is_success() {
warn!(
"BRP execute_brp_method: HTTP status error - status={}",
response.status()
);
return Err(
error_stack::Report::new(Error::JsonRpc("HTTP error".to_string()))
.attach(format!(
"BRP server returned HTTP error {}: {}",
response.status(),
response
.status()
.canonical_reason()
.unwrap_or("Unknown error")
))
.attach(format!("Method: {}, Port: {}", self.brp_method, self.port)),
);
}
Ok(())
}
fn handle_error(&self, e: reqwest::Error, url: &str, request_body: &str) -> Result<Response> {
warn!("BRP execute_brp_method: HTTP request failed - error={e}");
let error_details = format!(
"HTTP Error at {}\nMethod: {}\nPort: {}\nURL: {}\nError: {:?}\n",
std::time::SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |d| d.as_secs()),
self.brp_method,
self.port,
url,
e
);
if let Some(temp_dir) = std::env::temp_dir().to_str() {
let error_file = format!(
"{}/bevy_brp_http_error_{}.log",
temp_dir,
std::process::id()
);
let _ = std::fs::write(&error_file, &error_details);
debug!("HTTP error details written to: {error_file}");
}
let mut context_info = vec![
format!("Method: {}", self.brp_method),
format!("Port: {}", self.port),
format!("URL: {url}"),
];
if let Ok(body_json) = serde_json::from_str::<Value>(request_body)
&& let Some(params) = body_json.get_field(ParameterName::Params)
{
if let Some(entity) = params.get_field(ParameterName::Entity) {
context_info.push(format!("Entity: {entity}"));
}
if let Some(component) = params.get_field_str(ParameterName::Component) {
context_info.push(format!("Component: {component}"));
}
if let Some(path) = params.get_field_str(ParameterName::Path) {
context_info.push(format!("Path: {path}"));
}
}
let error_type = ReqwestErrorKind::classify(&e).label();
context_info.push(format!("Error type: {error_type}"));
context_info.push(format!("Port: ({})", self.port));
let error_message = format!(
"HTTP request failed for {} operation - {error_type}: {e}",
self.brp_method
);
Err(error_stack::Report::new(Error::JsonRpc(error_message))
.attach(context_info.join(", "))
.attach(format!("Full error: {e:?}"))
.attach(format!(
"Request body (first {ERROR_BODY_PREVIEW_CHARS} chars): {}",
request_body
.chars()
.take(ERROR_BODY_PREVIEW_CHARS)
.collect::<String>()
)))
}
}