1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
//! Regression gate on the depth of Autumn's ingress middleware stack (#2193,
//! #2198), plus the order characterization that gate must not be bought with.
//!
//! # What is being gated, and why depth is the right quantity
//!
//! `axum::routing::Route` is a newtype over `tower::util::BoxCloneSyncService`,
//! and `Router::layer` re-boxes: `Route::layer` ends in `Route::new(..)`, which
//! calls `BoxCloneSyncService::new(..)` again. So *N* sequential `.layer()`
//! calls produce *N* **nested** boxed services, not one flat stack.
//!
//! That nesting is not merely N boxes at build time — it costs on every
//! request, quadratically. `Route::call` runs `self.0.clone().oneshot(req)`,
//! and cloning a `BoxCloneSyncService` is `Box::new(self.clone())`, which
//! deep-clones the service it wraps — including the next `Route` down, which
//! boxes again, all the way to the leaf. A request descending *N* levels
//! therefore triggers a full deep clone at each level: `N + (N-1) + … + 1`
//! heap allocations. Measured against axum 0.8.9, per-request allocations for
//! *N* stacked no-op layers fit `13 + N(N+1)/2 + 2N` (13 being the fixed
//! per-request baseline at N = 0): 263 allocations at N = 20, 1388 at N = 50 —
//! while the *same* layers composed into a single `Router::layer` call cost a
//! constant 16 regardless of N.
//!
//! # How the depth is observed
//!
//! Every one of those deep clones passes through *every* service below the
//! level that initiated it. So a probe service installed at the innermost
//! position is cloned exactly once per traversal of the stack above it.
//! Counting its clones over a single request yields an exact integer: no
//! timing, no allocator hooks, no sampling, identical on every platform and in
//! debug or release.
//!
//! What that integer counts is **every service above the probe that clones on
//! call**, which is the `Route` box levels *plus* each
//! `axum::middleware::from_fn` (its generated `Service::call` starts with
//! `self.inner.clone()`) *plus* every hand-rolled service that has to clone its
//! inner to move it into a boxed future. Collapsing `Router::layer` calls
//! removes box levels; it does not remove clone-on-call traversals. So the
//! number moves with both, and a new `from_fn` inside an existing tuple raises
//! it without adding a box.
//!
//! Since #2214 that second half is also a proxy for a *heap allocation*: a
//! service clones its inner precisely when it needs to own it inside a
//! `Box::pin`ned future, which is the per-request allocation #2214 measured at
//! 19.6% of all bytes. A service with a named `pin_project!` future neither
//! clones nor boxes, so both costs leave together and this gate sees it.
//!
//! The probe is attached to a `MethodRouter` in a merged raw router, which
//! `mount_raw_routers` mounts *after* `build_router_pre_state` has already
//! applied the asset cache-control layer (and, under `i18n` locale prefixes,
//! the locale-routing extension). A route declared with `#[get]` therefore sits
//! one or two traversals deeper than the number measured here; the gate tracks
//! relative change, not a route's absolute cost.
//!
//! # Why the assertion is a window
//!
//! The measured integer decomposes as **D + C**. *D* counts `Route` box levels
//! above the probe: one per `Router::layer` call applied above it, plus one for
//! the base `Route` box the leaf `MethodRouter` is stored in. *C* counts the
//! clone-on-call services above the probe. Only *D* moves when `Router::layer`
//! calls are collapsed; *C* moves with the enabled Cargo features and with how
//! each middleware is written.
//!
//! Before #2214, `cargo test -p autumn-web` (the 8 default features) measured
//! **13** (D = 5, C = 8) and CI's `cargo test --workspace` — which unifies ~29
//! features across the workspace — measured **14**, because `oauth2` (enabled
//! by `examples/blog`) contributed an extra HTTP-interceptor `from_fn`. #2214
//! converted that interceptor along with the always-on `from_fn`s, so both
//! configurations now measure **9** (D = 5, C = 4).
//!
//! The assertion is still a window rather than an equality: a feature not
//! enabled anywhere in this workspace could reintroduce the spread, and the
//! ceiling is derived from the widest *predicted* configuration rather than the
//! widest current one, so the gate fails on the way back up. See
//! [`INGRESS_TRAVERSAL_WINDOW`] for the arithmetic.
//!
//! The lower bound matters too. Without it, a refactor that mounted merged raw
//! routers *after* the middleware — which is exactly how `/mcp` is treated —
//! would drop the probe out of the framework stack entirely (1-3 traversals),
//! and a ceiling-only assertion would stay green while measuring nothing.
use Arc;
use ;
use ;
use AutumnConfig;
use TestApp;
use ;
use Request;
/// Counts how many times the service it wraps is cloned.
// Hand-written (not derived) so the clone can be counted. Deriving would also
// wrongly require `S: Clone` on the struct itself rather than on the impl.
async
/// Accepted window for how many times a single request may traverse
/// (deep-clone) the framework's ingress stack.
///
/// # The arithmetic behind the bounds
///
/// The measurement is `D + C` (see the module header). *D* is the box-level
/// count — one per `Router::layer` call above the probe, plus the base `Route`
/// box — and *C* is the number of clone-on-call services above the probe, which
/// is fixed by the enabled features, not by how the layers are composed.
///
/// | configuration | D | C | measured |
/// | --- | --- | --- | --- |
/// | default features, before #2198 | 8 | 8 | **16** |
/// | CI workspace-unified features, before #2198 | 8 | 9 | **17** |
/// | default features, after #2198 | 5 | 8 | **13** |
/// | CI workspace-unified features, after #2198 | 5 | 9 | **14** |
/// | default features, after #2214 | 5 | 4 | **9** |
/// | CI workspace-unified features, after #2214 | 5 | 4 | **9** |
///
/// #2198 removes three box levels by folding `apply_middleware`'s four
/// remaining `Router::layer` calls — the inner group, the middle group, the
/// session layer, and the outer group — into a single nested-tuple call.
///
/// #2214 then removes four of the eight clone-on-call services by converting
/// every `axum::middleware::from_fn` on the always-on ingress path into a
/// hand-rolled `tower::Service` with a NAMED future. Four of those conversions
/// are visible to this probe — `EventAppContextLayer`,
/// `WebhookReplayCleanupLayer`, `MethodOverrideRejectionLayer`, and
/// `TrustedHostLayer`. `StartupBarrierLayer` is a fifth, but `apply_startup_barrier`
/// is production-only (`TestApp::build` mirrors just its two response-side
/// fallbacks), so it never entered this count in the first place;
/// `AssetCacheControlLayer` is a sixth, applied *before* the probe's mount
/// point and therefore also outside it. Each conversion removes both a
/// clone-on-call traversal (`FromFn::call` opens with `self.inner.clone()`) and
/// the `Box::pin` `from_fn` wraps its otherwise-unnameable future in — the
/// allocation #2214 is actually about. The *feature*-dependence of *C*
/// disappears with them: `oauth2`'s HTTP-interceptor `from_fn` is converted
/// too, so the default and workspace-unified measurements now agree.
///
/// **Upper bound 9** is the measurement itself, not the measurement plus slack.
/// Before #2214 this bound carried a `+1` of headroom to absorb the feature
/// spread — and that headroom is exactly what made the gate blind to the
/// regression it is named for, since restoring one `from_fn` inside an existing
/// tuple, or one collapsed `Router::layer` call, moves the number by exactly
/// one. With `oauth2`'s interceptor converted there is no spread left to
/// absorb: 9 was measured under the 8 default features AND under a
/// 13-feature build adding `oauth2`, `mail`, `storage`, `ws` and `openapi`.
/// So the bound is set to the measurement, and either regression fails it.
///
/// If a feature this workspace does not currently enable ever contributes a
/// tenth traversal, this gate will fail on it. That is the intended outcome:
/// identify the clone-on-call service the feature adds, convert it the way
/// #2214 converted the rest, or widen the window deliberately with the
/// measurement written down here — do not nudge the bound to make a red run
/// green.
///
/// **Lower bound 6** is a sentinel, not a budget. It sits under the current
/// floor (five box levels plus the four remaining clone-on-call services) and
/// well above the 1-3 a probe measures once it has fallen out of the framework
/// stack entirely — the failure mode described in the module header. It is
/// deliberately slack: tightening it toward the real measurement would make
/// every feature-set difference a failure without catching anything the ceiling
/// does not already catch.
///
/// # What this gate is blind to
///
/// It counts clone **events**, not allocations — and those are not the same
/// quantity. Erasing an individual layer through
/// `tower::util::BoxCloneSyncServiceLayer` *removes* a clone event from this
/// count while KEEPING a box and ADDING a boxed future to every request:
/// `BoxCloneSyncService::new` wraps the service in `map_future(Box::pin)`, its
/// `call` forwards to the inner service without cloning it, and its `clone` is
/// a recursive `clone_box`. Per-layer type erasure would therefore drive this
/// number DOWN while driving real per-request allocations UP, and nothing here
/// would notice.
///
/// That is why the fix for #2198 collapses `Router::layer` calls — which
/// deletes whole box levels — rather than erasing layer types, and why a future
/// change that lowers this number by erasure must be judged on allocation
/// counts instead of on this gate. #2214's conversions are judged that way too:
/// `per_request_allocations_stay_under_the_ceiling` and
/// `per_request_allocated_bytes_stay_under_the_ceiling` in
/// `tests/config_alloc_gate.rs` pin the blocks and bytes a request actually
/// allocates, which is what a `from_fn` box costs and what this count can only
/// stand proxy for.
const INGRESS_TRAVERSAL_WINDOW: RangeInclusive = 6..=9;
/// Build the production router with a clone-counting probe at the innermost
/// position, drive one request through it, and return the traversal count.
async
/// [`ingress_traversals_per_request`], with a hook to customize the `TestApp`
/// (register operator layers / static gates) before it is built.
async
/// App-wide operator layer whose service forwards without cloning its inner —
/// so the ONLY thing registering it can add to the traversal count is the box
/// level its application costs. Generic over the inner service, like every
/// real-world tower layer, so it satisfies `IntoAppLayer` both before and
/// after #2198's registration-time erasure.
;
async
async
async
async
/// CHARACTERIZATION (not a regression gate): pins the ingress *order* that the
/// depth collapse in #2198 must not disturb. It is green BEFORE the collapse —
/// against the four separate `Router::layer` calls — and must stay green AFTER
/// it, when those four become one nested tuple. A failure here means the
/// collapse reordered the stack, not that it failed to shrink it (that is
/// [`ingress_stack_depth_stays_within_budget`]).
///
/// The two composition forms run in OPPOSITE directions — consecutive
/// `Router::layer` calls put the LAST call outermost, a `tower-layer` tuple puts
/// its FIRST element outermost — and every framework layer is `Route -> Route`
/// with `Error = Infallible`, so a group written in the wrong order still
/// compiles and still type-checks. Both assertions below are therefore chosen to
/// FAIL under a reversal of the merged tuple's group order:
///
/// 1. A `400` produced by `method_override_rejection_filter` (innermost group)
/// still carries `x-request-id`, which only `RequestIdLayer` (middle group)
/// stamps. Reversed, the inner group would sit OUTSIDE `RequestIdLayer` and
/// the rejection would never reach it.
/// 2. That same `400` outranks CSRF's `403` on a request that carries neither a
/// valid `_method` nor a CSRF token — the ordering `router.rs` states as
/// "[method-override rejection] placed outside CSRF so ... a clear `400`
/// invalid `_method` outranks 'missing CSRF'". The paired control proves CSRF
/// is live in the very same app, so the `400` is an ordering result rather
/// than a disabled layer.
///
/// Deliberately NOT re-asserted here, to avoid duplicating an existing pin:
/// `ClientAddr` being populated before user/handler code (owned by
/// `trusted_proxy_resolution_runs_before_client_addr_is_read` in
/// `middleware_stack_order.rs`), and the bare `400`-with-framework-headers case
/// under DEFAULT config (owned by
/// `invalid_method_override_response_carries_framework_middleware` in
/// `src/test.rs`) — this test only adds the CSRF-enabled discriminator that
/// neither covers.
async
/// The framework's per-request overhead must be a CONSTANT, not a function of
/// how many app-wide layers an operator (or a plugin — `Plugin::build` receives
/// the same `AppBuilder`, so plugin layers register through the identical path)
/// has attached. Before #2198 every `AppBuilder::layer`/`static_gate`
/// registration was applied as its own `Router::layer` call: one more
/// `BoxCloneSyncService` nesting level per registration, re-cloned by every
/// clone initiator above it on every request. After #2198 the registrations are
/// type-erased once at registration time and composed into the framework's
/// single merged application, so the box-level count `D` — and with it this
/// probe's traversal count — does not move at all.
///
/// The passthrough layers here neither box nor clone on call, so the ONLY
/// thing their registration can contribute is composition overhead — which is
/// exactly what this test pins to zero. A layer whose own `Service::call`
/// clones (a `from_fn`, say) still adds its clone-on-call traversal; that cost
/// belongs to the operator's code, not to the framework's composition, and is
/// deliberately out of scope here.
async