wreq-rb 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. checksums.yaml +7 -0
  2. data/Cargo.lock +2688 -0
  3. data/Cargo.toml +6 -0
  4. data/README.md +179 -0
  5. data/ext/wreq_rb/Cargo.toml +39 -0
  6. data/ext/wreq_rb/extconf.rb +22 -0
  7. data/ext/wreq_rb/src/client.rs +565 -0
  8. data/ext/wreq_rb/src/error.rs +25 -0
  9. data/ext/wreq_rb/src/lib.rs +20 -0
  10. data/ext/wreq_rb/src/response.rs +132 -0
  11. data/lib/wreq-rb/version.rb +5 -0
  12. data/lib/wreq-rb.rb +17 -0
  13. data/patches/0001-add-transfer-size-tracking.patch +292 -0
  14. data/vendor/wreq/Cargo.toml +306 -0
  15. data/vendor/wreq/LICENSE +202 -0
  16. data/vendor/wreq/README.md +122 -0
  17. data/vendor/wreq/examples/cert_store.rs +77 -0
  18. data/vendor/wreq/examples/connect_via_lower_priority_tokio_runtime.rs +258 -0
  19. data/vendor/wreq/examples/emulation.rs +118 -0
  20. data/vendor/wreq/examples/form.rs +14 -0
  21. data/vendor/wreq/examples/http1_websocket.rs +37 -0
  22. data/vendor/wreq/examples/http2_websocket.rs +45 -0
  23. data/vendor/wreq/examples/json_dynamic.rs +41 -0
  24. data/vendor/wreq/examples/json_typed.rs +47 -0
  25. data/vendor/wreq/examples/keylog.rs +16 -0
  26. data/vendor/wreq/examples/request_with_emulation.rs +115 -0
  27. data/vendor/wreq/examples/request_with_interface.rs +37 -0
  28. data/vendor/wreq/examples/request_with_local_address.rs +16 -0
  29. data/vendor/wreq/examples/request_with_proxy.rs +13 -0
  30. data/vendor/wreq/examples/request_with_redirect.rs +22 -0
  31. data/vendor/wreq/examples/request_with_version.rs +15 -0
  32. data/vendor/wreq/examples/tor_socks.rs +24 -0
  33. data/vendor/wreq/examples/unix_socket.rs +33 -0
  34. data/vendor/wreq/src/client/body.rs +304 -0
  35. data/vendor/wreq/src/client/conn/conn.rs +231 -0
  36. data/vendor/wreq/src/client/conn/connector.rs +549 -0
  37. data/vendor/wreq/src/client/conn/http.rs +1023 -0
  38. data/vendor/wreq/src/client/conn/proxy/socks.rs +233 -0
  39. data/vendor/wreq/src/client/conn/proxy/tunnel.rs +260 -0
  40. data/vendor/wreq/src/client/conn/proxy.rs +39 -0
  41. data/vendor/wreq/src/client/conn/tls_info.rs +98 -0
  42. data/vendor/wreq/src/client/conn/uds.rs +44 -0
  43. data/vendor/wreq/src/client/conn/verbose.rs +149 -0
  44. data/vendor/wreq/src/client/conn.rs +323 -0
  45. data/vendor/wreq/src/client/core/body/incoming.rs +485 -0
  46. data/vendor/wreq/src/client/core/body/length.rs +118 -0
  47. data/vendor/wreq/src/client/core/body.rs +34 -0
  48. data/vendor/wreq/src/client/core/common/buf.rs +149 -0
  49. data/vendor/wreq/src/client/core/common/rewind.rs +141 -0
  50. data/vendor/wreq/src/client/core/common/watch.rs +76 -0
  51. data/vendor/wreq/src/client/core/common.rs +3 -0
  52. data/vendor/wreq/src/client/core/conn/http1.rs +342 -0
  53. data/vendor/wreq/src/client/core/conn/http2.rs +307 -0
  54. data/vendor/wreq/src/client/core/conn.rs +11 -0
  55. data/vendor/wreq/src/client/core/dispatch.rs +299 -0
  56. data/vendor/wreq/src/client/core/error.rs +435 -0
  57. data/vendor/wreq/src/client/core/ext.rs +201 -0
  58. data/vendor/wreq/src/client/core/http1.rs +178 -0
  59. data/vendor/wreq/src/client/core/http2.rs +483 -0
  60. data/vendor/wreq/src/client/core/proto/h1/conn.rs +988 -0
  61. data/vendor/wreq/src/client/core/proto/h1/decode.rs +1170 -0
  62. data/vendor/wreq/src/client/core/proto/h1/dispatch.rs +684 -0
  63. data/vendor/wreq/src/client/core/proto/h1/encode.rs +580 -0
  64. data/vendor/wreq/src/client/core/proto/h1/io.rs +879 -0
  65. data/vendor/wreq/src/client/core/proto/h1/role.rs +694 -0
  66. data/vendor/wreq/src/client/core/proto/h1.rs +104 -0
  67. data/vendor/wreq/src/client/core/proto/h2/client.rs +650 -0
  68. data/vendor/wreq/src/client/core/proto/h2/ping.rs +539 -0
  69. data/vendor/wreq/src/client/core/proto/h2.rs +379 -0
  70. data/vendor/wreq/src/client/core/proto/headers.rs +138 -0
  71. data/vendor/wreq/src/client/core/proto.rs +58 -0
  72. data/vendor/wreq/src/client/core/rt/bounds.rs +57 -0
  73. data/vendor/wreq/src/client/core/rt/timer.rs +150 -0
  74. data/vendor/wreq/src/client/core/rt/tokio.rs +99 -0
  75. data/vendor/wreq/src/client/core/rt.rs +25 -0
  76. data/vendor/wreq/src/client/core/upgrade.rs +267 -0
  77. data/vendor/wreq/src/client/core.rs +16 -0
  78. data/vendor/wreq/src/client/emulation.rs +161 -0
  79. data/vendor/wreq/src/client/http/client/error.rs +142 -0
  80. data/vendor/wreq/src/client/http/client/exec.rs +29 -0
  81. data/vendor/wreq/src/client/http/client/extra.rs +77 -0
  82. data/vendor/wreq/src/client/http/client/lazy.rs +79 -0
  83. data/vendor/wreq/src/client/http/client/pool.rs +1105 -0
  84. data/vendor/wreq/src/client/http/client/util.rs +104 -0
  85. data/vendor/wreq/src/client/http/client.rs +1003 -0
  86. data/vendor/wreq/src/client/http/future.rs +99 -0
  87. data/vendor/wreq/src/client/http.rs +1629 -0
  88. data/vendor/wreq/src/client/layer/config/options.rs +156 -0
  89. data/vendor/wreq/src/client/layer/config.rs +116 -0
  90. data/vendor/wreq/src/client/layer/cookie.rs +161 -0
  91. data/vendor/wreq/src/client/layer/decoder.rs +139 -0
  92. data/vendor/wreq/src/client/layer/redirect/future.rs +270 -0
  93. data/vendor/wreq/src/client/layer/redirect/policy.rs +63 -0
  94. data/vendor/wreq/src/client/layer/redirect.rs +145 -0
  95. data/vendor/wreq/src/client/layer/retry/classify.rs +105 -0
  96. data/vendor/wreq/src/client/layer/retry/scope.rs +51 -0
  97. data/vendor/wreq/src/client/layer/retry.rs +151 -0
  98. data/vendor/wreq/src/client/layer/timeout/body.rs +233 -0
  99. data/vendor/wreq/src/client/layer/timeout/future.rs +90 -0
  100. data/vendor/wreq/src/client/layer/timeout.rs +177 -0
  101. data/vendor/wreq/src/client/layer.rs +15 -0
  102. data/vendor/wreq/src/client/multipart.rs +717 -0
  103. data/vendor/wreq/src/client/request.rs +818 -0
  104. data/vendor/wreq/src/client/response.rs +534 -0
  105. data/vendor/wreq/src/client/ws/json.rs +99 -0
  106. data/vendor/wreq/src/client/ws/message.rs +453 -0
  107. data/vendor/wreq/src/client/ws.rs +714 -0
  108. data/vendor/wreq/src/client.rs +27 -0
  109. data/vendor/wreq/src/config.rs +140 -0
  110. data/vendor/wreq/src/cookie.rs +579 -0
  111. data/vendor/wreq/src/dns/gai.rs +249 -0
  112. data/vendor/wreq/src/dns/hickory.rs +78 -0
  113. data/vendor/wreq/src/dns/resolve.rs +180 -0
  114. data/vendor/wreq/src/dns.rs +69 -0
  115. data/vendor/wreq/src/error.rs +502 -0
  116. data/vendor/wreq/src/ext.rs +398 -0
  117. data/vendor/wreq/src/hash.rs +143 -0
  118. data/vendor/wreq/src/header.rs +506 -0
  119. data/vendor/wreq/src/into_uri.rs +187 -0
  120. data/vendor/wreq/src/lib.rs +586 -0
  121. data/vendor/wreq/src/proxy/mac.rs +82 -0
  122. data/vendor/wreq/src/proxy/matcher.rs +806 -0
  123. data/vendor/wreq/src/proxy/uds.rs +66 -0
  124. data/vendor/wreq/src/proxy/win.rs +31 -0
  125. data/vendor/wreq/src/proxy.rs +569 -0
  126. data/vendor/wreq/src/redirect.rs +575 -0
  127. data/vendor/wreq/src/retry.rs +198 -0
  128. data/vendor/wreq/src/sync.rs +129 -0
  129. data/vendor/wreq/src/tls/conn/cache.rs +123 -0
  130. data/vendor/wreq/src/tls/conn/cert_compression.rs +125 -0
  131. data/vendor/wreq/src/tls/conn/ext.rs +82 -0
  132. data/vendor/wreq/src/tls/conn/macros.rs +34 -0
  133. data/vendor/wreq/src/tls/conn/service.rs +138 -0
  134. data/vendor/wreq/src/tls/conn.rs +681 -0
  135. data/vendor/wreq/src/tls/keylog/handle.rs +64 -0
  136. data/vendor/wreq/src/tls/keylog.rs +99 -0
  137. data/vendor/wreq/src/tls/options.rs +464 -0
  138. data/vendor/wreq/src/tls/x509/identity.rs +122 -0
  139. data/vendor/wreq/src/tls/x509/parser.rs +71 -0
  140. data/vendor/wreq/src/tls/x509/store.rs +228 -0
  141. data/vendor/wreq/src/tls/x509.rs +68 -0
  142. data/vendor/wreq/src/tls.rs +154 -0
  143. data/vendor/wreq/src/trace.rs +55 -0
  144. data/vendor/wreq/src/util.rs +122 -0
  145. data/vendor/wreq/tests/badssl.rs +228 -0
  146. data/vendor/wreq/tests/brotli.rs +350 -0
  147. data/vendor/wreq/tests/client.rs +1098 -0
  148. data/vendor/wreq/tests/connector_layers.rs +227 -0
  149. data/vendor/wreq/tests/cookie.rs +306 -0
  150. data/vendor/wreq/tests/deflate.rs +347 -0
  151. data/vendor/wreq/tests/emulation.rs +260 -0
  152. data/vendor/wreq/tests/gzip.rs +347 -0
  153. data/vendor/wreq/tests/layers.rs +261 -0
  154. data/vendor/wreq/tests/multipart.rs +165 -0
  155. data/vendor/wreq/tests/proxy.rs +438 -0
  156. data/vendor/wreq/tests/redirect.rs +629 -0
  157. data/vendor/wreq/tests/retry.rs +135 -0
  158. data/vendor/wreq/tests/support/delay_server.rs +117 -0
  159. data/vendor/wreq/tests/support/error.rs +16 -0
  160. data/vendor/wreq/tests/support/layer.rs +183 -0
  161. data/vendor/wreq/tests/support/mod.rs +9 -0
  162. data/vendor/wreq/tests/support/server.rs +232 -0
  163. data/vendor/wreq/tests/timeouts.rs +281 -0
  164. data/vendor/wreq/tests/unix_socket.rs +135 -0
  165. data/vendor/wreq/tests/upgrade.rs +98 -0
  166. data/vendor/wreq/tests/zstd.rs +559 -0
  167. metadata +225 -0
