1 + | /*
|
2 + | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
3 + | * SPDX-License-Identifier: Apache-2.0
|
4 + | */
|
5 + |
|
6 + | //! HTTP/2 dispatch and two-ended request completion.
|
7 + | //!
|
8 + | //! [`H2Activation`] arrives with a prospective generation lease and a transient
|
9 + | //! sender cloned from the connection-owning cell. Dispatch first checks sender
|
10 + | //! and logical-connection state, then polls Hyper once. A request returned from
|
11 + | //! that poll was not accepted and may re-enter protocol acquisition.
|
12 + | //!
|
13 + | //! After Hyper accepts the request, the activation becomes an accepted lease.
|
14 + | //! [`H2RequestBody`] owns its send endpoint and [`H2ResponseBody`] owns its
|
15 + | //! receive endpoint. Either may finish first; the generation request count is
|
16 + | //! released only after both finish. A returned request re-arms the existing
|
17 + | //! body wrapper rather than nesting wrappers around the original body.
|
18 + |
|
19 + | use super::super::cell::h2::{H2Activation, H2CloseHandle, H2DispatchParts, H2LeaseEndpoint};
|
20 + | use super::super::connection::ConnectionState;
|
21 + | use super::super::ConnectionPool;
|
22 + | use super::{AcquisitionContext, H1HostHeaderInserted};
|
23 + | use crate::sync::{Arc, Mutex};
|
24 + | use aws_smithy_runtime_api::client::connection::CaptureSmithyConnection;
|
25 + | use aws_smithy_runtime_api::client::result::ConnectorError;
|
26 + | use aws_smithy_types::body::SdkBody;
|
27 + | use http_1x::{Request, Response};
|
28 + | use hyper::body::Body;
|
29 + | use std::future::{poll_fn, Future};
|
30 + | use std::pin::Pin;
|
31 + | use std::task::{Context, Poll};
|
32 + |
|
33 + | /// Result of one checked HTTP/2 dispatch attempt.
|
34 + | #[allow(
|
35 + | clippy::large_enum_variant,
|
36 + | reason = "boxing the successful response would allocate on every HTTP/2 request"
|
37 + | )]
|
38 + | pub(super) enum H2DispatchResult {
|
39 + | /// Hyper accepted the request and produced a guarded response.
|
40 + | Response(Response<SdkBody>),
|
41 + | /// Hyper did not accept the request, so it may re-enter acquisition.
|
42 + | Reacquire(Box<H2Reacquisition>),
|
43 + | }
|
44 + |
|
45 + | /// Request and terminal fallback retained for one replacement selection.
|
46 + | pub(super) struct H2Reacquisition {
|
47 + | /// Original request Hyper did not accept.
|
48 + | request: Request<SdkBody>,
|
49 + | /// Error returned if the request exhausts its replacement budget.
|
50 + | error: ConnectorError,
|
51 + | }
|
52 + |
|
53 + | impl H2Reacquisition {
|
54 + | /// Returns the original request and its terminal fallback.
|
55 + | pub(super) fn into_parts(self: Box<Self>) -> (Request<SdkBody>, ConnectorError) {
|
56 + | let Self { request, error } = *self;
|
57 + | (request, error)
|
58 + | }
|
59 + | }
|
60 + |
|
61 + | /// State retained after Hyper accepts one request.
|
62 + | struct H2AcceptedDispatch {
|
63 + | /// Connection retained through response creation.
|
64 + | connection: Arc<ConnectionState>,
|
65 + | /// Generation close authority used for connection-wide failures.
|
66 + | close: H2CloseHandle,
|
67 + | /// Metadata captured before the request moved into Hyper.
|
68 + | captured_metadata: Option<aws_smithy_runtime_api::client::connection::ConnectionMetadata>,
|
69 + | /// Receive endpoint transferred to the guarded response body.
|
70 + | receive_endpoint: H2LeaseEndpoint,
|
71 + | /// Whether the generation accepted an earlier request.
|
72 + | reused: bool,
|
73 + | }
|
74 + |
|
75 + | impl ConnectionPool {
|
76 + | /// Dispatches one request through a prospective H2 generation lease.
|
77 + | ///
|
78 + | /// Pool-side staleness reacquires before Hyper sees the request. Once
|
79 + | /// Hyper returns an envelope, only a reused generation may reacquire.
|
80 + | pub(super) async fn dispatch_h2(
|
81 + | &self,
|
82 + | context: &AcquisitionContext,
|
83 + | mut request: Request<SdkBody>,
|
84 + | mut activation: H2Activation,
|
85 + | ) -> Result<H2DispatchResult, ConnectorError> {
|
86 + | prepare_h2_request(&mut request, &context.absolute_uri);
|
87 + | let connection = activation.connection().clone();
|
88 + | let reused = activation.is_reused();
|
89 + | let close = activation.close_handle();
|
90 + | let captured_metadata = request
|
91 + | .extensions()
|
92 + | .get::<CaptureSmithyConnection>()
|
93 + | .cloned()
|
94 + | .map(|capture| {
|
95 + | let metadata = connection.info().h2_metadata(close.clone());
|
96 + | let captured = metadata.clone();
|
97 + | capture.set_connection_retriever(move || Some(captured.clone()));
|
98 + | metadata
|
99 + | });
|
100 + |
|
101 + | let H2DispatchParts {
|
102 + | mut sender,
|
103 + | send_endpoint,
|
104 + | receive_endpoint,
|
105 + | } = activation.take_dispatch_parts();
|
106 + | if sender.is_closed() {
|
107 + | close.close(super::super::connection::CloseReason::ProtocolClosed);
|
108 + | let metadata =
|
109 + | captured_metadata.unwrap_or_else(|| connection.info().h2_metadata(close));
|
110 + | return finish_unaccepted_request(
|
111 + | request,
|
112 + | reused,
|
113 + | UnacceptedStage::BeforeHyper,
|
114 + | ConnectorError::other(H2ConnectionClosedBeforeDispatch.into(), None)
|
115 + | .with_connection(metadata),
|
116 + | );
|
117 + | }
|
118 + | let Some(dispatch) = ConnectionState::try_commit_dispatch(&connection) else {
|
119 + | let metadata =
|
120 + | captured_metadata.unwrap_or_else(|| connection.info().h2_metadata(close));
|
121 + | return finish_unaccepted_request(
|
122 + | request,
|
123 + | reused,
|
124 + | UnacceptedStage::BeforeHyper,
|
125 + | ConnectorError::other(H2ConnectionClosedBeforeDispatch.into(), None)
|
126 + | .with_connection(metadata),
|
127 + | );
|
128 + | };
|
129 + |
|
130 + | let body = H2RequestBodyHandle::arm(&mut request, send_endpoint);
|
131 + |
|
132 + | let mut send = Box::pin(sender.hyper_mut().try_send_request(request));
|
133 + | let first = poll_fn(|cx| Poll::Ready(send.as_mut().poll(cx))).await;
|
134 + |
|
135 + | match first {
|
136 + | Poll::Ready(Err(mut error)) if error.message().is_some() => {
|
137 + | let returned = error
|
138 + | .take_message()
|
139 + | .expect("checked returned request disappeared");
|
140 + | body.clear();
|
141 + | drop(receive_endpoint);
|
142 + | drop(dispatch);
|
143 + | drop(activation);
|
144 + | close.close(super::super::connection::CloseReason::ProtocolClosed);
|
145 + | let metadata =
|
146 + | captured_metadata.unwrap_or_else(|| connection.info().h2_metadata(close));
|
147 + | finish_unaccepted_request(
|
148 + | returned,
|
149 + | reused,
|
150 + | UnacceptedStage::ReturnedByHyper,
|
151 + | super::super::super::downcast_error(Box::new(error.into_error()))
|
152 + | .with_connection(metadata),
|
153 + | )
|
154 + | }
|
155 + | Poll::Ready(result) => {
|
156 + | activation.accept(dispatch);
|
157 + | let sender_closed = sender.is_closed();
|
158 + | self.finish_h2_send(
|
159 + | sender_closed,
|
160 + | result,
|
161 + | H2AcceptedDispatch {
|
162 + | connection,
|
163 + | close,
|
164 + | captured_metadata,
|
165 + | receive_endpoint,
|
166 + | reused,
|
167 + | },
|
168 + | )
|
169 + | }
|
170 + | Poll::Pending => {
|
171 + | activation.accept(dispatch);
|
172 + | let result = send.await;
|
173 + | let sender_closed = sender.is_closed();
|
174 + | self.finish_h2_send(
|
175 + | sender_closed,
|
176 + | result,
|
177 + | H2AcceptedDispatch {
|
178 + | connection,
|
179 + | close,
|
180 + | captured_metadata,
|
181 + | receive_endpoint,
|
182 + | reused,
|
183 + | },
|
184 + | )
|
185 + | }
|
186 + | }
|
187 + | }
|
188 + |
|
189 + | #[allow(
|
190 + | clippy::result_large_err,
|
191 + | reason = "ConnectorError preserves SDK classification and connection metadata"
|
192 + | )]
|
193 + | fn finish_h2_send(
|
194 + | &self,
|
195 + | sender_closed: bool,
|
196 + | result: Result<
|
197 + | Response<hyper::body::Incoming>,
|
198 + | hyper::client::conn::TrySendError<Request<SdkBody>>,
|
199 + | >,
|
200 + | accepted: H2AcceptedDispatch,
|
201 + | ) -> Result<H2DispatchResult, ConnectorError> {
|
202 + | let H2AcceptedDispatch {
|
203 + | connection,
|
204 + | close,
|
205 + | captured_metadata,
|
206 + | receive_endpoint,
|
207 + | reused,
|
208 + | } = accepted;
|
209 + | match result {
|
210 + | Ok(mut response) => {
|
211 + | connection
|
212 + | .info()
|
213 + | .apply_connector_extras(response.extensions_mut());
|
214 + | let (parts, body) = response.into_parts();
|
215 + | let body = H2ResponseBody::new(body, receive_endpoint);
|
216 + | Ok(H2DispatchResult::Response(Response::from_parts(
|
217 + | parts,
|
218 + | SdkBody::from_body_1_x(body),
|
219 + | )))
|
220 + | }
|
221 + | Err(mut error) => {
|
222 + | // Hyper may return a queued envelope when its connection task
|
223 + | // drops after the first poll. The returned request is the
|
224 + | // authority for reacquisition; an error without it is terminal.
|
225 + | if let Some(request) = error.take_message() {
|
226 + | if let Some(body) = request.extensions().get::<H2RequestBodyHandle>() {
|
227 + | body.clear();
|
228 + | }
|
229 + | drop(receive_endpoint);
|
230 + | close.close(super::super::connection::CloseReason::ProtocolClosed);
|
231 + | let metadata =
|
232 + | captured_metadata.unwrap_or_else(|| connection.info().h2_metadata(close));
|
233 + | return finish_unaccepted_request(
|
234 + | request,
|
235 + | reused,
|
236 + | UnacceptedStage::ReturnedByHyper,
|
237 + | super::super::super::downcast_error(Box::new(error.into_error()))
|
238 + | .with_connection(metadata),
|
239 + | );
|
240 + | }
|
241 + | if sender_closed {
|
242 + | close.close(super::super::connection::CloseReason::ProtocolClosed);
|
243 + | }
|
244 + | drop(receive_endpoint);
|
245 + | let metadata =
|
246 + | captured_metadata.unwrap_or_else(|| connection.info().h2_metadata(close));
|
247 + | Err(
|
248 + | super::super::super::downcast_error(Box::new(error.into_error()))
|
249 + | .with_connection(metadata),
|
250 + | )
|
251 + | }
|
252 + | }
|
253 + | }
|
254 + | }
|
255 + |
|
256 + | /// Restores the absolute URI and removes only a pool-synthesized H1 `Host`.
|
257 + | fn prepare_h2_request(request: &mut Request<SdkBody>, absolute_uri: &http_1x::Uri) {
|
258 + | *request.uri_mut() = absolute_uri.clone();
|
259 + | if request
|
260 + | .extensions_mut()
|
261 + | .remove::<H1HostHeaderInserted>()
|
262 + | .is_some()
|
263 + | {
|
264 + | request.headers_mut().remove(http_1x::header::HOST);
|
265 + | }
|
266 + | }
|
267 + |
|
268 + | /// Point at which the pool learned that Hyper had not accepted the request.
|
269 + | #[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
270 + | enum UnacceptedStage {
|
271 + | /// The pool rejected a stale generation before calling Hyper.
|
272 + | BeforeHyper,
|
273 + | /// Hyper returned the original request envelope.
|
274 + | ReturnedByHyper,
|
275 + | }
|
276 + |
|
277 + | /// Applies retry authority to a request that Hyper did not accept.
|
278 + | ///
|
279 + | /// Pool-side checks always reacquire because no protocol code observed the
|
280 + | /// request. A returned Hyper envelope reacquires only after prior successful
|
281 + | /// use proves that replacing a stale pooled generation is appropriate.
|
282 + | #[allow(
|
283 + | clippy::result_large_err,
|
284 + | reason = "ConnectorError preserves SDK classification and connection metadata"
|
285 + | )]
|
286 + | fn finish_unaccepted_request(
|
287 + | request: Request<SdkBody>,
|
288 + | reused: bool,
|
289 + | stage: UnacceptedStage,
|
290 + | error: ConnectorError,
|
291 + | ) -> Result<H2DispatchResult, ConnectorError> {
|
292 + | if stage == UnacceptedStage::BeforeHyper || reused {
|
293 + | Ok(H2DispatchResult::Reacquire(Box::new(H2Reacquisition {
|
294 + | request,
|
295 + | error,
|
296 + | })))
|
297 + | } else {
|
298 + | Err(error)
|
299 + | }
|
300 + | }
|
301 + |
|
302 + | /// A fresh HTTP/2 generation closed before accepting its first request.
|
303 + | #[derive(Debug)]
|
304 + | struct H2ConnectionClosedBeforeDispatch;
|
305 + |
|
306 + | impl std::fmt::Display for H2ConnectionClosedBeforeDispatch {
|
307 + | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
308 + | f.write_str("the fresh HTTP/2 connection closed before request dispatch")
|
309 + | }
|
310 + | }
|
311 + |
|
312 + | impl std::error::Error for H2ConnectionClosedBeforeDispatch {}
|
313 + |
|
314 + | /// Handle retained in a request extension after its body is wrapped once.
|
315 + | #[derive(Clone)]
|
316 + | struct H2RequestBodyHandle {
|
317 + | /// Re-armable send endpoint shared with the wrapped request body.
|
318 + | slot: Arc<Mutex<Option<H2LeaseEndpoint>>>,
|
319 + | }
|
320 + |
|
321 + | impl H2RequestBodyHandle {
|
322 + | /// Wraps an unwrapped body and arms the current send endpoint.
|
323 + | fn arm(request: &mut Request<SdkBody>, endpoint: H2LeaseEndpoint) -> Self {
|
324 + | let is_end_stream = request.body().is_end_stream();
|
325 + | let handle = request
|
326 + | .extensions()
|
327 + | .get::<Self>()
|
328 + | .cloned()
|
329 + | .unwrap_or_else(|| {
|
330 + | let handle = Self {
|
331 + | slot: Arc::new(Mutex::new(None)),
|
332 + | };
|
333 + | let body = std::mem::replace(request.body_mut(), SdkBody::taken());
|
334 + | *request.body_mut() = SdkBody::from_body_1_x(H2RequestBody {
|
335 + | inner: body,
|
336 + | slot: handle.slot.clone(),
|
337 + | });
|
338 + | request.extensions_mut().insert(handle.clone());
|
339 + | handle
|
340 + | });
|
341 + | arm_send_endpoint(&handle.slot, endpoint);
|
342 + | if is_end_stream {
|
343 + | finish_send_endpoint(&handle.slot);
|
344 + | }
|
345 + | handle
|
346 + | }
|
347 + |
|
348 + | /// Disarms an endpoint after Hyper returns the request unaccepted.
|
349 + | fn clear(&self) {
|
350 + | finish_send_endpoint(&self.slot);
|
351 + | }
|
352 + | }
|
353 + |
|
354 + | /// Request body whose endpoint can be re-armed after certified non-acceptance.
|
355 + | struct H2RequestBody {
|
356 + | /// Original SDK body wrapped exactly once.
|
357 + | inner: SdkBody,
|
358 + | /// Endpoint replaced when Hyper returns an unaccepted request.
|
359 + | slot: Arc<Mutex<Option<H2LeaseEndpoint>>>,
|
360 + | }
|
361 + |
|
362 + | impl Body for H2RequestBody {
|
363 + | type Data = <SdkBody as Body>::Data;
|
364 + | type Error = <SdkBody as Body>::Error;
|
365 + |
|
366 + | fn poll_frame(
|
367 + | mut self: Pin<&mut Self>,
|
368 + | cx: &mut Context<'_>,
|
369 + | ) -> Poll<Option<Result<hyper::body::Frame<Self::Data>, Self::Error>>> {
|
370 + | let result = Pin::new(&mut self.inner).poll_frame(cx);
|
371 + | if matches!(result, Poll::Ready(None) | Poll::Ready(Some(Err(_)))) {
|
372 + | finish_send_endpoint(&self.slot);
|
373 + | }
|
374 + | result
|
375 + | }
|
376 + |
|
377 + | fn is_end_stream(&self) -> bool {
|
378 + | self.inner.is_end_stream()
|
379 + | }
|
380 + |
|
381 + | fn size_hint(&self) -> hyper::body::SizeHint {
|
382 + | self.inner.size_hint()
|
383 + | }
|
384 + | }
|
385 + |
|
386 + | impl Drop for H2RequestBody {
|
387 + | fn drop(&mut self) {
|
388 + | finish_send_endpoint(&self.slot);
|
389 + | }
|
390 + | }
|
391 + |
|
392 + | /// Response body that owns the receive endpoint through stream completion.
|
393 + | struct H2ResponseBody {
|
394 + | /// Hyper response stream.
|
395 + | inner: hyper::body::Incoming,
|
396 + | /// Receive endpoint completed on terminal frame, error, or drop.
|
397 + | receive: Option<H2LeaseEndpoint>,
|
398 + | }
|
399 + |
|
400 + | impl H2ResponseBody {
|
401 + | /// Wraps a response and completes an endpoint already at end stream.
|
402 + | fn new(inner: hyper::body::Incoming, receive: H2LeaseEndpoint) -> Self {
|
403 + | let is_end_stream = inner.is_end_stream();
|
404 + | Self {
|
405 + | inner,
|
406 + | receive: retain_receive_endpoint(is_end_stream, receive),
|
407 + | }
|
408 + | }
|
409 + | }
|
410 + |
|
411 + | /// Retains a receive endpoint only while response frames may remain.
|
412 + | fn retain_receive_endpoint(
|
413 + | is_end_stream: bool,
|
414 + | receive: H2LeaseEndpoint,
|
415 + | ) -> Option<H2LeaseEndpoint> {
|
416 + | if is_end_stream {
|
417 + | receive.complete();
|
418 + | None
|
419 + | } else {
|
420 + | Some(receive)
|
421 + | }
|
422 + | }
|
423 + |
|
424 + | impl Body for H2ResponseBody {
|
425 + | type Data = <hyper::body::Incoming as Body>::Data;
|
426 + | type Error = <hyper::body::Incoming as Body>::Error;
|
427 + |
|
428 + | fn poll_frame(
|
429 + | mut self: Pin<&mut Self>,
|
430 + | cx: &mut Context<'_>,
|
431 + | ) -> Poll<Option<Result<hyper::body::Frame<Self::Data>, Self::Error>>> {
|
432 + | let result = Pin::new(&mut self.inner).poll_frame(cx);
|
433 + | if matches!(result, Poll::Ready(None) | Poll::Ready(Some(Err(_)))) {
|
434 + | if let Some(endpoint) = self.receive.take() {
|
435 + | endpoint.complete();
|
436 + | }
|
437 + | }
|
438 + | result
|
439 + | }
|
440 + |
|
441 + | fn is_end_stream(&self) -> bool {
|
442 + | self.inner.is_end_stream()
|
443 + | }
|
444 + |
|
445 + | fn size_hint(&self) -> hyper::body::SizeHint {
|
446 + | self.inner.size_hint()
|
447 + | }
|
448 + | }
|
449 + |
|
450 + | fn arm_send_endpoint(slot: &Arc<Mutex<Option<H2LeaseEndpoint>>>, endpoint: H2LeaseEndpoint) {
|
451 + | let previous = slot.lock().replace(endpoint);
|
452 + | drop(previous);
|
453 + | }
|
454 + |
|
455 + | /// Terminates the send endpoint after upload completion, error, or rejection.
|
456 + | fn finish_send_endpoint(slot: &Arc<Mutex<Option<H2LeaseEndpoint>>>) {
|
457 + | let endpoint = slot.lock().take();
|
458 + | if let Some(endpoint) = endpoint {
|
459 + | endpoint.complete();
|
460 + | }
|
461 + | }
|
462 + |
|
463 + | #[cfg(all(test, not(smithy_http_client_loom), feature = "rt-tokio"))]
|
464 + | mod tests {
|
465 + | use super::*;
|
466 + | use crate::client::pool::cell::OriginCell;
|
467 + | use crate::client::pool::origin::OriginKey;
|
468 + | use crate::client::pool::partition::{EligibilityGroup, PartitionId};
|
469 + | use crate::sync::Arc;
|
470 + | use http_1x::uri::Scheme;
|
471 + | use http_body_util::BodyExt;
|
472 + |
|
473 + | /// Request body that fails on its first frame.
|
474 + | struct FailingBody;
|
475 + |
|
476 + | impl Body for FailingBody {
|
477 + | type Data = hyper::body::Bytes;
|
478 + | type Error = std::io::Error;
|
479 + |
|
480 + | fn poll_frame(
|
481 + | self: Pin<&mut Self>,
|
482 + | _cx: &mut Context<'_>,
|
483 + | ) -> Poll<Option<Result<hyper::body::Frame<Self::Data>, Self::Error>>> {
|
484 + | Poll::Ready(Some(Err(std::io::Error::other("request body failed"))))
|
485 + | }
|
486 + | }
|
487 + |
|
488 + | fn cell() -> Arc<OriginCell> {
|
489 + | Arc::new(OriginCell::new(
|
490 + | PartitionId::from_index(1),
|
491 + | OriginKey::from_parts(Scheme::HTTPS, "example.com", None).unwrap(),
|
492 + | EligibilityGroup::Pool,
|
493 + | None,
|
494 + | None,
|
495 + | ))
|
496 + | }
|
497 + |
|
498 + | #[tokio::test]
|
499 + | async fn returned_request_rearms_one_body_wrapper() {
|
500 + | let cell = cell();
|
501 + | let mut request = Request::post("https://example.com/upload")
|
502 + | .body(SdkBody::from("payload"))
|
503 + | .unwrap();
|
504 + |
|
505 + | let (first_endpoint, first_probe) = H2LeaseEndpoint::send_for_test(&cell);
|
506 + | let first = H2RequestBodyHandle::arm(&mut request, first_endpoint);
|
507 + | let first_slot = first.slot.clone();
|
508 + | assert!(!first_probe.send_complete());
|
509 + |
|
510 + | let (second_endpoint, second_probe) = H2LeaseEndpoint::send_for_test(&cell);
|
511 + | let second = H2RequestBodyHandle::arm(&mut request, second_endpoint);
|
512 + | assert!(
|
513 + | Arc::ptr_eq(&first_slot, &second.slot),
|
514 + | "rearming replaced the request-body wrapper"
|
515 + | );
|
516 + | assert!(
|
517 + | first_probe.send_complete(),
|
518 + | "rearming did not cancel the prior prospective endpoint"
|
519 + | );
|
520 + | assert!(!second_probe.send_complete());
|
521 + |
|
522 + | second.clear();
|
523 + | assert!(
|
524 + | second_probe.send_complete(),
|
525 + | "returned request retained its rejected send endpoint"
|
526 + | );
|
527 + | let body = request
|
528 + | .into_body()
|
529 + | .collect()
|
530 + | .await
|
531 + | .expect("wrapped request body failed")
|
532 + | .to_bytes();
|
533 + | assert_eq!(hyper::body::Bytes::from_static(b"payload"), body);
|
534 + | }
|
535 + |
|
536 + | #[test]
|
537 + | fn empty_request_body_completes_its_send_endpoint_when_armed() {
|
538 + | let cell = cell();
|
539 + | let mut request = Request::new(SdkBody::empty());
|
540 + | let (endpoint, probe) = H2LeaseEndpoint::send_for_test(&cell);
|
541 + |
|
542 + | H2RequestBodyHandle::arm(&mut request, endpoint);
|
543 + |
|
544 + | assert!(probe.send_complete());
|
545 + | }
|
546 + | #[test]
|
547 + | fn dropping_request_body_completes_its_send_endpoint() {
|
548 + | let cell = cell();
|
549 + | let mut request = Request::new(SdkBody::from("payload"));
|
550 + | let (endpoint, probe) = H2LeaseEndpoint::send_for_test(&cell);
|
551 + |
|
552 + | H2RequestBodyHandle::arm(&mut request, endpoint);
|
553 + | let body = std::mem::replace(request.body_mut(), SdkBody::empty());
|
554 + | drop(body);
|
555 + |
|
556 + | assert!(probe.send_complete());
|
557 + | }
|
558 + |
|
559 + | #[tokio::test]
|
560 + | async fn request_body_error_completes_its_send_endpoint() {
|
561 + | let cell = cell();
|
562 + | let mut request = Request::new(SdkBody::from_body_1_x(FailingBody));
|
563 + | let (endpoint, probe) = H2LeaseEndpoint::send_for_test(&cell);
|
564 + |
|
565 + | H2RequestBodyHandle::arm(&mut request, endpoint);
|
566 + | let frame = request
|
567 + | .body_mut()
|
568 + | .frame()
|
569 + | .await
|
570 + | .expect("failing body omitted its error frame");
|
571 + |
|
572 + | assert!(frame.is_err());
|
573 + | assert!(probe.send_complete());
|
574 + | }
|
575 + |
|
576 + | #[test]
|
577 + | fn bodyless_response_completes_its_receive_endpoint_without_drop() {
|
578 + | let cell = cell();
|
579 + | let (endpoint, probe) = H2LeaseEndpoint::receive_for_test(&cell);
|
580 + |
|
581 + | let retained = retain_receive_endpoint(true, endpoint);
|
582 + |
|
583 + | assert!(retained.is_none());
|
584 + | assert!(probe.receive_complete());
|
585 + | }
|
586 + |
|
587 + | #[test]
|
588 + | fn h2_preparation_restores_uri_and_removes_only_synthesized_host() {
|
589 + | let absolute: http_1x::Uri = "https://example.com/resource".parse().unwrap();
|
590 + | let mut request = Request::get("/resource").body(SdkBody::empty()).unwrap();
|
591 + | request
|
592 + | .headers_mut()
|
593 + | .insert(http_1x::header::HOST, "example.com".parse().unwrap());
|
594 + | request.extensions_mut().insert(H1HostHeaderInserted);
|
595 + |
|
596 + | prepare_h2_request(&mut request, &absolute);
|
597 + |
|
598 + | assert_eq!(&absolute, request.uri());
|
599 + | assert!(!request.headers().contains_key(http_1x::header::HOST));
|
600 + | assert!(request.extensions().get::<H1HostHeaderInserted>().is_none());
|
601 + |
|
602 + | let mut user_host = Request::get("/resource").body(SdkBody::empty()).unwrap();
|
603 + | user_host
|
604 + | .headers_mut()
|
605 + | .insert(http_1x::header::HOST, "signed.example".parse().unwrap());
|
606 + | prepare_h2_request(&mut user_host, &absolute);
|
607 + | assert_eq!("signed.example", user_host.headers()[http_1x::header::HOST]);
|
608 + | }
|
609 + |
|
610 + | #[test]
|
611 + | fn retry_authority_distinguishes_pool_checks_from_hyper_returns() {
|
612 + | let checked_before_hyper = finish_unaccepted_request(
|
613 + | Request::new(SdkBody::empty()),
|
614 + | false,
|
615 + | UnacceptedStage::BeforeHyper,
|
616 + | ConnectorError::user("pre-Hyper failure".into()),
|
617 + | );
|
618 + | assert!(matches!(
|
619 + | checked_before_hyper,
|
620 + | Ok(H2DispatchResult::Reacquire(_))
|
621 + | ));
|
622 + |
|
623 + | let fresh_hyper_return = finish_unaccepted_request(
|
624 + | Request::new(SdkBody::empty()),
|
625 + | false,
|
626 + | UnacceptedStage::ReturnedByHyper,
|
627 + | ConnectorError::user("fresh Hyper failure".into()),
|
628 + | );
|
629 + | assert!(fresh_hyper_return.is_err());
|
630 + |
|
631 + | let reused_hyper_return = finish_unaccepted_request(
|
632 + | Request::new(SdkBody::empty()),
|
633 + | true,
|
634 + | UnacceptedStage::ReturnedByHyper,
|
635 + | ConnectorError::user("reused failure".into()),
|
636 + | );
|
637 + | assert!(matches!(
|
638 + | reused_hyper_return,
|
639 + | Ok(H2DispatchResult::Reacquire(_))
|
640 + | ));
|
641 + | }
|
642 + | }
|