Skip to content

Commit 13d53e1

Browse files
committed
feat(client): adds HttpInfo to responses when HttpConnector is used
- Adds `client::connect::Connected::extra()`, which allows connectors to specify arbitrary custom information about a connected transport. If a connector provides this extra value, it will be set in the `Response` extensions. Closes #1402
1 parent d55b5ef commit 13d53e1

File tree

5 files changed

+145
-13
lines changed

5 files changed

+145
-13
lines changed

examples/hello.rs

-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ static PHRASE: &'static [u8] = b"Hello World!";
1010

1111
fn main() {
1212
pretty_env_logger::init();
13-
1413
let addr = ([127, 0, 0, 1], 3000).into();
1514

1615
// new_service is run for each connection, creating a 'service'

src/client/connect/http.rs

+50-1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ use self::sealed::HttpConnectorBlockingTask;
2424
/// A connector for the `http` scheme.
2525
///
2626
/// Performs DNS resolution in a thread pool, and then connects over TCP.
27+
///
28+
/// # Note
29+
///
30+
/// Sets the [`HttpInfo`](HttpInfo) value on responses, which includes
31+
/// transport information such as the remote socket address used.
2732
#[derive(Clone)]
2833
pub struct HttpConnector {
2934
executor: HttpConnectExecutor,
@@ -36,6 +41,37 @@ pub struct HttpConnector {
3641
reuse_address: bool,
3742
}
3843

44+
/// Extra information about the transport when an HttpConnector is used.
45+
///
46+
/// # Example
47+
///
48+
/// ```rust
49+
/// use hyper::client::{Client, connect::HttpInfo};
50+
/// use hyper::rt::Future;
51+
///
52+
/// let client = Client::new();
53+
///
54+
/// let fut = client.get("http://example.local".parse().unwrap())
55+
/// .inspect(|resp| {
56+
/// resp
57+
/// .extensions()
58+
/// .get::<HttpInfo>()
59+
/// .map(|info| {
60+
/// println!("remote addr = {}", info.remote_addr());
61+
/// });
62+
/// });
63+
/// ```
64+
///
65+
/// # Note
66+
///
67+
/// If a different connector is used besides [`HttpConnector`](HttpConnector),
68+
/// this value will not exist in the extensions. Consult that specific
69+
/// connector to see what "extra" information it might provide to responses.
70+
#[derive(Clone, Debug)]
71+
pub struct HttpInfo {
72+
remote_addr: SocketAddr,
73+
}
74+
3975
impl HttpConnector {
4076
/// Construct a new HttpConnector.
4177
///
@@ -187,6 +223,13 @@ impl Connect for HttpConnector {
187223
}
188224
}
189225

226+
impl HttpInfo {
227+
/// Get the remote address of the transport used.
228+
pub fn remote_addr(&self) -> SocketAddr {
229+
self.remote_addr
230+
}
231+
}
232+
190233
#[inline]
191234
fn invalid_url(err: InvalidUrl, handle: &Option<Handle>) -> HttpConnecting {
192235
HttpConnecting {
@@ -277,7 +320,13 @@ impl Future for HttpConnecting {
277320

278321
sock.set_nodelay(self.nodelay)?;
279322

280-
return Ok(Async::Ready((sock, Connected::new())));
323+
let extra = HttpInfo {
324+
remote_addr: sock.peer_addr()?,
325+
};
326+
let connected = Connected::new()
327+
.extra(extra);
328+
329+
return Ok(Async::Ready((sock, connected)));
281330
},
282331
State::Error(ref mut e) => return Err(e.take().expect("polled more than once")),
283332
}

src/client/connect/mod.rs

+68-3
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,16 @@
66
//! establishes connections over TCP.
77
//! - The [`Connect`](Connect) trait and related types to build custom connectors.
88
use std::error::Error as StdError;
9-
use std::mem;
9+
use std::{fmt, mem};
1010

1111
use bytes::{BufMut, Bytes, BytesMut};
1212
use futures::Future;
13-
use http::{uri, Uri};
13+
use http::{uri, Response, Uri};
1414
use tokio_io::{AsyncRead, AsyncWrite};
1515

1616
#[cfg(feature = "runtime")] mod dns;
1717
#[cfg(feature = "runtime")] mod http;
18-
#[cfg(feature = "runtime")] pub use self::http::HttpConnector;
18+
#[cfg(feature = "runtime")] pub use self::http::{HttpConnector, HttpInfo};
1919

2020
/// Connect to a destination, returning an IO transport.
2121
///
@@ -48,8 +48,11 @@ pub struct Destination {
4848
pub struct Connected {
4949
//alpn: Alpn,
5050
pub(super) is_proxied: bool,
51+
pub(super) extra: Option<Extra>,
5152
}
5253

54+
pub(super) struct Extra(Box<ExtraInner>);
55+
5356
/*TODO: when HTTP1 Upgrades to H2 are added, this will be needed
5457
#[derive(Debug)]
5558
pub(super) enum Alpn {
@@ -245,6 +248,7 @@ impl Connected {
245248
Connected {
246249
//alpn: Alpn::Http1,
247250
is_proxied: false,
251+
extra: None,
248252
}
249253
}
250254

@@ -260,6 +264,12 @@ impl Connected {
260264
self
261265
}
262266

267+
/// Set extra connection information to be set in the extensions of every `Response`.
268+
pub fn extra<T: Clone + Send + Sync + 'static>(mut self, extra: T) -> Connected {
269+
self.extra = Some(Extra(Box::new(ExtraEnvelope(extra))));
270+
self
271+
}
272+
263273
/*
264274
/// Set that the connected transport negotiated HTTP/2 as it's
265275
/// next protocol.
@@ -268,6 +278,61 @@ impl Connected {
268278
self
269279
}
270280
*/
281+
282+
// Don't public expose that `Connected` is `Clone`, unsure if we want to
283+
// keep that contract...
284+
pub(super) fn clone(&self) -> Connected {
285+
Connected {
286+
is_proxied: self.is_proxied,
287+
extra: self.extra.clone(),
288+
}
289+
}
290+
}
291+
292+
// ===== impl Extra =====
293+
294+
impl Extra {
295+
pub(super) fn set(&self, res: &mut Response<::Body>) {
296+
self.0.set(res);
297+
}
298+
}
299+
300+
impl Clone for Extra {
301+
fn clone(&self) -> Extra {
302+
Extra(self.0.clone_box())
303+
}
304+
}
305+
306+
impl fmt::Debug for Extra {
307+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
308+
f.debug_struct("Extra")
309+
.finish()
310+
}
311+
}
312+
313+
// This indirection allows the `Connected` to have a type-erased "extra" value,
314+
// while that type still knows its inner extra type. This allows the correct
315+
// TypeId to be used when inserting into `res.extensions_mut()`.
316+
#[derive(Clone)]
317+
struct ExtraEnvelope<T>(T);
318+
319+
trait ExtraInner: Send + Sync {
320+
fn clone_box(&self) -> Box<ExtraInner>;
321+
fn set(&self, res: &mut Response<::Body>);
322+
}
323+
324+
impl<T> ExtraInner for ExtraEnvelope<T>
325+
where
326+
T: Clone + Send + Sync + 'static
327+
{
328+
fn clone_box(&self) -> Box<ExtraInner> {
329+
Box::new(self.clone())
330+
}
331+
332+
fn set(&self, res: &mut Response<::Body>) {
333+
let extra = self.0.clone();
334+
res.extensions_mut().insert(extra);
335+
}
271336
}
272337

273338
#[cfg(test)]

src/client/mod.rs

+16-7
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ use http::uri::Scheme;
9191

9292
use body::{Body, Payload};
9393
use common::{Exec, lazy as hyper_lazy, Lazy};
94-
use self::connect::{Connect, Destination};
94+
use self::connect::{Connect, Connected, Destination};
9595
use self::pool::{Key as PoolKey, Pool, Poolable, Pooled, Reservation};
9696

9797
#[cfg(feature = "runtime")] pub use self::connect::HttpConnector;
@@ -290,7 +290,7 @@ where C: Connect + Sync + 'static,
290290
// CONNECT always sends origin-form, so check it first...
291291
if req.method() == &Method::CONNECT {
292292
authority_form(req.uri_mut());
293-
} else if pooled.is_proxied {
293+
} else if pooled.conn_info.is_proxied {
294294
absolute_form(req.uri_mut());
295295
} else {
296296
origin_form(req.uri_mut());
@@ -305,6 +305,15 @@ where C: Connect + Sync + 'static,
305305
let fut = pooled.send_request_retryable(req)
306306
.map_err(ClientError::map_with_reused(pooled.is_reused()));
307307

308+
// If the Connector included 'extra' info, add to Response...
309+
let extra_info = pooled.conn_info.extra.clone();
310+
let fut = fut.map(move |mut res| {
311+
if let Some(extra) = extra_info {
312+
extra.set(&mut res);
313+
}
314+
res
315+
});
316+
308317
// As of [email protected], there is a race condition in the mpsc
309318
// channel, such that sending when the receiver is closing can
310319
// result in the message being stuck inside the queue. It won't
@@ -499,7 +508,7 @@ where C: Connect + Sync + 'static,
499508
})
500509
.map(move |tx| {
501510
pool.pooled(connecting, PoolClient {
502-
is_proxied: connected.is_proxied,
511+
conn_info: connected,
503512
tx: match ver {
504513
Ver::Http1 => PoolTx::Http1(tx),
505514
Ver::Http2 => PoolTx::Http2(tx.into_http2()),
@@ -565,7 +574,7 @@ impl Future for ResponseFuture {
565574
// FIXME: allow() required due to `impl Trait` leaking types to this lint
566575
#[allow(missing_debug_implementations)]
567576
struct PoolClient<B> {
568-
is_proxied: bool,
577+
conn_info: Connected,
569578
tx: PoolTx<B>,
570579
}
571580

@@ -624,17 +633,17 @@ where
624633
match self.tx {
625634
PoolTx::Http1(tx) => {
626635
Reservation::Unique(PoolClient {
627-
is_proxied: self.is_proxied,
636+
conn_info: self.conn_info,
628637
tx: PoolTx::Http1(tx),
629638
})
630639
},
631640
PoolTx::Http2(tx) => {
632641
let b = PoolClient {
633-
is_proxied: self.is_proxied,
642+
conn_info: self.conn_info.clone(),
634643
tx: PoolTx::Http2(tx.clone()),
635644
};
636645
let a = PoolClient {
637-
is_proxied: self.is_proxied,
646+
conn_info: self.conn_info,
638647
tx: PoolTx::Http2(tx),
639648
};
640649
Reservation::Shared(a, b)

tests/client.rs

+11-1
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,17 @@ macro_rules! test {
274274

275275
let rx = rx.expect("thread panicked");
276276

277-
rt.block_on(res.join(rx).map(|r| r.0))
277+
rt.block_on(res.join(rx).map(|r| r.0)).map(move |mut resp| {
278+
// Always check that HttpConnector has set the "extra" info...
279+
let extra = resp
280+
.extensions_mut()
281+
.remove::<::hyper::client::connect::HttpInfo>()
282+
.expect("HttpConnector should set HttpInfo");
283+
284+
assert_eq!(extra.remote_addr(), addr, "HttpInfo should have server addr");
285+
286+
resp
287+
})
278288
});
279289
}
280290

0 commit comments

Comments
 (0)