@@ -0,0 +1,132 @@
1
+ use magnus::{
2
+ method, prelude::*, Module, RHash, Ruby, Value,
3
+ };
4
+
5
+ use crate::error::generic_error;
6
+
7
+ /// Wraps a wreq::Response in a Ruby-accessible type.
8
+ #[magnus::wrap(class = "Wreq::Response", free_immediately)]
9
+ pub struct Response {
10
+ status: u16,
11
+ headers: Vec<(String, String)>,
12
+ body: Vec<u8>,
13
+ url: String,
14
+ version: String,
15
+ content_length: Option<u64>,
16
+ transfer_size: Option<u64>,
17
+ }
18
+
19
+ impl Response {
20
+ pub fn new(
21
+ status: u16,
22
+ headers: Vec<(String, String)>,
23
+ body: Vec<u8>,
24
+ url: String,
25
+ version: String,
26
+ content_length: Option<u64>,
27
+ transfer_size: Option<u64>,
28
+ ) -> Self {
29
+ Self {
30
+ status,
31
+ headers,
32
+ body,
33
+ url,
34
+ version,
35
+ content_length,
36
+ transfer_size,
37
+ }
38
+ }
39
+
40
+ fn status(&self) -> u16 {
41
+ self.status
42
+ }
43
+
44
+ fn text(&self) -> Result<String, magnus::Error> {
45
+ String::from_utf8(self.body.clone()).map_err(|e| generic_error(e))
46
+ }
47
+
48
+ fn body_bytes(&self) -> Vec<u8> {
49
+ self.body.clone()
50
+ }
51
+
52
+ fn headers(&self) -> Result<RHash, magnus::Error> {
53
+ let ruby = unsafe { Ruby::get_unchecked() };
54
+ let hash = ruby.hash_new();
55
+ for (k, v) in &self.headers {
56
+ hash.aset(k.as_str(), v.as_str())?;
57
+ }
58
+ Ok(hash)
59
+ }
60
+
61
+ fn url(&self) -> String {
62
+ self.url.clone()
63
+ }
64
+
65
+ fn http_version(&self) -> String {
66
+ self.version.clone()
67
+ }
68
+
69
+ fn content_length(&self) -> Option<u64> {
70
+ self.content_length
71
+ }
72
+
73
+ fn transfer_size(&self) -> Option<u64> {
74
+ self.transfer_size
75
+ }
76
+
77
+ fn is_success(&self) -> bool {
78
+ (200..300).contains(&self.status)
79
+ }
80
+
81
+ fn is_redirect(&self) -> bool {
82
+ (300..400).contains(&self.status)
83
+ }
84
+
85
+ fn is_client_error(&self) -> bool {
86
+ (400..500).contains(&self.status)
87
+ }
88
+
89
+ fn is_server_error(&self) -> bool {
90
+ (500..600).contains(&self.status)
91
+ }
92
+
93
+ fn json(&self) -> Result<Value, magnus::Error> {
94
+ let ruby = unsafe { Ruby::get_unchecked() };
95
+ let text = self.text()?;
96
+ let json_module: Value = ruby.class_object().const_get("JSON")?;
97
+ json_module.funcall("parse", (text,))
98
+ }
99
+
100
+ fn inspect(&self) -> String {
101
+ format!(
102
+ "#<Wreq::Response status={} url={:?}>",
103
+ self.status, self.url
104
+ )
105
+ }
106
+
107
+ fn to_s(&self) -> Result<String, magnus::Error> {
108
+ self.text()
109
+ }
110
+ }
111
+
112
+ pub fn init(ruby: &magnus::Ruby, module: &magnus::RModule) -> Result<(), magnus::Error> {
113
+ let class = module.define_class("Response", ruby.class_object())?;
114
+ class.define_method("status", method!(Response::status, 0))?;
115
+ class.define_method("code", method!(Response::status, 0))?;
116
+ class.define_method("text", method!(Response::text, 0))?;
117
+ class.define_method("body", method!(Response::text, 0))?;
118
+ class.define_method("body_bytes", method!(Response::body_bytes, 0))?;
119
+ class.define_method("headers", method!(Response::headers, 0))?;
120
+ class.define_method("url", method!(Response::url, 0))?;
121
+ class.define_method("version", method!(Response::http_version, 0))?;
122
+ class.define_method("content_length", method!(Response::content_length, 0))?;
123
+ class.define_method("transfer_size", method!(Response::transfer_size, 0))?;
124
+ class.define_method("success?", method!(Response::is_success, 0))?;
125
+ class.define_method("redirect?", method!(Response::is_redirect, 0))?;
126
+ class.define_method("client_error?", method!(Response::is_client_error, 0))?;
127
+ class.define_method("server_error?", method!(Response::is_server_error, 0))?;
128
+ class.define_method("json", method!(Response::json, 0))?;
129
+ class.define_method("inspect", method!(Response::inspect, 0))?;
130
+ class.define_method("to_s", method!(Response::to_s, 0))?;
131
+ Ok(())
132
+ }
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wreq
4
+ VERSION = "0.3.0"
5
+ end
data/lib/wreq-rb.rb ADDED
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ begin
6
+ # pre-compiled extension by rake-compiler is located inside lib/wreq_rb/<ruby_version>/
7
+ RUBY_VERSION =~ /(\d+\.\d+)/
8
+ require_relative "wreq_rb/#{Regexp.last_match(1)}/wreq_rb"
9
+ rescue LoadError => e
10
+ # fallback to the locally built extension
11
+ require_relative 'wreq_rb/wreq_rb'
12
+ end
13
+
14
+ require_relative "wreq-rb/version"
15
+
16
+ module Wreq
17
+ end
@@ -0,0 +1,292 @@
1
+ diff --git a/src/client/http.rs b/src/client/http.rs
2
+ index 10be0c83..462c77d0 100644
3
+ --- a/src/client/http.rs
4
+ +++ b/src/client/http.rs
5
+ @@ -33,6 +33,7 @@ use self::future::Pending;
6
+ feature = "deflate",
7
+ ))]
8
+ use super::layer::decoder::{AcceptEncoding, DecompressionLayer};
9
+ +use super::layer::transfer_size::{CountingBody, TransferSizeLayer, TransferSizeService};
10
+ #[cfg(feature = "ws")]
11
+ use super::ws::WebSocketRequestBuilder;
12
+ use super::{
13
+ @@ -104,7 +105,7 @@ type Decompression<T> = super::layer::decoder::Decompression<T>;
14
+ feature = "brotli",
15
+ feature = "deflate"
16
+ ))]
17
+ -type ResponseBody = TimeoutBody<tower_http::decompression::DecompressionBody<Incoming>>;
18
+ +type ResponseBody = TimeoutBody<tower_http::decompression::DecompressionBody<CountingBody<Incoming>>>;
19
+
20
+ /// Response body type with timeout only (no compression features).
21
+ #[cfg(not(any(
22
+ @@ -113,23 +114,25 @@ type ResponseBody = TimeoutBody<tower_http::decompression::DecompressionBody<Inc
23
+ feature = "brotli",
24
+ feature = "deflate"
25
+ )))]
26
+ -type ResponseBody = TimeoutBody<Incoming>;
27
+ +type ResponseBody = TimeoutBody<CountingBody<Incoming>>;
28
+
29
+ /// The complete HTTP client service stack with all middleware layers.
30
+ type ClientService = Timeout<
31
+ ResponseBodyTimeout<
32
+ ConfigService<
33
+ Decompression<
34
+ - Retry<
35
+ - RetryPolicy,
36
+ - FollowRedirect<
37
+ - CookieService<
38
+ - MapErr<
39
+ - HttpClient<Connector, Body>,
40
+ - fn(client::error::Error) -> BoxError,
41
+ + TransferSizeService<
42
+ + Retry<
43
+ + RetryPolicy,
44
+ + FollowRedirect<
45
+ + CookieService<
46
+ + MapErr<
47
+ + HttpClient<Connector, Body>,
48
+ + fn(client::error::Error) -> BoxError,
49
+ + >,
50
+ >,
51
+ + FollowRedirectPolicy,
52
+ >,
53
+ - FollowRedirectPolicy,
54
+ >,
55
+ >,
56
+ >,
57
+ @@ -582,6 +585,10 @@ impl ClientBuilder {
58
+ })
59
+ .service(service);
60
+
61
+ + let service = ServiceBuilder::new()
62
+ + .layer(TransferSizeLayer::new())
63
+ + .service(service);
64
+ +
65
+ #[cfg(any(
66
+ feature = "gzip",
67
+ feature = "zstd",
68
+ diff --git a/src/client/layer.rs b/src/client/layer.rs
69
+ index 05bb533f..cb0b0866 100644
70
+ --- a/src/client/layer.rs
71
+ +++ b/src/client/layer.rs
72
+ @@ -13,3 +13,4 @@ pub mod decoder;
73
+ pub mod redirect;
74
+ pub mod retry;
75
+ pub mod timeout;
76
+ +pub mod transfer_size;
77
+ diff --git a/src/client/layer/transfer_size.rs b/src/client/layer/transfer_size.rs
78
+ new file mode 100644
79
+ index 00000000..7e8a4390
80
+ --- /dev/null
81
+ +++ b/src/client/layer/transfer_size.rs
82
+ @@ -0,0 +1,176 @@
83
+ +//! Middleware that records the network transfer size (pre-decompression bytes).
84
+ +//!
85
+ +//! This layer sits **below** the decompression layer so that it sees the raw
86
+ +//! compressed bytes flowing through the body. It wraps the response body in
87
+ +//! [`CountingBody`] which counts bytes as they flow through `poll_frame()`.
88
+ +//! The running total is accessible via a shared [`TransferSizeHandle`] stored
89
+ +//! in the response extensions.
90
+ +
91
+ +use std::{
92
+ + pin::Pin,
93
+ + sync::{
94
+ + atomic::{AtomicU64, Ordering},
95
+ + Arc,
96
+ + },
97
+ + task::{Context, Poll},
98
+ +};
99
+ +
100
+ +use bytes::Bytes;
101
+ +use http::{Request, Response};
102
+ +use http_body::{Body, Frame};
103
+ +use tower::{Layer, Service};
104
+ +
105
+ +/// A shared handle to the running byte count of the response body on the wire.
106
+ +///
107
+ +/// Call [`TransferSizeHandle::get()`] **after** the body has been fully
108
+ +/// consumed to obtain the total transferred bytes.
109
+ +#[derive(Debug, Clone)]
110
+ +pub struct TransferSizeHandle(Arc<AtomicU64>);
111
+ +
112
+ +impl TransferSizeHandle {
113
+ + /// Returns the number of bytes that have flowed through the body so far.
114
+ + #[inline]
115
+ + pub fn get(&self) -> u64 {
116
+ + self.0.load(Ordering::Relaxed)
117
+ + }
118
+ +}
119
+ +
120
+ +// ===== CountingBody =====
121
+ +
122
+ +pin_project_lite::pin_project! {
123
+ + /// A body wrapper that counts raw bytes flowing through `poll_frame()`.
124
+ + pub struct CountingBody<B> {
125
+ + #[pin]
126
+ + inner: B,
127
+ + counter: Arc<AtomicU64>,
128
+ + }
129
+ +}
130
+ +
131
+ +impl<B> Body for CountingBody<B>
132
+ +where
133
+ + B: Body<Data = Bytes>,
134
+ + B::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
135
+ +{
136
+ + type Data = Bytes;
137
+ + type Error = B::Error;
138
+ +
139
+ + fn poll_frame(
140
+ + self: Pin<&mut Self>,
141
+ + cx: &mut Context<'_>,
142
+ + ) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
143
+ + let this = self.project();
144
+ + match this.inner.poll_frame(cx) {
145
+ + Poll::Ready(Some(Ok(frame))) => {
146
+ + if let Some(data) = frame.data_ref() {
147
+ + this.counter
148
+ + .fetch_add(data.len() as u64, Ordering::Relaxed);
149
+ + }
150
+ + Poll::Ready(Some(Ok(frame)))
151
+ + }
152
+ + other => other,
153
+ + }
154
+ + }
155
+ +
156
+ + #[inline]
157
+ + fn is_end_stream(&self) -> bool {
158
+ + self.inner.is_end_stream()
159
+ + }
160
+ +
161
+ + #[inline]
162
+ + fn size_hint(&self) -> http_body::SizeHint {
163
+ + self.inner.size_hint()
164
+ + }
165
+ +}
166
+ +
167
+ +// ===== TransferSizeLayer / TransferSizeService =====
168
+ +
169
+ +/// A [`Layer`] that wraps responses with transfer-size tracking.
170
+ +#[derive(Debug, Clone, Copy, Default)]
171
+ +pub struct TransferSizeLayer;
172
+ +
173
+ +impl TransferSizeLayer {
174
+ + /// Create a new [`TransferSizeLayer`].
175
+ + #[inline]
176
+ + pub const fn new() -> Self {
177
+ + Self
178
+ + }
179
+ +}
180
+ +
181
+ +impl<S> Layer<S> for TransferSizeLayer {
182
+ + type Service = TransferSizeService<S>;
183
+ +
184
+ + #[inline]
185
+ + fn layer(&self, inner: S) -> Self::Service {
186
+ + TransferSizeService { inner }
187
+ + }
188
+ +}
189
+ +
190
+ +/// The [`Service`] created by [`TransferSizeLayer`].
191
+ +#[derive(Debug, Clone)]
192
+ +pub struct TransferSizeService<S> {
193
+ + inner: S,
194
+ +}
195
+ +
196
+ +impl<S, ReqBody, ResBody> Service<Request<ReqBody>> for TransferSizeService<S>
197
+ +where
198
+ + S: Service<Request<ReqBody>, Response = Response<ResBody>>,
199
+ + ResBody: Body<Data = Bytes>,
200
+ + ResBody::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
201
+ +{
202
+ + type Response = Response<CountingBody<ResBody>>;
203
+ + type Error = S::Error;
204
+ + type Future = TransferSizeFuture<S::Future>;
205
+ +
206
+ + #[inline]
207
+ + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
208
+ + self.inner.poll_ready(cx)
209
+ + }
210
+ +
211
+ + #[inline]
212
+ + fn call(&mut self, req: Request<ReqBody>) -> Self::Future {
213
+ + TransferSizeFuture {
214
+ + inner: self.inner.call(req),
215
+ + }
216
+ + }
217
+ +}
218
+ +
219
+ +// ===== TransferSizeFuture =====
220
+ +
221
+ +pin_project_lite::pin_project! {
222
+ + /// Future returned by [`TransferSizeService`].
223
+ + pub struct TransferSizeFuture<F> {
224
+ + #[pin]
225
+ + inner: F,
226
+ + }
227
+ +}
228
+ +
229
+ +impl<F, ResBody, E> std::future::Future for TransferSizeFuture<F>
230
+ +where
231
+ + F: std::future::Future<Output = Result<Response<ResBody>, E>>,
232
+ + ResBody: Body<Data = Bytes>,
233
+ + ResBody::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
234
+ +{
235
+ + type Output = Result<Response<CountingBody<ResBody>>, E>;
236
+ +
237
+ + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
238
+ + let this = self.project();
239
+ + match this.inner.poll(cx) {
240
+ + Poll::Ready(Ok(response)) => {
241
+ + let counter = Arc::new(AtomicU64::new(0));
242
+ + let handle = TransferSizeHandle(counter.clone());
243
+ +
244
+ + let (mut parts, body) = response.into_parts();
245
+ + parts.extensions.insert(handle);
246
+ +
247
+ + let counting_body = CountingBody {
248
+ + inner: body,
249
+ + counter,
250
+ + };
251
+ +
252
+ + Poll::Ready(Ok(Response::from_parts(parts, counting_body)))
253
+ + }
254
+ + Poll::Ready(Err(e)) => Poll::Ready(Err(e)),
255
+ + Poll::Pending => Poll::Pending,
256
+ + }
257
+ + }
258
+ +}
259
+ diff --git a/src/client/response.rs b/src/client/response.rs
260
+ index d98859b9..16ed35c3 100644
261
+ --- a/src/client/response.rs
262
+ +++ b/src/client/response.rs
263
+ @@ -20,6 +20,7 @@ use serde::de::DeserializeOwned;
264
+ use super::{
265
+ conn::HttpInfo,
266
+ core::{ext::ReasonPhrase, upgrade},
267
+ + layer::transfer_size::TransferSizeHandle,
268
+ };
269
+ #[cfg(feature = "cookies")]
270
+ use crate::cookie;
271
+ @@ -91,6 +92,21 @@ impl Response {
272
+ HttpBody::size_hint(self.res.body()).exact()
273
+ }
274
+
275
+ + /// Get a handle to the transfer size counter for this response.
276
+ + ///
277
+ + /// The handle tracks the number of raw (pre-decompression) bytes received
278
+ + /// from the network as the body is consumed. Call
279
+ + /// [`TransferSizeHandle::get()`] **after** the body has been fully read
280
+ + /// (e.g. after [`bytes()`](Self::bytes) or [`text()`](Self::text)) to
281
+ + /// obtain the total network transfer size.
282
+ + ///
283
+ + /// Returns `None` only if the response was constructed outside the normal
284
+ + /// client pipeline (e.g. via `From<http::Response>`).
285
+ + #[inline]
286
+ + pub fn transfer_size_handle(&self) -> Option<&TransferSizeHandle> {
287
+ + self.res.extensions().get::<TransferSizeHandle>()
288
+ + }
289
+ +
290
+ /// Retrieve the cookies contained in the [`Response`].
291
+ ///
292
+ /// Note that invalid 'Set-Cookie' headers will be ignored.