Skip to content

Commit 976a77a

Browse files
committed
feat(client): add ALPN h2 support for client connectors
- Adds `Connected::negotiated_h2()` method to signal the connection must use HTTP2. `Connect` implementations should set this if using ALPN. If a connection to a host is detected to have been upgraded via ALPN, any other oustanding connect futures will be canceled, and the waiting requests will make use of the single HTTP2 connection. The `http2_only` builder configuration still works the same, not requiring ALPN at all, and always using only a single connection.
1 parent bf188b2 commit 976a77a

File tree

7 files changed

+200
-95
lines changed

7 files changed

+200
-95
lines changed

src/client/conn.rs

+1
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,7 @@ impl Builder {
466466
T: AsyncRead + AsyncWrite + Send + 'static,
467467
B: Payload + 'static,
468468
{
469+
trace!("client handshake HTTP/{}", if self.http2 { 2 } else { 1 });
469470
Handshake {
470471
builder: self.clone(),
471472
io: Some(io),

src/client/connect/mod.rs

+7-12
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ pub trait Connect: Send + Sync {
3636
/// A set of properties to describe where and how to try to connect.
3737
#[derive(Clone, Debug)]
3838
pub struct Destination {
39-
//pub(super) alpn: Alpn,
4039
pub(super) uri: Uri,
4140
}
4241

@@ -46,21 +45,18 @@ pub struct Destination {
4645
/// was used, or if connected to an HTTP proxy.
4746
#[derive(Debug)]
4847
pub struct Connected {
49-
//alpn: Alpn,
48+
pub(super) alpn: Alpn,
5049
pub(super) is_proxied: bool,
5150
pub(super) extra: Option<Extra>,
5251
}
5352

5453
pub(super) struct Extra(Box<ExtraInner>);
5554

56-
/*TODO: when HTTP1 Upgrades to H2 are added, this will be needed
57-
#[derive(Debug)]
55+
#[derive(Clone, Copy, Debug, PartialEq)]
5856
pub(super) enum Alpn {
59-
Http1,
60-
//H2,
61-
//Http1OrH2
57+
H2,
58+
None,
6259
}
63-
*/
6460

6561
impl Destination {
6662
/// Get the protocol scheme.
@@ -246,7 +242,7 @@ impl Connected {
246242
/// Create new `Connected` type with empty metadata.
247243
pub fn new() -> Connected {
248244
Connected {
249-
//alpn: Alpn::Http1,
245+
alpn: Alpn::None,
250246
is_proxied: false,
251247
extra: None,
252248
}
@@ -274,19 +270,18 @@ impl Connected {
274270
self
275271
}
276272

277-
/*
278273
/// Set that the connected transport negotiated HTTP/2 as it's
279274
/// next protocol.
280-
pub fn h2(mut self) -> Connected {
275+
pub fn negotiated_h2(mut self) -> Connected {
281276
self.alpn = Alpn::H2;
282277
self
283278
}
284-
*/
285279

286280
// Don't public expose that `Connected` is `Clone`, unsure if we want to
287281
// keep that contract...
288282
pub(super) fn clone(&self) -> Connected {
289283
Connected {
284+
alpn: self.alpn.clone(),
290285
is_proxied: self.is_proxied,
291286
extra: self.extra.clone(),
292287
}

src/client/mod.rs

+83-49
Original file line numberDiff line numberDiff line change
@@ -86,12 +86,12 @@ use futures::{Async, Future, Poll};
8686
use futures::future::{self, Either, Executor};
8787
use futures::sync::oneshot;
8888
use http::{Method, Request, Response, Uri, Version};
89-
use http::header::{Entry, HeaderValue, HOST};
89+
use http::header::{HeaderValue, HOST};
9090
use http::uri::Scheme;
9191

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

9797
#[cfg(feature = "runtime")] pub use self::connect::HttpConnector;
@@ -192,23 +192,19 @@ where C: Connect + Sync + 'static,
192192

193193
/// Send a constructed Request using this Client.
194194
pub fn request(&self, mut req: Request<B>) -> ResponseFuture {
195-
let is_http_11 = self.ver == Ver::Http1 && match req.version() {
196-
Version::HTTP_11 => true,
197-
Version::HTTP_10 => false,
198-
other => {
195+
let is_http_connect = req.method() == &Method::CONNECT;
196+
match req.version() {
197+
Version::HTTP_11 => (),
198+
Version::HTTP_10 => if is_http_connect {
199+
debug!("CONNECT is not allowed for HTTP/1.0");
200+
return ResponseFuture::new(Box::new(future::err(::Error::new_user_unsupported_request_method())));
201+
},
202+
other => if self.ver != Ver::Http2 {
199203
error!("Request has unsupported version \"{:?}\"", other);
200204
return ResponseFuture::new(Box::new(future::err(::Error::new_user_unsupported_version())));
201205
}
202206
};
203207

204-
let is_http_connect = req.method() == &Method::CONNECT;
205-
206-
if !is_http_11 && is_http_connect {
207-
debug!("client does not support CONNECT requests for {:?}", req.version());
208-
return ResponseFuture::new(Box::new(future::err(::Error::new_user_unsupported_request_method())));
209-
}
210-
211-
212208
let uri = req.uri().clone();
213209
let domain = match (uri.scheme_part(), uri.authority_part()) {
214210
(Some(scheme), Some(auth)) => {
@@ -233,21 +229,7 @@ where C: Connect + Sync + 'static,
233229
}
234230
};
235231

236-
if self.set_host && self.ver == Ver::Http1 {
237-
if let Entry::Vacant(entry) = req.headers_mut().entry(HOST).expect("HOST is always valid header name") {
238-
let hostname = uri.host().expect("authority implies host");
239-
let host = if let Some(port) = uri.port() {
240-
let s = format!("{}:{}", hostname, port);
241-
HeaderValue::from_str(&s)
242-
} else {
243-
HeaderValue::from_str(hostname)
244-
}.expect("uri host is valid header value");
245-
entry.insert(host);
246-
}
247-
}
248-
249-
250-
let pool_key = (Arc::new(domain.to_string()), self.ver);
232+
let pool_key = Arc::new(domain.to_string());
251233
ResponseFuture::new(Box::new(self.retryably_send_request(req, pool_key)))
252234
}
253235

@@ -283,23 +265,38 @@ where C: Connect + Sync + 'static,
283265
fn send_request(&self, mut req: Request<B>, pool_key: PoolKey) -> impl Future<Item=Response<Body>, Error=ClientError<B>> {
284266
let conn = self.connection_for(req.uri().clone(), pool_key);
285267

286-
let ver = self.ver;
268+
let set_host = self.set_host;
287269
let executor = self.executor.clone();
288270
conn.and_then(move |mut pooled| {
289-
if ver == Ver::Http1 {
290-
// CONNECT always sends origin-form, so check it first...
271+
if pooled.is_http1() {
272+
if set_host {
273+
let uri = req.uri().clone();
274+
req
275+
.headers_mut()
276+
.entry(HOST)
277+
.expect("HOST is always valid header name")
278+
.or_insert_with(|| {
279+
let hostname = uri.host().expect("authority implies host");
280+
if let Some(port) = uri.port() {
281+
let s = format!("{}:{}", hostname, port);
282+
HeaderValue::from_str(&s)
283+
} else {
284+
HeaderValue::from_str(hostname)
285+
}.expect("uri host is valid header value")
286+
});
287+
}
288+
289+
// CONNECT always sends authority-form, so check it first...
291290
if req.method() == &Method::CONNECT {
292291
authority_form(req.uri_mut());
293292
} else if pooled.conn_info.is_proxied {
294293
absolute_form(req.uri_mut());
295294
} else {
296295
origin_form(req.uri_mut());
297296
};
298-
} else {
299-
debug_assert!(
300-
req.method() != &Method::CONNECT,
301-
"Client should have returned Error for HTTP2 CONNECT"
302-
);
297+
} else if req.method() == &Method::CONNECT {
298+
debug!("client does not support CONNECT requests over HTTP2");
299+
return Either::A(future::err(ClientError::Normal(::Error::new_user_unsupported_request_method())));
303300
}
304301

305302
let fut = pooled.send_request_retryable(req)
@@ -322,10 +319,10 @@ where C: Connect + Sync + 'static,
322319
// To counteract this, we must check if our senders 'want' channel
323320
// has been closed after having tried to send. If so, error out...
324321
if pooled.is_closed() {
325-
return Either::A(fut);
322+
return Either::B(Either::A(fut));
326323
}
327324

328-
Either::B(fut
325+
Either::B(Either::B(fut
329326
.and_then(move |mut res| {
330327
// If pooled is HTTP/2, we can toss this reference immediately.
331328
//
@@ -337,7 +334,7 @@ where C: Connect + Sync + 'static,
337334
// for a new request to start.
338335
//
339336
// It won't be ready if there is a body to stream.
340-
if ver == Ver::Http2 || !pooled.is_pool_enabled() || pooled.is_ready() {
337+
if pooled.is_http2() || !pooled.is_pool_enabled() || pooled.is_ready() {
341338
drop(pooled);
342339
} else if !res.body().is_end_stream() {
343340
let (delayed_tx, delayed_rx) = oneshot::channel();
@@ -370,7 +367,7 @@ where C: Connect + Sync + 'static,
370367
}
371368
}
372369
Ok(res)
373-
}))
370+
})))
374371
})
375372
}
376373

@@ -463,8 +460,9 @@ where C: Connect + Sync + 'static,
463460
let pool = self.pool.clone();
464461
let h1_writev = self.h1_writev;
465462
let h1_title_case_headers = self.h1_title_case_headers;
463+
let ver = self.ver;
464+
let is_ver_h2 = self.ver == Ver::Http2;
466465
let connector = self.connector.clone();
467-
let ver = pool_key.1;
468466
let dst = Destination {
469467
uri,
470468
};
@@ -474,7 +472,7 @@ where C: Connect + Sync + 'static,
474472
// If the pool_key is for HTTP/2, and there is already a
475473
// connection being estabalished, then this can't take a
476474
// second lock. The "connect_to" future is Canceled.
477-
let connecting = match pool.connecting(&pool_key) {
475+
let connecting = match pool.connecting(&pool_key, ver) {
478476
Some(lock) => lock,
479477
None => {
480478
let canceled = ::Error::new_canceled(Some("HTTP/2 connection in progress"));
@@ -484,11 +482,31 @@ where C: Connect + Sync + 'static,
484482
Either::A(connector.connect(dst)
485483
.map_err(::Error::new_connect)
486484
.and_then(move |(io, connected)| {
487-
conn::Builder::new()
485+
// If ALPN is h2 and we aren't http2_only already,
486+
// then we need to convert our pool checkout into
487+
// a single HTTP2 one.
488+
let connecting = if connected.alpn == Alpn::H2 && !is_ver_h2 {
489+
match connecting.alpn_h2(&pool) {
490+
Some(lock) => {
491+
trace!("ALPN negotiated h2, updating pool");
492+
lock
493+
},
494+
None => {
495+
// Another connection has already upgraded,
496+
// the pool checkout should finish up for us.
497+
let canceled = ::Error::new_canceled(Some("ALPN upgraded to HTTP/2"));
498+
return Either::B(future::err(canceled));
499+
}
500+
}
501+
} else {
502+
connecting
503+
};
504+
let is_h2 = is_ver_h2 || connected.alpn == Alpn::H2;
505+
Either::A(conn::Builder::new()
488506
.exec(executor.clone())
489507
.h1_writev(h1_writev)
490508
.h1_title_case_headers(h1_title_case_headers)
491-
.http2_only(pool_key.1 == Ver::Http2)
509+
.http2_only(is_h2)
492510
.handshake(io)
493511
.and_then(move |(tx, conn)| {
494512
let bg = executor.execute(conn.map_err(|e| {
@@ -509,12 +527,13 @@ where C: Connect + Sync + 'static,
509527
.map(move |tx| {
510528
pool.pooled(connecting, PoolClient {
511529
conn_info: connected,
512-
tx: match ver {
513-
Ver::Http1 => PoolTx::Http1(tx),
514-
Ver::Http2 => PoolTx::Http2(tx.into_http2()),
530+
tx: if is_h2 {
531+
PoolTx::Http2(tx.into_http2())
532+
} else {
533+
PoolTx::Http1(tx)
515534
},
516535
})
517-
})
536+
}))
518537
}))
519538
})
520539
}
@@ -591,6 +610,17 @@ impl<B> PoolClient<B> {
591610
}
592611
}
593612

613+
fn is_http1(&self) -> bool {
614+
!self.is_http2()
615+
}
616+
617+
fn is_http2(&self) -> bool {
618+
match self.tx {
619+
PoolTx::Http1(_) => false,
620+
PoolTx::Http2(_) => true,
621+
}
622+
}
623+
594624
fn is_ready(&self) -> bool {
595625
match self.tx {
596626
PoolTx::Http1(ref tx) => tx.is_ready(),
@@ -650,6 +680,10 @@ where
650680
}
651681
}
652682
}
683+
684+
fn can_share(&self) -> bool {
685+
self.is_http2()
686+
}
653687
}
654688

655689
// FIXME: allow() required due to `impl Trait` leaking types to this lint

0 commit comments

Comments
 (0)