Compare commits

...

5 Commits

Author SHA1 Message Date
Joel Wurtz f61dff3d26
Merge adf3a06805 into fcd10fbb5e 2025-03-22 07:09:15 +01:00
dependabot[bot] fcd10fbb5e
build(deps): bump taiki-e/install-action from 2.49.17 to 2.49.27 (#3600)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.49.17 to 2.49.27.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.49.17...v2.49.27)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-21 06:58:14 +00:00
Rob Ede 95b6a81f43
refactor: switch size parsing to bytesize crate 2025-03-21 06:06:50 +00:00
Rob Ede ab18efe0ac
chore: check in lockfile 2025-03-21 05:51:45 +00:00
Joel Wurtz adf3a06805
feat(awc): allow to retrieve request head in client response 2024-12-16 15:40:23 +01:00
24 changed files with 4072 additions and 68 deletions

3
.cspell.yml Normal file
View File

@ -0,0 +1,3 @@
version: "0.2"
words:
- actix

View File

@ -49,7 +49,7 @@ jobs:
toolchain: ${{ matrix.version.version }}
- name: Install just, cargo-hack, cargo-nextest, cargo-ci-cache-clean
uses: taiki-e/install-action@v2.49.17
uses: taiki-e/install-action@v2.49.32
with:
tool: just,cargo-hack,cargo-nextest,cargo-ci-cache-clean
@ -83,7 +83,7 @@ jobs:
uses: actions-rust-lang/setup-rust-toolchain@v1.11.0
- name: Install just, cargo-hack
uses: taiki-e/install-action@v2.49.17
uses: taiki-e/install-action@v2.49.32
with:
tool: just,cargo-hack

View File

@ -64,7 +64,7 @@ jobs:
toolchain: ${{ matrix.version.version }}
- name: Install just, cargo-hack, cargo-nextest, cargo-ci-cache-clean
uses: taiki-e/install-action@v2.49.17
uses: taiki-e/install-action@v2.49.32
with:
tool: just,cargo-hack,cargo-nextest,cargo-ci-cache-clean
@ -113,7 +113,7 @@ jobs:
toolchain: nightly
- name: Install just
uses: taiki-e/install-action@v2.49.17
uses: taiki-e/install-action@v2.49.32
with:
tool: just

View File

@ -24,7 +24,7 @@ jobs:
components: llvm-tools
- name: Install just, cargo-llvm-cov, cargo-nextest
uses: taiki-e/install-action@v2.49.17
uses: taiki-e/install-action@v2.49.32
with:
tool: just,cargo-llvm-cov,cargo-nextest

View File

@ -77,7 +77,7 @@ jobs:
toolchain: ${{ vars.RUST_VERSION_EXTERNAL_TYPES }}
- name: Install just
uses: taiki-e/install-action@v2.49.17
uses: taiki-e/install-action@v2.49.32
with:
tool: just

1
.gitignore vendored
View File

@ -1,4 +1,3 @@
Cargo.lock
target/
guide/build/
/gh-pages

3970
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -188,16 +188,16 @@ impl Decoder for ClientPayloadCodec {
}
}
impl Encoder<Message<(RequestHeadType, BodySize)>> for ClientCodec {
impl Encoder<Message<(&mut RequestHeadType, BodySize)>> for ClientCodec {
type Error = io::Error;
fn encode(
&mut self,
item: Message<(RequestHeadType, BodySize)>,
item: Message<(&mut RequestHeadType, BodySize)>,
dst: &mut BytesMut,
) -> Result<(), Self::Error> {
match item {
Message::Item((mut head, length)) => {
Message::Item((head, length)) => {
let inner = &mut self.inner;
inner.version = head.as_ref().version;
inner
@ -219,7 +219,7 @@ impl Encoder<Message<(RequestHeadType, BodySize)>> for ClientCodec {
inner.encoder.encode(
dst,
&mut head,
head,
false,
false,
inner.version,

View File

@ -18,8 +18,8 @@ all-features = true
proc-macro = true
[dependencies]
bytesize = "2"
darling = "0.20"
parse-size = "1"
proc-macro2 = "1"
quote = "1"
syn = "2"
@ -27,7 +27,7 @@ syn = "2"
[dev-dependencies]
actix-multipart = "0.7"
actix-web = "4"
rustversion = "1"
rustversion-msrv = "0.100"
trybuild = "1"
[lints]

View File

@ -9,8 +9,8 @@
use std::collections::HashSet;
use bytesize::ByteSize;
use darling::{FromDeriveInput, FromField, FromMeta};
use parse_size::parse_size;
use proc_macro::TokenStream;
use proc_macro2::Ident;
use quote::quote;
@ -103,7 +103,7 @@ struct ParsedField<'t> {
/// # Field Limits
///
/// You can use the `#[multipart(limit = "<size>")]` attribute to set field level limits. The limit
/// string is parsed using [parse_size].
/// string is parsed using [`bytesize`].
///
/// Note: the form is also subject to the global limits configured using `MultipartFormConfig`.
///
@ -150,7 +150,7 @@ struct ParsedField<'t> {
/// struct Form { }
/// ```
///
/// [parse_size]: https://docs.rs/parse-size/1/parse_size
/// [`bytesize`]: https://docs.rs/bytesize/2
#[proc_macro_derive(MultipartForm, attributes(multipart))]
pub fn impl_multipart_form(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let input: syn::DeriveInput = parse_macro_input!(input);
@ -191,8 +191,8 @@ pub fn impl_multipart_form(input: proc_macro::TokenStream) -> proc_macro::TokenS
let attrs = FieldAttrs::from_field(field).map_err(|err| err.write_errors())?;
let serialization_name = attrs.rename.unwrap_or_else(|| rust_name.to_string());
let limit = match attrs.limit.map(|limit| match parse_size(&limit) {
Ok(size) => Ok(usize::try_from(size).unwrap()),
let limit = match attrs.limit.map(|limit| match limit.parse::<ByteSize>() {
Ok(ByteSize(size)) => Ok(usize::try_from(size).unwrap()),
Err(err) => Err(syn::Error::new(
field.ident.as_ref().unwrap().span(),
format!("Could not parse size limit `{}`: {}", limit, err),

View File

@ -1,4 +1,4 @@
#[rustversion::stable(1.72)] // MSRV
#[rustversion_msrv::msrv]
#[test]
fn compile_macros() {
let t = trybuild::TestCases::new();

View File

@ -1,16 +1,16 @@
error: Could not parse size limit `2 bytes`: invalid digit found in string
error: Could not parse size limit `2 bytes`: couldn't parse "bytes" into a known SI unit, couldn't parse unit of "bytes"
--> tests/trybuild/size-limit-parse-fail.rs:6:5
|
6 | description: Text<String>,
| ^^^^^^^^^^^
error: Could not parse size limit `2 megabytes`: invalid digit found in string
error: Could not parse size limit `2 megabytes`: couldn't parse "megabytes" into a known SI unit, couldn't parse unit of "megabytes"
--> tests/trybuild/size-limit-parse-fail.rs:12:5
|
12 | description: Text<String>,
| ^^^^^^^^^^^
error: Could not parse size limit `four meters`: invalid digit found in string
error: Could not parse size limit `four meters`: couldn't parse "four meters" into a ByteSize, cannot parse float from empty string
--> tests/trybuild/size-limit-parse-fail.rs:18:5
|
18 | description: Text<String>,

View File

@ -34,7 +34,7 @@ actix-web = "4"
futures-core = { version = "0.3.17", default-features = false, features = ["alloc"] }
trybuild = "1"
rustversion = "1"
rustversion-msrv = "0.100"
[lints]
workspace = true

View File

@ -1,4 +1,4 @@
#[rustversion::stable(1.72)] // MSRV
#[rustversion_msrv::msrv]
#[test]
fn compile_macros() {
let t = trybuild::TestCases::new();

View File

@ -8,6 +8,7 @@
- Do not send `Host` header on HTTP/2 requests, as it is not required, and some web servers may reject it.
- Update `brotli` dependency to `7`.
- Minimum supported Rust version (MSRV) is now 1.75.
- Allow to retrieve request head used to send the http request on `ClientResponse`
## 3.5.1

View File

@ -243,7 +243,7 @@ where
self,
head: H,
body: RB,
) -> LocalBoxFuture<'static, Result<(ResponseHead, Payload), SendRequestError>>
) -> LocalBoxFuture<'static, Result<(RequestHeadType, ResponseHead, Payload), SendRequestError>>
where
H: Into<RequestHeadType> + 'static,
RB: MessageBody + 'static,
@ -273,17 +273,24 @@ where
head: H,
) -> LocalBoxFuture<
'static,
Result<(ResponseHead, Framed<Connection<A, B>, ClientCodec>), SendRequestError>,
Result<
(
RequestHeadType,
ResponseHead,
Framed<Connection<A, B>, ClientCodec>,
),
SendRequestError,
>,
> {
Box::pin(async move {
match self {
Connection::Tcp(ConnectionType::H1(ref _conn)) => {
let (head, framed) = h1proto::open_tunnel(self, head.into()).await?;
Ok((head, framed))
let (head, res_head, framed) = h1proto::open_tunnel(self, head.into()).await?;
Ok((head, res_head, framed))
}
Connection::Tls(ConnectionType::H1(ref _conn)) => {
let (head, framed) = h1proto::open_tunnel(self, head.into()).await?;
Ok((head, framed))
let (head, res_head, framed) = h1proto::open_tunnel(self, head.into()).await?;
Ok((head, res_head, framed))
}
Connection::Tls(ConnectionType::H2(mut conn)) => {
conn.release();

View File

@ -28,7 +28,7 @@ pub(crate) async fn send_request<Io, B>(
io: H1Connection<Io>,
mut head: RequestHeadType,
body: B,
) -> Result<(ResponseHead, Payload), SendRequestError>
) -> Result<(RequestHeadType, ResponseHead, Payload), SendRequestError>
where
Io: ConnectionIo,
B: MessageBody,
@ -86,7 +86,7 @@ where
// special handle for EXPECT request.
let (do_send, mut res_head) = if is_expect {
pin_framed.send((head, body.size()).into()).await?;
pin_framed.send((&mut head, body.size()).into()).await?;
let head = poll_fn(|cx| pin_framed.as_mut().poll_next(cx))
.await
@ -96,7 +96,7 @@ where
// and current head would be used as final response head.
(head.status == StatusCode::CONTINUE, Some(head))
} else {
pin_framed.feed((head, body.size()).into()).await?;
pin_framed.feed((&mut head, body.size()).into()).await?;
(true, None)
};
@ -118,17 +118,16 @@ where
res_head = Some(head);
}
let head = res_head.unwrap();
match pin_framed.codec_ref().message_type() {
h1::MessageType::None => {
let keep_alive = pin_framed.codec_ref().keep_alive();
pin_framed.io_mut().on_release(keep_alive);
Ok((head, Payload::None))
Ok((head, res_head.unwrap(), Payload::None))
}
_ => Ok((
head,
res_head.unwrap(),
Payload::Stream {
payload: Box::pin(PlStream::new(framed)),
},
@ -138,21 +137,21 @@ where
pub(crate) async fn open_tunnel<Io>(
io: Io,
head: RequestHeadType,
) -> Result<(ResponseHead, Framed<Io, h1::ClientCodec>), SendRequestError>
mut head: RequestHeadType,
) -> Result<(RequestHeadType, ResponseHead, Framed<Io, h1::ClientCodec>), SendRequestError>
where
Io: ConnectionIo,
{
// create Framed and send request.
let mut framed = Framed::new(io, h1::ClientCodec::default());
framed.send((head, BodySize::None).into()).await?;
framed.send((&mut head, BodySize::None).into()).await?;
// read response head.
let head = poll_fn(|cx| Pin::new(&mut framed).poll_next(cx))
let res_head = poll_fn(|cx| Pin::new(&mut framed).poll_next(cx))
.await
.ok_or(ConnectError::Disconnected)??;
Ok((head, framed))
Ok((head, res_head, framed))
}
/// send request body to the peer

View File

@ -29,7 +29,7 @@ pub(crate) async fn send_request<Io, B>(
mut io: H2Connection<Io>,
head: RequestHeadType,
body: B,
) -> Result<(ResponseHead, Payload), SendRequestError>
) -> Result<(RequestHeadType, ResponseHead, Payload), SendRequestError>
where
Io: ConnectionIo,
B: MessageBody,
@ -129,10 +129,10 @@ where
let (parts, body) = resp.into_parts();
let payload = if head_req { Payload::None } else { body.into() };
let mut head = ResponseHead::new(parts.status);
head.version = parts.version;
head.headers = parts.headers.into();
Ok((head, payload))
let mut res_head = ResponseHead::new(parts.status);
res_head.version = parts.version;
res_head.headers = parts.headers.into();
Ok((head, res_head, payload))
}
async fn send_body<B>(body: B, mut send: SendStream<Bytes>) -> Result<(), SendRequestError>

View File

@ -49,7 +49,11 @@ pub enum ConnectResponse {
/// Tunnel used for WebSocket communication.
///
/// Contains response head and framed HTTP/1.1 codec.
Tunnel(ResponseHead, Framed<BoxedSocket, ClientCodec>),
Tunnel(
RequestHeadType,
ResponseHead,
Framed<BoxedSocket, ClientCodec>,
),
}
impl ConnectResponse {
@ -70,9 +74,15 @@ impl ConnectResponse {
///
/// # Panics
/// Panics if enum variant is not `Tunnel`.
pub fn into_tunnel_response(self) -> (ResponseHead, Framed<BoxedSocket, ClientCodec>) {
pub fn into_tunnel_response(
self,
) -> (
RequestHeadType,
ResponseHead,
Framed<BoxedSocket, ClientCodec>,
) {
match self {
ConnectResponse::Tunnel(head, framed) => (head, framed),
ConnectResponse::Tunnel(req, head, framed) => (req, head, framed),
_ => {
panic!("TunnelResponse only reachable with ConnectResponse::TunnelResponse variant")
}
@ -133,12 +143,12 @@ pin_project_lite::pin_project! {
req: Option<ConnectRequest>
},
Client {
fut: LocalBoxFuture<'static, Result<(ResponseHead, Payload), SendRequestError>>
fut: LocalBoxFuture<'static, Result<(RequestHeadType, ResponseHead, Payload), SendRequestError>>
},
Tunnel {
fut: LocalBoxFuture<
'static,
Result<(ResponseHead, Framed<Connection<Io>, ClientCodec>), SendRequestError>,
Result<(RequestHeadType, ResponseHead, Framed<Connection<Io>, ClientCodec>), SendRequestError>,
>,
}
}
@ -181,16 +191,16 @@ where
}
ConnectRequestProj::Client { fut } => {
let (head, payload) = ready!(fut.as_mut().poll(cx))?;
let (req, head, payload) = ready!(fut.as_mut().poll(cx))?;
Poll::Ready(Ok(ConnectResponse::Client(ClientResponse::new(
head, payload,
req, head, payload,
))))
}
ConnectRequestProj::Tunnel { fut } => {
let (head, framed) = ready!(fut.as_mut().poll(cx))?;
let (req, head, framed) = ready!(fut.as_mut().poll(cx))?;
let framed = framed.into_map_io(|io| Box::new(io) as _);
Poll::Ready(Ok(ConnectResponse::Tunnel(head, framed)))
Poll::Ready(Ok(ConnectResponse::Tunnel(req, head, framed)))
}
}
}

View File

@ -329,6 +329,7 @@ mod tests {
let res = client.get(srv.url("/")).send().await.unwrap();
assert_eq!(res.status().as_u16(), 400);
assert_eq!(res.req_head().uri.path(), "/test");
}
#[actix_rt::test]

View File

@ -8,7 +8,7 @@ use std::{
use actix_http::{
error::PayloadError, header::HeaderMap, BoxedPayloadStream, Extensions, HttpMessage, Payload,
ResponseHead, StatusCode, Version,
RequestHead, RequestHeadType, ResponseHead, StatusCode, Version,
};
use actix_rt::time::{sleep, Sleep};
use bytes::Bytes;
@ -23,6 +23,7 @@ use crate::cookie::{Cookie, ParseError as CookieParseError};
pin_project! {
/// Client Response
pub struct ClientResponse<S = BoxedPayloadStream> {
pub(crate) req_head: RequestHeadType,
pub(crate) head: ResponseHead,
#[pin]
pub(crate) payload: Payload<S>,
@ -34,8 +35,9 @@ pin_project! {
impl<S> ClientResponse<S> {
/// Create new Request instance
pub(crate) fn new(head: ResponseHead, payload: Payload<S>) -> Self {
pub(crate) fn new(req_head: RequestHeadType, head: ResponseHead, payload: Payload<S>) -> Self {
ClientResponse {
req_head,
head,
payload,
timeout: ResponseTimeout::default(),
@ -43,6 +45,12 @@ impl<S> ClientResponse<S> {
}
}
/// Returns the request head used to send the request.
#[inline]
pub fn req_head(&self) -> &RequestHead {
self.req_head.as_ref()
}
#[inline]
pub(crate) fn head(&self) -> &ResponseHead {
&self.head
@ -77,6 +85,7 @@ impl<S> ClientResponse<S> {
ClientResponse {
payload,
req_head: self.req_head,
head: self.head,
timeout: self.timeout,
extensions: self.extensions,
@ -105,6 +114,7 @@ impl<S> ClientResponse<S> {
Self {
payload: self.payload,
head: self.head,
req_head: self.req_head,
timeout,
extensions: self.extensions,
}

View File

@ -1,6 +1,8 @@
//! Test helpers for actix http client to use during testing.
use actix_http::{h1, header::TryIntoHeaderPair, Payload, ResponseHead, StatusCode, Version};
use actix_http::{
h1, header::TryIntoHeaderPair, Payload, RequestHead, ResponseHead, StatusCode, Version,
};
use bytes::Bytes;
#[cfg(feature = "cookies")]
@ -9,6 +11,7 @@ use crate::ClientResponse;
/// Test `ClientResponse` builder
pub struct TestResponse {
req_head: RequestHead,
head: ResponseHead,
#[cfg(feature = "cookies")]
cookies: CookieJar,
@ -18,6 +21,7 @@ pub struct TestResponse {
impl Default for TestResponse {
fn default() -> TestResponse {
TestResponse {
req_head: RequestHead::default(),
head: ResponseHead::new(StatusCode::OK),
#[cfg(feature = "cookies")]
cookies: CookieJar::new(),
@ -86,10 +90,10 @@ impl TestResponse {
}
if let Some(pl) = self.payload {
ClientResponse::new(head, pl)
ClientResponse::new(self.req_head.into(), head, pl)
} else {
let (_, payload) = h1::Payload::create(true);
ClientResponse::new(head, payload.into())
ClientResponse::new(self.req_head.into(), head, payload.into())
}
}
}

View File

@ -351,7 +351,7 @@ impl WebsocketsRequest {
fut.await?
};
let (head, framed) = res.into_tunnel_response();
let (req_head, head, framed) = res.into_tunnel_response();
// verify response
if head.status != StatusCode::SWITCHING_PROTOCOLS {
@ -411,7 +411,7 @@ impl WebsocketsRequest {
// response and ws framed
Ok((
ClientResponse::new(head, Payload::None),
ClientResponse::new(req_head, head, Payload::None),
framed.into_map_codec(|_| {
if server_mode {
ws::Codec::new().max_size(max_size)

View File

@ -7,14 +7,14 @@ fmt:
cargo +nightly fmt
fd --hidden --type=file --extension=md --extension=yml --exec-batch npx -y prettier --write
# Downgrade dev-dependencies necessary to run MSRV checks/tests.
# Downgrade dependencies necessary to run MSRV checks/tests.
[private]
downgrade-for-msrv:
cargo update -p=parse-size --precise=1.0.0
cargo update -p=clap --precise=4.4.18
cargo update -p=divan --precise=0.1.15
cargo update -p=litemap --precise=0.7.4
cargo update -p=zerofrom --precise=0.1.5
cargo update -p=clap --precise=4.4.18 # next ver: 1.74.0
cargo update -p=divan --precise=0.1.15 # next ver: 1.80.0
cargo update -p=litemap --precise=0.7.4 # next ver: 1.81.0
cargo update -p=zerofrom --precise=0.1.5 # next ver: 1.81.0
cargo update -p=half --precise=2.4.1 # next ver: 1.81.0
msrv := ```
cargo metadata --format-version=1 \