1 1 | /*
|
2 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
3 3 | * SPDX-License-Identifier: Apache-2.0
|
4 4 | */
|
5 5 |
|
6 6 | //! Request routing and protocol dispatch.
|
7 7 | //!
|
8 8 | //! This module is the request entry point after a [`super::Client`] has
|
9 9 | //! resolved its partition. It rejects unsupported request forms, resolves the
|
10 - | //! origin cell and partition runtime, and starts partition maintenance before
|
11 - | //! handing the request to a protocol-specific dispatcher. Connection
|
12 - | //! acquisition, wire-form preparation, and response ownership live in child
|
13 - | //! modules.
|
10 + | //! origin cell and partition runtime, and owns acquisition across protocol
|
11 + | //! selection. Child modules prepare protocol wire forms and retain response
|
12 + | //! ownership.
|
14 13 |
|
15 14 | mod h1;
|
15 + | mod h2;
|
16 16 |
|
17 - | use super::cell::OriginCell;
|
18 - | use super::establish::TransportTimeout;
|
17 + | use self::h1::H1DispatchResult;
|
18 + | use self::h2::H2DispatchResult;
|
19 + | use super::admission::ProtocolRequirement;
|
20 + | use super::cell::{AcquisitionEvent, AcquisitionResult, OriginCell, WaiterId};
|
21 + | use super::establish::{self, TransportTimeout};
|
19 22 | use super::partition::DriverSpawner;
|
20 23 | use super::registry::PartitionState;
|
21 24 | use super::{ConnectionPool, PoolInner};
|
22 25 | use crate::sync::Arc;
|
23 26 | use aws_smithy_runtime_api::client::result::ConnectorError;
|
24 27 | use aws_smithy_types::body::SdkBody;
|
25 - | use http_1x::{Method, Request, Response, Uri, Version};
|
28 + | use http_1x::{header, Method, Request, Response, Uri, Version};
|
29 + | use std::future::poll_fn;
|
26 30 | use std::sync::Arc as StdArc;
|
27 31 |
|
32 + | /// Replacement selections allowed after the initial HTTP/2 dispatch attempt.
|
33 + | const MAX_H2_REACQUISITIONS: usize = 2;
|
34 + |
|
28 35 | /// Stable state shared by acquisition and protocol dispatch for one request.
|
29 36 | ///
|
30 37 | /// This context owns ordinary shared configuration and identities. Linear
|
31 38 | /// acquisition values, such as an establishment permit or an exclusive
|
32 39 | /// protocol handle, remain separate arguments so their transfer is explicit.
|
33 40 | #[derive(Clone)]
|
34 41 | pub(super) struct AcquisitionContext {
|
35 42 | /// Shared pool policy and transport construction.
|
36 43 | pub(super) pool: Arc<PoolInner>,
|
37 44 | /// Partition from which the request was issued.
|
38 45 | pub(super) partition: Arc<PartitionState>,
|
39 46 | /// Partition-local state for the request's canonical origin.
|
40 47 | pub(super) cell: Arc<OriginCell>,
|
41 48 | /// Absolute request URI retained across protocol wire-form changes.
|
42 49 | pub(super) absolute_uri: Uri,
|
43 50 | /// Runtime that owns establishment, drivers, and pending return work.
|
44 51 | pub(super) owner_spawner: StdArc<dyn DriverSpawner>,
|
45 52 | /// Transport connection timeout configured for this operation.
|
46 53 | pub(super) connect_timeout: Option<TransportTimeout>,
|
47 54 | }
|
48 55 |
|
49 56 | impl ConnectionPool {
|
50 57 | /// Resolves common acquisition state and dispatches one request.
|
51 58 | pub(super) async fn send_request(
|
52 59 | &self,
|
53 60 | partition: Arc<PartitionState>,
|
54 61 | request: Request<SdkBody>,
|
55 62 | connect_timeout: Option<TransportTimeout>,
|
56 63 | ) -> Result<Response<SdkBody>, ConnectorError> {
|
57 64 | validate_request_before_acquisition(&request)
|
58 65 | .map_err(|error| ConnectorError::user(error.into()))?;
|
59 66 |
|
60 67 | let absolute_uri = request.uri().clone();
|
61 68 | let cell = self
|
62 69 | .inner
|
63 70 | .registry
|
64 71 | .resolve_cell(&partition, &absolute_uri)
|
65 72 | .map_err(|error| ConnectorError::user(error.into()))?;
|
66 73 | tracing::trace!(
|
67 74 | request_partition = ?partition.id(),
|
68 75 | origin_scheme = %cell.id().origin().scheme(),
|
69 76 | origin_host = cell.id().origin().host(),
|
70 77 | origin_port = ?cell.id().origin().port(),
|
71 78 | "request resolved to connection-pool cell"
|
72 79 | );
|
73 80 |
|
74 81 | let owner_spawner = partition
|
75 82 | .owner_spawner()
|
76 83 | .map_err(|error| ConnectorError::user(error.into()))?;
|
77 84 | partition.ensure_maintenance_started(owner_spawner.as_ref());
|
78 85 |
|
79 86 | let context = AcquisitionContext {
|
80 87 | pool: self.inner.clone(),
|
81 88 | partition,
|
82 89 | cell,
|
83 90 | absolute_uri,
|
84 91 | owner_spawner,
|
85 92 | connect_timeout,
|
86 93 | };
|
87 94 |
|
88 - | self.send_h1_request(context, request).await
|
95 + | self.dispatch_acquired(context, request).await
|
96 + | }
|
97 + |
|
98 + | /// Acquires a compatible protocol value and dispatches the request.
|
99 + | ///
|
100 + | /// Reacquisition is allowed only while a protocol dispatcher returns the
|
101 + | /// original request envelope.
|
102 + | async fn dispatch_acquired(
|
103 + | &self,
|
104 + | context: AcquisitionContext,
|
105 + | mut request: Request<SdkBody>,
|
106 + | ) -> Result<Response<SdkBody>, ConnectorError> {
|
107 + | let requirement = protocol_requirement(&request);
|
108 + | let mut h2_reacquisitions = H2ReacquisitionBudget::default();
|
109 + |
|
110 + | loop {
|
111 + | match Self::acquire(&context, requirement).await? {
|
112 + | AcquisitionResult::H1(selection) => {
|
113 + | match self.dispatch_h1(&context, request, selection).await? {
|
114 + | H1DispatchResult::Response(response) => return Ok(response),
|
115 + | H1DispatchResult::Reacquire(returned) => request = returned,
|
116 + | }
|
117 + | }
|
118 + | AcquisitionResult::H2(activation) => {
|
119 + | match self.dispatch_h2(&context, request, activation).await? {
|
120 + | H2DispatchResult::Response(response) => return Ok(response),
|
121 + | H2DispatchResult::Reacquire(reacquisition) => {
|
122 + | let (returned, error) = reacquisition.into_parts();
|
123 + | if !h2_reacquisitions.admit_replacement() {
|
124 + | return Err(error);
|
125 + | }
|
126 + | request = returned;
|
127 + | }
|
128 + | }
|
129 + | }
|
130 + | AcquisitionResult::Failed(_) => {
|
131 + | unreachable!("failed acquisition returned as a successful result")
|
132 + | }
|
133 + | AcquisitionResult::Reacquire => {
|
134 + | unreachable!("internal reacquisition escaped the acquisition loop")
|
135 + | }
|
136 + | }
|
137 + | }
|
138 + | }
|
139 + |
|
140 + | /// Acquires one protocol value compatible with the request.
|
141 + | ///
|
142 + | /// Local reusable values complete immediately. Otherwise one waiter
|
143 + | /// remains registered while it receives a reusable protocol value, an
|
144 + | /// establishment failure, or authority to start establishment.
|
145 + | async fn acquire(
|
146 + | context: &AcquisitionContext,
|
147 + | requirement: ProtocolRequirement,
|
148 + | ) -> Result<AcquisitionResult, ConnectorError> {
|
149 + | 'acquire: loop {
|
150 + | if requirement.accepts_h2() {
|
151 + | if let Some(activation) = OriginCell::select_h2(&context.cell) {
|
152 + | tracing::trace!(
|
153 + | connection_id = %activation.connection().id(),
|
154 + | request_partition = ?context.partition.id(),
|
155 + | connection_partition = ?activation.connection().owner_partition(),
|
156 + | origin_scheme = %context.cell.id().origin().scheme(),
|
157 + | origin_host = context.cell.id().origin().host(),
|
158 + | origin_port = ?context.cell.id().origin().port(),
|
159 + | "HTTP/2 pool hit; activating local generation"
|
160 + | );
|
161 + | return Ok(AcquisitionResult::H2(activation));
|
162 + | }
|
163 + | }
|
164 + | if requirement.accepts_h1() {
|
165 + | if let Some(selection) = OriginCell::select_h1(&context.cell) {
|
166 + | tracing::trace!(
|
167 + | connection_id = %selection.connection_id(),
|
168 + | request_partition = ?context.partition.id(),
|
169 + | connection_partition = ?selection.connection().owner_partition(),
|
170 + | origin_scheme = %selection.connection().info().origin().scheme(),
|
171 + | origin_host = selection.connection().info().origin().host(),
|
172 + | origin_port = ?selection.connection().info().origin().port(),
|
173 + | "HTTP/1 pool hit; reusing idle connection"
|
174 + | );
|
175 + | return Ok(AcquisitionResult::H1(selection));
|
176 + | }
|
177 + | }
|
178 + |
|
179 + | let waiter = OriginCell::register_waiter(&context.cell, requirement);
|
180 + | tracing::trace!(
|
181 + | request_partition = ?context.partition.id(),
|
182 + | origin_scheme = %context.cell.id().origin().scheme(),
|
183 + | origin_host = context.cell.id().origin().host(),
|
184 + | origin_port = ?context.cell.id().origin().port(),
|
185 + | protocol_requirement = ?requirement,
|
186 + | "connection acquisition queued"
|
187 + | );
|
188 + | let mut waiter_guard = WaiterGuard::new(context.cell.clone(), waiter);
|
189 + | loop {
|
190 + | match poll_fn(|cx| context.cell.poll_waiter(waiter, cx)).await {
|
191 + | AcquisitionEvent::Complete(AcquisitionResult::H1(selection)) => {
|
192 + | waiter_guard.disarm();
|
193 + | return Ok(AcquisitionResult::H1(selection));
|
194 + | }
|
195 + | AcquisitionEvent::Complete(AcquisitionResult::H2(activation)) => {
|
196 + | waiter_guard.disarm();
|
197 + | OriginCell::service_h2_waiters(&context.cell);
|
198 + | OriginCell::service_peer_h2_waiters(&context.cell);
|
199 + | tracing::trace!(
|
200 + | connection_id = %activation.connection().id(),
|
201 + | request_partition = ?context.partition.id(),
|
202 + | connection_partition = ?activation.connection().owner_partition(),
|
203 + | origin_scheme = %context.cell.id().origin().scheme(),
|
204 + | origin_host = context.cell.id().origin().host(),
|
205 + | origin_port = ?context.cell.id().origin().port(),
|
206 + | "HTTP/2 acquisition completed"
|
207 + | );
|
208 + | return Ok(AcquisitionResult::H2(activation));
|
209 + | }
|
210 + | AcquisitionEvent::Complete(AcquisitionResult::Failed(error)) => {
|
211 + | waiter_guard.disarm();
|
212 + | return Err(error);
|
213 + | }
|
214 + | AcquisitionEvent::Complete(AcquisitionResult::Reacquire) => {
|
215 + | waiter_guard.disarm();
|
216 + | continue 'acquire;
|
217 + | }
|
218 + | AcquisitionEvent::Establish(permit) => {
|
219 + | tracing::trace!(
|
220 + | request_partition = ?context.partition.id(),
|
221 + | origin_scheme = %context.cell.id().origin().scheme(),
|
222 + | origin_host = context.cell.id().origin().host(),
|
223 + | origin_port = ?context.cell.id().origin().port(),
|
224 + | "connection establishment starting"
|
225 + | );
|
226 + | let attempt =
|
227 + | establish::establish(context.clone(), waiter, permit, requirement);
|
228 + | let completion =
|
229 + | EstablishmentCompletionGuard::new(context.cell.clone(), waiter);
|
230 + | context.owner_spawner.spawn(Box::pin(async move {
|
231 + | let mut completion = completion;
|
232 + | if !completion.start() {
|
233 + | drop(attempt);
|
234 + | completion.disarm();
|
235 + | return;
|
236 + | }
|
237 + | match attempt.await {
|
238 + | establish::EstablishmentOutcome::Complete(result) => {
|
239 + | completion.complete(result);
|
240 + | }
|
241 + | establish::EstablishmentOutcome::Transferred => completion.disarm(),
|
242 + | }
|
243 + | }));
|
244 + | }
|
245 + | }
|
246 + | }
|
247 + | }
|
248 + | }
|
249 + | }
|
250 + |
|
251 + | /// Per-request bound on replacement HTTP/2 selections.
|
252 + | #[derive(Default)]
|
253 + | struct H2ReacquisitionBudget {
|
254 + | /// Replacement selections admitted after the initial selection.
|
255 + | completed: usize,
|
256 + | }
|
257 + |
|
258 + | impl H2ReacquisitionBudget {
|
259 + | /// Returns whether one more replacement selection may proceed.
|
260 + | fn admit_replacement(&mut self) -> bool {
|
261 + | if self.completed >= MAX_H2_REACQUISITIONS {
|
262 + | return false;
|
263 + | }
|
264 + | self.completed += 1;
|
265 + | true
|
266 + | }
|
267 + | }
|
268 + |
|
269 + | /// Guarantees a terminal result for one submitted establishment attempt.
|
270 + | ///
|
271 + | /// The guard enters the owner-runtime future before submission. Runtime task
|
272 + | /// drop reports failure to the waiter. A result that wins before the submitted
|
273 + | /// future starts disarms the guard without polling the connector.
|
274 + | struct EstablishmentCompletionGuard {
|
275 + | cell: Arc<OriginCell>,
|
276 + | waiter: WaiterId,
|
277 + | active: bool,
|
278 + | }
|
279 + |
|
280 + | impl EstablishmentCompletionGuard {
|
281 + | fn new(cell: Arc<OriginCell>, waiter: WaiterId) -> Self {
|
282 + | Self {
|
283 + | cell,
|
284 + | waiter,
|
285 + | active: true,
|
286 + | }
|
287 + | }
|
288 + |
|
289 + | fn start(&self) -> bool {
|
290 + | self.cell.start_establishment(self.waiter)
|
291 + | }
|
292 + |
|
293 + | fn complete(mut self, result: AcquisitionResult) {
|
294 + | self.active = false;
|
295 + | self.cell.complete_establishment(self.waiter, result);
|
296 + | }
|
297 + |
|
298 + | fn disarm(&mut self) {
|
299 + | self.active = false;
|
300 + | }
|
301 + | }
|
302 + |
|
303 + | impl Drop for EstablishmentCompletionGuard {
|
304 + | fn drop(&mut self) {
|
305 + | if self.active {
|
306 + | let error = ConnectorError::io(EstablishmentTaskDropped.into());
|
307 + | self.cell
|
308 + | .complete_establishment(self.waiter, AcquisitionResult::Failed(error));
|
309 + | tracing::debug!(
|
310 + | request_partition = ?self.cell.id().partition(),
|
311 + | connection_partition = ?self.cell.id().partition(),
|
312 + | origin_scheme = %self.cell.id().origin().scheme(),
|
313 + | origin_host = self.cell.id().origin().host(),
|
314 + | origin_port = ?self.cell.id().origin().port(),
|
315 + | "connection establishment task dropped"
|
316 + | );
|
317 + | }
|
318 + | }
|
319 + | }
|
320 + |
|
321 + | /// The owner runtime discarded an establishment future before completion.
|
322 + | #[derive(Debug)]
|
323 + | struct EstablishmentTaskDropped;
|
324 + |
|
325 + | impl std::fmt::Display for EstablishmentTaskDropped {
|
326 + | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
327 + | f.write_str("the owner-runtime connection establishment task was dropped")
|
328 + | }
|
329 + | }
|
330 + |
|
331 + | impl std::error::Error for EstablishmentTaskDropped {}
|
332 + |
|
333 + | /// Cancels a request's waiter until it consumes a terminal acquisition result.
|
334 + | struct WaiterGuard {
|
335 + | cell: Arc<OriginCell>,
|
336 + | waiter: WaiterId,
|
337 + | active: bool,
|
338 + | }
|
339 + |
|
340 + | impl WaiterGuard {
|
341 + | fn new(cell: Arc<OriginCell>, waiter: WaiterId) -> Self {
|
342 + | Self {
|
343 + | cell,
|
344 + | waiter,
|
345 + | active: true,
|
346 + | }
|
347 + | }
|
348 + |
|
349 + | fn disarm(&mut self) {
|
350 + | self.active = false;
|
351 + | }
|
352 + | }
|
353 + |
|
354 + | impl Drop for WaiterGuard {
|
355 + | fn drop(&mut self) {
|
356 + | if self.active {
|
357 + | OriginCell::cancel_waiter(&self.cell, self.waiter);
|
358 + | }
|
89 359 | }
|
90 360 | }
|
91 361 |
|
92 362 | /// Rejects request forms the pool cannot route before acquiring connection state.
|
93 363 | fn validate_request_before_acquisition(
|
94 364 | request: &Request<SdkBody>,
|
95 365 | ) -> Result<(), RequestDispatchError> {
|
366 + | if request.version() == Version::HTTP_2 && request.method() == Method::CONNECT {
|
367 + | return Err(RequestDispatchError::ExtendedConnectUnsupported);
|
368 + | }
|
369 + | if request.version() == Version::HTTP_2 && has_upgrade_semantics(request) {
|
370 + | return Err(RequestDispatchError::UpgradeOverHttp2);
|
371 + | }
|
96 372 | match request.version() {
|
97 373 | Version::HTTP_11 | Version::HTTP_2 => Ok(()),
|
98 374 | Version::HTTP_10 if request.method() == Method::CONNECT => {
|
99 375 | Err(RequestDispatchError::ConnectOverHttp10)
|
100 376 | }
|
101 377 | Version::HTTP_10 => Ok(()),
|
102 378 | version => Err(RequestDispatchError::UnsupportedVersion(version)),
|
103 379 | }
|
104 380 | }
|
105 381 |
|
382 + | /// Returns the protocol capability needed to preserve the request's wire semantics.
|
383 + | fn protocol_requirement(request: &Request<SdkBody>) -> ProtocolRequirement {
|
384 + | if request.version() == Version::HTTP_2 {
|
385 + | ProtocolRequirement::H2Required
|
386 + | } else if request.version() == Version::HTTP_10
|
387 + | || request.method() == Method::CONNECT
|
388 + | || has_upgrade_semantics(request)
|
389 + | {
|
390 + | ProtocolRequirement::H1Required
|
391 + | } else {
|
392 + | ProtocolRequirement::H1Compatible
|
393 + | }
|
394 + | }
|
395 + |
|
396 + | /// Returns whether the request asks HTTP/1 to switch protocols.
|
397 + | fn has_upgrade_semantics(request: &Request<SdkBody>) -> bool {
|
398 + | request.headers().contains_key(header::UPGRADE)
|
399 + | || request
|
400 + | .headers()
|
401 + | .get_all(header::CONNECTION)
|
402 + | .iter()
|
403 + | .any(|value| {
|
404 + | value.to_str().is_ok_and(|value| {
|
405 + | value
|
406 + | .split(',')
|
407 + | .any(|token| token.trim().eq_ignore_ascii_case("upgrade"))
|
408 + | })
|
409 + | })
|
410 + | }
|
411 + |
|
412 + | /// Marker for a `Host` header synthesized during an HTTP/1 dispatch attempt.
|
413 + | #[derive(Clone, Copy, Debug)]
|
414 + | struct H1HostHeaderInserted;
|
415 + |
|
106 416 | /// A request rejected before Hyper accepts it for dispatch.
|
107 417 | #[derive(Debug)]
|
108 418 | enum RequestDispatchError {
|
109 419 | /// The absolute URI did not contain the authority required for `Host`.
|
110 420 | MissingAuthority,
|
111 421 | /// The derived `Host` value was not a valid HTTP header value.
|
112 422 | InvalidHostHeader(http_1x::header::InvalidHeaderValue),
|
113 423 | /// HTTP/1.0 cannot represent a CONNECT request accepted by this client.
|
114 424 | ConnectOverHttp10,
|
115 425 | /// The request selected an HTTP version this client cannot dispatch.
|
116 426 | UnsupportedVersion(Version),
|
427 + | /// Extended CONNECT requires HTTP/2 stream-lifecycle support.
|
428 + | ExtendedConnectUnsupported,
|
429 + | /// HTTP/1 upgrade semantics cannot be represented by this HTTP/2 path.
|
430 + | UpgradeOverHttp2,
|
117 431 | }
|
118 432 |
|
119 433 | impl std::fmt::Display for RequestDispatchError {
|
120 434 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
121 435 | match self {
|
122 436 | Self::MissingAuthority => f.write_str("request URI must contain an authority"),
|
123 437 | Self::InvalidHostHeader(_) => {
|
124 438 | f.write_str("request URI authority is not a valid Host header")
|
125 439 | }
|
126 440 | Self::ConnectOverHttp10 => f.write_str("CONNECT is not supported over HTTP/1.0"),
|
127 441 | Self::UnsupportedVersion(version) => {
|
128 442 | write!(f, "unsupported HTTP version: {version:?}")
|
129 443 | }
|
444 + | Self::ExtendedConnectUnsupported => {
|
445 + | f.write_str("extended CONNECT over HTTP/2 is not supported")
|
446 + | }
|
447 + | Self::UpgradeOverHttp2 => {
|
448 + | f.write_str("HTTP/1 protocol upgrade semantics cannot use HTTP/2")
|
449 + | }
|
130 450 | }
|
131 451 | }
|
132 452 | }
|
133 453 |
|
134 454 | impl std::error::Error for RequestDispatchError {
|
135 455 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
136 456 | match self {
|
137 457 | Self::InvalidHostHeader(error) => Some(error),
|
138 458 | _ => None,
|
139 459 | }
|
140 460 | }
|
141 461 | }
|
462 + |
|
463 + | #[cfg(all(test, not(smithy_http_client_loom)))]
|
464 + | mod tests {
|
465 + | use super::*;
|
466 + |
|
467 + | fn request(method: Method, version: Version) -> Request<SdkBody> {
|
468 + | Request::builder()
|
469 + | .method(method)
|
470 + | .uri("https://example.com/resource")
|
471 + | .version(version)
|
472 + | .body(SdkBody::empty())
|
473 + | .unwrap()
|
474 + | }
|
475 + |
|
476 + | #[test]
|
477 + | fn request_semantics_select_protocol_capability() {
|
478 + | assert_eq!(
|
479 + | ProtocolRequirement::H1Compatible,
|
480 + | protocol_requirement(&request(Method::GET, Version::HTTP_11))
|
481 + | );
|
482 + | assert_eq!(
|
483 + | ProtocolRequirement::H2Required,
|
484 + | protocol_requirement(&request(Method::GET, Version::HTTP_2))
|
485 + | );
|
486 + | assert_eq!(
|
487 + | ProtocolRequirement::H1Required,
|
488 + | protocol_requirement(&request(Method::GET, Version::HTTP_10))
|
489 + | );
|
490 + | assert_eq!(
|
491 + | ProtocolRequirement::H1Required,
|
492 + | protocol_requirement(&request(Method::CONNECT, Version::HTTP_11))
|
493 + | );
|
494 + |
|
495 + | let mut upgrade = request(Method::GET, Version::HTTP_11);
|
496 + | upgrade
|
497 + | .headers_mut()
|
498 + | .insert(header::CONNECTION, "keep-alive, Upgrade".parse().unwrap());
|
499 + | assert_eq!(
|
500 + | ProtocolRequirement::H1Required,
|
501 + | protocol_requirement(&upgrade)
|
502 + | );
|
503 + | }
|
504 + |
|
505 + | #[test]
|
506 + | fn unsupported_h2_tunnel_and_upgrade_forms_are_rejected() {
|
507 + | assert!(matches!(
|
508 + | validate_request_before_acquisition(&request(Method::CONNECT, Version::HTTP_2)),
|
509 + | Err(RequestDispatchError::ExtendedConnectUnsupported)
|
510 + | ));
|
511 + |
|
512 + | let mut upgrade = request(Method::GET, Version::HTTP_2);
|
513 + | upgrade
|
514 + | .headers_mut()
|
515 + | .insert(header::UPGRADE, "websocket".parse().unwrap());
|
516 + | assert!(matches!(
|
517 + | validate_request_before_acquisition(&upgrade),
|
518 + | Err(RequestDispatchError::UpgradeOverHttp2)
|
519 + | ));
|
520 + | }
|
521 + |
|
522 + | #[test]
|
523 + | fn h2_reacquisition_is_bounded_after_two_replacements() {
|
524 + | let mut budget = H2ReacquisitionBudget::default();
|
525 + | for _ in 0..MAX_H2_REACQUISITIONS {
|
526 + | assert!(budget.admit_replacement());
|
527 + | }
|
528 + | assert!(!budget.admit_replacement());
|
529 + | }
|
530 + | }
|