use crate::{Method, Request, Response};
use rama_core::error::BoxErrorExt as _;
use rama_core::{
Service,
error::{BoxError, ErrorExt},
extensions::{Extension, Extensions, ExtensionsRef},
};
use rama_http_headers::authorization::Credentials;
use rama_http_types::StreamingBody;
use rama_net::uri::Uri;
pub trait HttpClientExt: private::HttpClientExtSealed + Sized + Send + Sync + 'static {
type ExecuteResponse;
type ExecuteError;
fn get(&self, url: impl IntoUrl) -> RequestBuilder<'_, Self, Self::ExecuteResponse>;
fn post(&self, url: impl IntoUrl) -> RequestBuilder<'_, Self, Self::ExecuteResponse>;
fn put(&self, url: impl IntoUrl) -> RequestBuilder<'_, Self, Self::ExecuteResponse>;
fn patch(&self, url: impl IntoUrl) -> RequestBuilder<'_, Self, Self::ExecuteResponse>;
fn delete(&self, url: impl IntoUrl) -> RequestBuilder<'_, Self, Self::ExecuteResponse>;
fn head(&self, url: impl IntoUrl) -> RequestBuilder<'_, Self, Self::ExecuteResponse>;
fn connect(&self, url: impl IntoUrl) -> RequestBuilder<'_, Self, Self::ExecuteResponse>;
fn request(
&self,
method: Method,
url: impl IntoUrl,
) -> RequestBuilder<'_, Self, Self::ExecuteResponse>;
fn build_from_request<Body: Into<crate::Body>>(
&self,
request: Request<Body>,
) -> RequestBuilder<'_, Self, Self::ExecuteResponse>;
fn execute(
&self,
request: Request,
) -> impl Future<Output = Result<Self::ExecuteResponse, Self::ExecuteError>>;
}
impl<S, Body> HttpClientExt for S
where
S: Service<Request, Output = Response<Body>, Error: Into<BoxError>>,
{
type ExecuteResponse = Response<Body>;
type ExecuteError = S::Error;
fn get(&self, url: impl IntoUrl) -> RequestBuilder<'_, Self, Self::ExecuteResponse> {
self.request(Method::GET, url)
}
fn post(&self, url: impl IntoUrl) -> RequestBuilder<'_, Self, Self::ExecuteResponse> {
self.request(Method::POST, url)
}
fn put(&self, url: impl IntoUrl) -> RequestBuilder<'_, Self, Self::ExecuteResponse> {
self.request(Method::PUT, url)
}
fn patch(&self, url: impl IntoUrl) -> RequestBuilder<'_, Self, Self::ExecuteResponse> {
self.request(Method::PATCH, url)
}
fn delete(&self, url: impl IntoUrl) -> RequestBuilder<'_, Self, Self::ExecuteResponse> {
self.request(Method::DELETE, url)
}
fn head(&self, url: impl IntoUrl) -> RequestBuilder<'_, Self, Self::ExecuteResponse> {
self.request(Method::HEAD, url)
}
fn connect(&self, url: impl IntoUrl) -> RequestBuilder<'_, Self, Self::ExecuteResponse> {
self.request(Method::CONNECT, url)
}
fn request(
&self,
method: Method,
url: impl IntoUrl,
) -> RequestBuilder<'_, Self, Self::ExecuteResponse> {
let uri = match url.into_url() {
Ok(uri) => uri,
Err(err) => {
return RequestBuilder {
http_client_service: self,
state: RequestBuilderState::Error(err),
_phantom: std::marker::PhantomData,
};
}
};
let builder = crate::request::Builder::new().method(method).uri(uri);
RequestBuilder {
http_client_service: self,
state: RequestBuilderState::PreBody(builder),
_phantom: std::marker::PhantomData,
}
}
fn build_from_request<RequestBody: Into<crate::Body>>(
&self,
request: Request<RequestBody>,
) -> RequestBuilder<'_, Self, Self::ExecuteResponse> {
RequestBuilder {
http_client_service: self,
state: RequestBuilderState::PostBody(request.map(Into::into)),
_phantom: std::marker::PhantomData,
}
}
fn execute(
&self,
request: Request,
) -> impl Future<Output = Result<Self::ExecuteResponse, Self::ExecuteError>> {
Service::serve(self, request)
}
}
pub trait IntoUrl: private::IntoUrlSealed {}
impl IntoUrl for Uri {}
impl IntoUrl for &str {}
impl IntoUrl for String {}
impl IntoUrl for &String {}
pub trait IntoHeaderName: private::IntoHeaderNameSealed {}
impl IntoHeaderName for crate::HeaderName {}
impl IntoHeaderName for Option<crate::HeaderName> {}
impl IntoHeaderName for &str {}
impl IntoHeaderName for String {}
impl IntoHeaderName for &String {}
impl IntoHeaderName for &[u8] {}
pub trait IntoHeaderValue: private::IntoHeaderValueSealed {}
impl IntoHeaderValue for crate::HeaderValue {}
impl IntoHeaderValue for &str {}
impl IntoHeaderValue for String {}
impl IntoHeaderValue for &String {}
impl IntoHeaderValue for &[u8] {}
mod private {
use rama_http_types::HeaderName;
use rama_net::Protocol;
use super::*;
pub trait IntoUrlSealed {
fn into_url(self) -> Result<Uri, BoxError>;
}
impl IntoUrlSealed for Uri {
fn into_url(self) -> Result<Uri, BoxError> {
let protocol: Option<Protocol> = self.scheme().cloned();
match protocol {
Some(protocol) => {
if protocol.is_http() || protocol.is_ws() {
Ok(self)
} else {
Err(BoxError::from_static_str("unsupported protocol")
.context_field("protocol", protocol))
}
}
None => Err(BoxError::from_static_str("Missing scheme in URI")),
}
}
}
impl IntoUrlSealed for &str {
fn into_url(self) -> Result<Uri, BoxError> {
match self.parse::<Uri>() {
Ok(uri) => uri.into_url(),
Err(_) => {
Err(BoxError::from_static_str("invalid url").context_str_field("raw_str", self))
}
}
}
}
impl IntoUrlSealed for String {
#[inline(always)]
fn into_url(self) -> Result<Uri, BoxError> {
self.as_str().into_url()
}
}
impl IntoUrlSealed for &String {
#[inline(always)]
fn into_url(self) -> Result<Uri, BoxError> {
self.as_str().into_url()
}
}
pub trait IntoHeaderNameSealed {
fn into_header_name(self) -> Result<crate::HeaderName, BoxError>;
}
impl IntoHeaderNameSealed for HeaderName {
fn into_header_name(self) -> Result<crate::HeaderName, BoxError> {
Ok(self)
}
}
impl IntoHeaderNameSealed for Option<HeaderName> {
fn into_header_name(self) -> Result<crate::HeaderName, BoxError> {
match self {
Some(name) => Ok(name),
None => Err(BoxError::from_static_str("Header name is required")),
}
}
}
impl IntoHeaderNameSealed for &str {
fn into_header_name(self) -> Result<crate::HeaderName, BoxError> {
let name = self.parse::<crate::HeaderName>()?;
Ok(name)
}
}
impl IntoHeaderNameSealed for String {
#[inline(always)]
fn into_header_name(self) -> Result<crate::HeaderName, BoxError> {
self.as_str().into_header_name()
}
}
impl IntoHeaderNameSealed for &String {
#[inline(always)]
fn into_header_name(self) -> Result<crate::HeaderName, BoxError> {
self.as_str().into_header_name()
}
}
impl IntoHeaderNameSealed for &[u8] {
fn into_header_name(self) -> Result<crate::HeaderName, BoxError> {
let name = crate::HeaderName::from_bytes(self)?;
Ok(name)
}
}
pub trait IntoHeaderValueSealed {
fn into_header_value(self) -> Result<crate::HeaderValue, BoxError>;
}
impl IntoHeaderValueSealed for crate::HeaderValue {
fn into_header_value(self) -> Result<crate::HeaderValue, BoxError> {
Ok(self)
}
}
impl IntoHeaderValueSealed for &str {
fn into_header_value(self) -> Result<crate::HeaderValue, BoxError> {
let value = self.parse::<crate::HeaderValue>()?;
Ok(value)
}
}
impl IntoHeaderValueSealed for String {
fn into_header_value(self) -> Result<crate::HeaderValue, BoxError> {
self.as_str().into_header_value()
}
}
impl IntoHeaderValueSealed for &String {
#[inline(always)]
fn into_header_value(self) -> Result<crate::HeaderValue, BoxError> {
self.as_str().into_header_value()
}
}
impl IntoHeaderValueSealed for &[u8] {
fn into_header_value(self) -> Result<crate::HeaderValue, BoxError> {
let value = crate::HeaderValue::from_bytes(self)?;
Ok(value)
}
}
pub trait HttpClientExtSealed {}
impl<S, Body> HttpClientExtSealed for S where
S: Service<Request, Output = Response<Body>, Error: Into<BoxError>>
{
}
}
pub struct RequestBuilder<'a, S, Response> {
http_client_service: &'a S,
state: RequestBuilderState,
_phantom: std::marker::PhantomData<fn(Response) -> ()>,
}
impl<S, Response> std::fmt::Debug for RequestBuilder<'_, S, Response>
where
S: std::fmt::Debug,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RequestBuilder")
.field("http_client_service", &self.http_client_service)
.field("state", &self.state)
.finish()
}
}
#[derive(Debug)]
enum RequestBuilderState {
PreBody(crate::request::Builder),
PostBody(crate::Request),
Error(BoxError),
}
impl<S, Body> RequestBuilder<'_, S, Response<Body>>
where
S: Service<Request, Output = Response<Body>, Error: Into<BoxError>>,
{
#[must_use]
pub fn header<K, V>(mut self, key: K, value: V) -> Self
where
K: IntoHeaderName,
V: IntoHeaderValue,
{
match self.state {
RequestBuilderState::PreBody(builder) => {
let key = match key.into_header_name() {
Ok(key) => key,
Err(err) => {
self.state = RequestBuilderState::Error(err);
return self;
}
};
let value = match value.into_header_value() {
Ok(value) => value,
Err(err) => {
self.state = RequestBuilderState::Error(err);
return self;
}
};
self.state = RequestBuilderState::PreBody(builder.header(key, value));
self
}
RequestBuilderState::PostBody(mut request) => {
let key = match key.into_header_name() {
Ok(key) => key,
Err(err) => {
self.state = RequestBuilderState::Error(err);
return self;
}
};
let value = match value.into_header_value() {
Ok(value) => value,
Err(err) => {
self.state = RequestBuilderState::Error(err);
return self;
}
};
request.headers_mut().append(key, value);
self.state = RequestBuilderState::PostBody(request);
self
}
RequestBuilderState::Error(err) => {
self.state = RequestBuilderState::Error(err);
self
}
}
}
#[must_use]
pub fn typed_header<H>(self, header: H) -> Self
where
H: crate::headers::HeaderEncode,
{
if let Some(value) = header.encode_to_value() {
self.header(H::name().clone(), value)
} else {
self
}
}
#[must_use]
pub fn headers(mut self, headers: crate::HeaderMap) -> Self {
for (key, value) in headers.into_iter() {
self = self.header(key, value);
}
self
}
#[must_use]
pub fn overwrite_header<K, V>(mut self, key: K, value: V) -> Self
where
K: IntoHeaderName,
V: IntoHeaderValue,
{
match self.state {
RequestBuilderState::PreBody(mut builder) => {
if let Some(headers) = builder.headers_mut() {
let key = match key.into_header_name() {
Ok(key) => key,
Err(err) => {
self.state = RequestBuilderState::Error(err);
return self;
}
};
let value = match value.into_header_value() {
Ok(value) => value,
Err(err) => {
self.state = RequestBuilderState::Error(err);
return self;
}
};
_ = headers.insert(key, value);
}
self.state = RequestBuilderState::PreBody(builder);
self
}
RequestBuilderState::PostBody(mut request) => {
let key = match key.into_header_name() {
Ok(key) => key,
Err(err) => {
self.state = RequestBuilderState::Error(err);
return self;
}
};
let value = match value.into_header_value() {
Ok(value) => value,
Err(err) => {
self.state = RequestBuilderState::Error(err);
return self;
}
};
_ = request.headers_mut().insert(key, value);
self.state = RequestBuilderState::PostBody(request);
self
}
RequestBuilderState::Error(err) => {
self.state = RequestBuilderState::Error(err);
self
}
}
}
#[must_use]
pub fn overwrite_typed_header<H>(self, header: H) -> Self
where
H: crate::headers::HeaderEncode,
{
if let Some(value) = header.encode_to_value() {
self.overwrite_header(H::name().clone(), value)
} else {
self
}
}
#[must_use]
pub fn auth(self, credentials: impl Credentials) -> Self {
let header = crate::headers::Authorization::new(credentials);
self.typed_header(header)
}
#[must_use]
pub fn extension<T>(mut self, extension: T) -> Self
where
T: Extension,
{
match self.state {
RequestBuilderState::PreBody(builder) => {
let builder = builder.extension(extension);
self.state = RequestBuilderState::PreBody(builder);
self
}
RequestBuilderState::PostBody(request) => {
request.extensions().insert(extension);
self.state = RequestBuilderState::PostBody(request);
self
}
state @ RequestBuilderState::Error(_) => {
self.state = state;
self
}
}
}
pub fn extensions(&self) -> Option<&Extensions> {
match &self.state {
RequestBuilderState::PreBody(builder) => builder.extensions(),
RequestBuilderState::PostBody(request) => Some(request.extensions()),
RequestBuilderState::Error(_) => None,
}
}
#[must_use]
pub fn body<T>(mut self, body: T) -> Self
where
T: TryInto<crate::Body, Error: Into<BoxError>>,
{
self.state = match self.state {
RequestBuilderState::PreBody(mut builder) => match body.try_into() {
Ok(body) => {
sync_content_length_pre_body(&mut builder, &body);
match builder.body(body) {
Ok(req) => RequestBuilderState::PostBody(req),
Err(err) => RequestBuilderState::Error(BoxError::from(err)),
}
}
Err(err) => RequestBuilderState::Error(err.into()),
},
RequestBuilderState::PostBody(mut req) => match body.try_into() {
Ok(body) => {
sync_content_length_post_body(&mut req, &body);
*req.body_mut() = body;
RequestBuilderState::PostBody(req)
}
Err(err) => RequestBuilderState::Error(BoxError::from(err.into())),
},
RequestBuilderState::Error(err) => RequestBuilderState::Error(err),
};
self
}
#[must_use]
pub fn form<T: serde::Serialize + ?Sized>(mut self, form: &T) -> Self {
self.state = match self.state {
RequestBuilderState::PreBody(mut builder) => match serde_html_form::to_string(form) {
Ok(body) => {
let len = body.len() as u64;
let builder = match builder.headers_mut() {
Some(headers) => {
if !headers.contains_key(crate::header::CONTENT_TYPE) {
headers.insert(
crate::header::CONTENT_TYPE,
crate::HeaderValue::from_static(
"application/x-www-form-urlencoded",
),
);
}
headers.insert(
crate::header::CONTENT_LENGTH,
crate::HeaderValue::from(len),
);
builder
}
None => builder
.header(
crate::header::CONTENT_TYPE,
crate::HeaderValue::from_static(
"application/x-www-form-urlencoded",
),
)
.header(crate::header::CONTENT_LENGTH, crate::HeaderValue::from(len)),
};
match builder.body(body.into()) {
Ok(req) => RequestBuilderState::PostBody(req),
Err(err) => RequestBuilderState::Error(BoxError::from(err)),
}
}
Err(err) => RequestBuilderState::Error(BoxError::from(err)),
},
RequestBuilderState::PostBody(mut req) => match serde_html_form::to_string(form) {
Ok(body) => {
let len = body.len() as u64;
if !req.headers().contains_key(crate::header::CONTENT_TYPE) {
req.headers_mut().insert(
crate::header::CONTENT_TYPE,
crate::HeaderValue::from_static("application/x-www-form-urlencoded"),
);
}
req.headers_mut()
.insert(crate::header::CONTENT_LENGTH, crate::HeaderValue::from(len));
*req.body_mut() = body.into();
RequestBuilderState::PostBody(req)
}
Err(err) => RequestBuilderState::Error(BoxError::from(err)),
},
RequestBuilderState::Error(err) => RequestBuilderState::Error(err),
};
self
}
#[must_use]
pub fn json<T: serde::Serialize + ?Sized>(mut self, json: &T) -> Self {
self.state = match self.state {
RequestBuilderState::PreBody(mut builder) => match serde_json::to_vec(json) {
Ok(body) => {
let len = body.len() as u64;
let builder = match builder.headers_mut() {
Some(headers) => {
if !headers.contains_key(crate::header::CONTENT_TYPE) {
headers.insert(
crate::header::CONTENT_TYPE,
crate::HeaderValue::from_static("application/json"),
);
}
headers.insert(
crate::header::CONTENT_LENGTH,
crate::HeaderValue::from(len),
);
builder
}
None => builder
.header(
crate::header::CONTENT_TYPE,
crate::HeaderValue::from_static("application/json"),
)
.header(crate::header::CONTENT_LENGTH, crate::HeaderValue::from(len)),
};
match builder.body(body.into()) {
Ok(req) => RequestBuilderState::PostBody(req),
Err(err) => RequestBuilderState::Error(BoxError::from(err)),
}
}
Err(err) => RequestBuilderState::Error(BoxError::from(err)),
},
RequestBuilderState::PostBody(mut req) => match serde_json::to_vec(json) {
Ok(body) => {
let len = body.len() as u64;
if !req.headers().contains_key(crate::header::CONTENT_TYPE) {
req.headers_mut().insert(
crate::header::CONTENT_TYPE,
crate::HeaderValue::from_static("application/json"),
);
}
req.headers_mut()
.insert(crate::header::CONTENT_LENGTH, crate::HeaderValue::from(len));
*req.body_mut() = body.into();
RequestBuilderState::PostBody(req)
}
Err(err) => RequestBuilderState::Error(BoxError::from(err)),
},
RequestBuilderState::Error(err) => RequestBuilderState::Error(err),
};
self
}
#[cfg(feature = "multipart")]
#[must_use]
pub fn multipart(mut self, form: super::multipart::Form) -> Self {
let content_type = form.content_type();
let content_length = form.content_length();
let body = form.into_body();
self.state = match self.state {
RequestBuilderState::PreBody(mut builder) => {
if let Some(headers) = builder.headers_mut() {
headers.insert(crate::header::CONTENT_TYPE, content_type);
sync_content_length_in_map(headers, content_length);
}
match builder.body(body) {
Ok(req) => RequestBuilderState::PostBody(req),
Err(err) => RequestBuilderState::Error(BoxError::from(err)),
}
}
RequestBuilderState::PostBody(mut req) => {
let headers = req.headers_mut();
headers.insert(crate::header::CONTENT_TYPE, content_type);
sync_content_length_in_map(headers, content_length);
*req.body_mut() = body;
RequestBuilderState::PostBody(req)
}
RequestBuilderState::Error(err) => RequestBuilderState::Error(err),
};
self
}
#[must_use]
pub fn octet_stream<T: Into<rama_core::bytes::Bytes>>(mut self, bytes: T) -> Self {
let bytes = bytes.into();
let len = bytes.len() as u64;
self.state = match self.state {
RequestBuilderState::PreBody(mut builder) => {
let builder = match builder.headers_mut() {
Some(headers) => {
if !headers.contains_key(crate::header::CONTENT_TYPE) {
headers.insert(
crate::header::CONTENT_TYPE,
crate::HeaderValue::from_static("application/octet-stream"),
);
}
headers
.insert(crate::header::CONTENT_LENGTH, crate::HeaderValue::from(len));
builder
}
None => builder
.header(
crate::header::CONTENT_TYPE,
crate::HeaderValue::from_static("application/octet-stream"),
)
.header(crate::header::CONTENT_LENGTH, crate::HeaderValue::from(len)),
};
match builder.body(bytes.into()) {
Ok(req) => RequestBuilderState::PostBody(req),
Err(err) => RequestBuilderState::Error(BoxError::from(err)),
}
}
RequestBuilderState::PostBody(mut req) => {
if !req.headers().contains_key(crate::header::CONTENT_TYPE) {
req.headers_mut().insert(
crate::header::CONTENT_TYPE,
crate::HeaderValue::from_static("application/octet-stream"),
);
}
req.headers_mut()
.insert(crate::header::CONTENT_LENGTH, crate::HeaderValue::from(len));
*req.body_mut() = bytes.into();
RequestBuilderState::PostBody(req)
}
RequestBuilderState::Error(err) => RequestBuilderState::Error(err),
};
self
}
#[must_use]
pub fn version(mut self, version: crate::Version) -> Self {
match self.state {
RequestBuilderState::PreBody(builder) => {
self.state = RequestBuilderState::PreBody(builder.version(version));
self
}
RequestBuilderState::PostBody(mut request) => {
*request.version_mut() = version;
self.state = RequestBuilderState::PostBody(request);
self
}
RequestBuilderState::Error(err) => {
self.state = RequestBuilderState::Error(err);
self
}
}
}
pub async fn try_into_request(self) -> Result<Request, BoxError> {
Ok(match self.state {
RequestBuilderState::PreBody(builder) => builder.body(crate::Body::empty())?,
RequestBuilderState::PostBody(request) => request,
RequestBuilderState::Error(err) => return Err(err),
})
}
pub async fn send(self) -> Result<Response<Body>, BoxError> {
let request = match self.state {
RequestBuilderState::PreBody(builder) => builder.body(crate::Body::empty())?,
RequestBuilderState::PostBody(request) => request,
RequestBuilderState::Error(err) => return Err(err),
};
let uri = request.uri().clone();
match self.http_client_service.serve(request).await {
Ok(response) => Ok(response),
Err(err) => Err(err.into().context(uri)),
}
}
}
fn sync_content_length_pre_body(builder: &mut crate::request::Builder, body: &crate::Body) {
let Some(headers) = builder.headers_mut() else {
return;
};
let exact = body.size_hint().exact();
sync_content_length_in_map(headers, exact);
}
fn sync_content_length_post_body<B>(req: &mut crate::Request<B>, body: &crate::Body) {
let exact = body.size_hint().exact();
sync_content_length_in_map(req.headers_mut(), exact);
}
fn sync_content_length_in_map(headers: &mut crate::HeaderMap, exact: Option<u64>) {
match exact {
Some(len) => {
headers.insert(crate::header::CONTENT_LENGTH, crate::HeaderValue::from(len));
}
None => {
headers.remove(crate::header::CONTENT_LENGTH);
}
}
}
#[cfg(test)]
mod test {
use rama_http_types::StatusCode;
use super::*;
use crate::{
StreamingBody,
layer::{
required_header::AddRequiredRequestHeadersLayer,
retry::{ManagedPolicy, RetryLayer},
trace::TraceLayer,
},
service::web::response::IntoResponse,
};
use rama_core::{
layer::{Layer, MapResultLayer},
service::{BoxService, service_fn},
};
use rama_utils::backoff::ExponentialBackoff;
use std::convert::Infallible;
async fn fake_client_fn<Body>(request: Request<Body>) -> Result<Response, Infallible>
where
Body: StreamingBody<Data: Send + 'static, Error: Send + 'static> + Send + 'static,
{
let ua = request.headers().get(crate::header::USER_AGENT).unwrap();
assert_eq!(
ua.to_str().unwrap(),
format!("{}/{}", rama_utils::info::NAME, rama_utils::info::VERSION)
);
Ok(StatusCode::OK.into_response())
}
fn map_internal_client_error<E, Body>(
result: Result<Response<Body>, E>,
) -> Result<Response, rama_core::error::BoxError>
where
E: Into<rama_core::error::BoxError>,
Body: StreamingBody<Data = rama_core::bytes::Bytes, Error: Into<BoxError>>
+ Send
+ Sync
+ 'static,
{
match result {
Ok(response) => Ok(response.map(crate::Body::new)),
Err(err) => Err(err.into()),
}
}
type HttpClient = BoxService<Request, Response, BoxError>;
fn client() -> HttpClient {
let builder = (
MapResultLayer::new(map_internal_client_error),
TraceLayer::new_for_http(),
);
#[cfg(feature = "compression")]
let builder = (
builder,
crate::layer::decompression::DecompressionLayer::new(),
);
(
builder,
RetryLayer::new(ManagedPolicy::default().with_backoff(ExponentialBackoff::default())),
AddRequiredRequestHeadersLayer::default(),
)
.into_layer(service_fn(fake_client_fn))
.boxed()
}
#[tokio::test]
async fn test_client_happy_path() {
let response = client().get("http://127.0.0.1:8080").send().await.unwrap();
assert_eq!(response.status(), StatusCode::OK);
}
async fn echo_headers_client_fn(request: Request<crate::Body>) -> Result<Response, Infallible> {
let mut headers_lines = Vec::new();
for (name, value) in request.headers() {
headers_lines.push(format!(
"{}: {}",
name.as_str(),
value.to_str().unwrap_or("<bin>")
));
}
let body = headers_lines.join("\n");
Ok(crate::Response::new(crate::Body::from(body)))
}
fn echo_client() -> HttpClient {
let builder = (
MapResultLayer::new(map_internal_client_error),
TraceLayer::new_for_http(),
);
(builder,)
.into_layer(service_fn(echo_headers_client_fn))
.boxed()
}
async fn dump_headers(resp: Response) -> String {
use crate::body::util::BodyExt as _;
let bytes = resp.into_body().collect().await.unwrap().to_bytes();
String::from_utf8(bytes.to_vec()).unwrap()
}
#[tokio::test]
async fn test_form_helper_sets_content_length_and_overwrites_stale() {
let resp = echo_client()
.post("http://x/")
.header(crate::header::CONTENT_LENGTH, "9999") .form(&[("k", "vvv")])
.send()
.await
.unwrap();
let dump = dump_headers(resp).await;
assert!(
dump.contains("content-length: 5"),
"expected content-length: 5 in:\n{dump}"
);
assert!(
dump.contains("content-type: application/x-www-form-urlencoded"),
"missing CT in:\n{dump}"
);
}
#[tokio::test]
async fn test_json_helper_sets_content_length_and_overwrites_stale() {
let resp = echo_client()
.post("http://x/")
.header(crate::header::CONTENT_LENGTH, "9999")
.json(&serde_json::json!({"a": 1}))
.send()
.await
.unwrap();
let dump = dump_headers(resp).await;
assert!(
dump.contains("content-length: 7"),
"expected content-length: 7 in:\n{dump}"
);
assert!(dump.contains("content-type: application/json"));
}
#[tokio::test]
async fn test_octet_stream_helper_sets_content_length_and_overwrites_stale() {
let resp = echo_client()
.post("http://x/")
.header(crate::header::CONTENT_LENGTH, "9999")
.octet_stream(b"abcd".as_slice())
.send()
.await
.unwrap();
let dump = dump_headers(resp).await;
assert!(
dump.contains("content-length: 4"),
"expected content-length: 4 in:\n{dump}"
);
assert!(dump.contains("content-type: application/octet-stream"));
}
#[tokio::test]
async fn test_body_helper_clears_content_length_for_streaming_body() {
use rama_core::futures::stream;
let stream = stream::iter([Ok::<_, BoxError>(rama_core::bytes::Bytes::from_static(
b"chunk",
))]);
let body = crate::Body::from_stream(stream);
let resp = echo_client()
.post("http://x/")
.header(crate::header::CONTENT_LENGTH, "9999")
.body(body)
.send()
.await
.unwrap();
let dump = dump_headers(resp).await;
assert!(
!dump.to_ascii_lowercase().contains("content-length:"),
"stale content-length leaked into:\n{dump}"
);
}
}