gunnar_wire/limits.rs
1//! The fetch path's allocation ceilings.
2//!
3//! # Why this module exists
4//!
5//! `gunnar-pack`'s [`IndexOptions::alloc_limit_bytes`] guards the **receive**
6//! path against a hostile pack claiming an enormous object. Nothing guarded the
7//! **serve** path, and the serve path has the same two sources of
8//! attacker-chosen size:
9//!
10//! * **the request** — a v2 argument section and a v0 `want`/`have` exchange are
11//! read until the peer sends a flush-pkt, into a `Vec` that grows with every
12//! line. Nothing said how many lines that could be. A client that opens a
13//! connection and sends `have <oid>` forever makes the server allocate
14//! forever, and it does so *before* any repository is touched, so no
15//! traversal budget is in the way;
16//! * **an object** — the whole-object writer materialises each object's
17//! inflated content before it deflates it, so peak memory is the largest
18//! object in the transfer. That size is not chosen by this request, but it was
19//! chosen by whoever pushed it.
20//!
21//! # The principle, restated because it is the same one
22//!
23//! `gunnar-pack`'s [`read`] module puts it as: **a guard that is off by default
24//! is not a guard.** Both ceilings here default to `Some`, and both are
25//! expressible as `None` by a caller that asks — [`Limits::no_alloc_limit`] —
26//! which is the shape that made the pack indexer's guard real rather than
27//! optional.
28//!
29//! # What each ceiling actually prevents
30//!
31//! [`Limits::request_bytes`] is **preventive**: the bytes are counted as they
32//! are framed, and the read stops at the ceiling. Nothing above it is ever held.
33//!
34//! [`Limits::object_bytes`] is **preventive too, and that is why it is applied
35//! where it is**. It is enforced in the object *selection* walk, from the
36//! object's header, before any content is read — not in the pack writer, which
37//! only ever sees an object that has already been materialised. A ceiling
38//! checked after `gix_object::Find::try_find` has filled a buffer is a report,
39//! not a guard: the allocation it exists to stop has already happened by the
40//! time it can fire.
41//!
42//! An object over the ceiling is **refused, not skipped**. Skipping it would
43//! produce a pack whose closure does not hold, which is a corrupt clone rather
44//! than a refused one — the same judgement `gunnar-store`'s `write_stream`
45//! makes about an oversized declared size.
46//!
47//! # What is still `O(repository)`, said plainly
48//!
49//! These ceilings bound the two sizes gunnar does not choose. They do **not**
50//! make a fetch constant-memory, and the remaining terms are all the same shape
51//! — one machine word or one object id per object in the transfer — so they are
52//! listed with what they cost at ten million sha256 objects rather than left as
53//! "some overhead":
54//!
55//! | | where | ~bytes/object | at 10M |
56//! |---|---|---|---|
57//! | the selection | `graph::Selection::objects` | 32 | 320 MB |
58//! | the client's closure | `graph::select_objects` step 5 | ~51 | 510 MB |
59//! | the writer's de-duplicated copy | `gunnar_pack::write::prepare_ids` | 32 + ~51 transient | 830 MB |
60//! | gix's count list | `gunnar_pack::copy::write_pack_copy` | ~48 | 480 MB |
61//! | the `ofs-delta` offset table | `gunnar_pack::copy::write_pack_copy` | 16 | 160 MB |
62//!
63//! And one term whose denominator is **the pack**, not the transfer, so it is
64//! listed apart rather than added to the column above:
65//!
66//! | | where | ~bytes/object **in the pack** | at a 10M-object pack |
67//! |---|---|---|---|
68//! | The entry-extent table | `gunnar_pack::copy::PackExtents` | 48 (sha256) / 36 (sha1) | 480 MB |
69//!
70//! It buys the removal of one full inflate per object served — MEASURED
71//! 2026-08-04 over the 19-repository corpus, **1.97× less server CPU on a clone
72//! at the median** and 21.4% less peak RSS on `znippy.git` — and it is built
73//! once per request and shared across `iter_from_counts`'s clones rather than
74//! per worker. Two thirds of it (`Vec<(Offset, ObjectId)>`) is a table
75//! `iter_from_counts` already builds for itself whenever a **thin** fetch has to
76//! resolve a delta base, so on that path the new term is the 8-byte offset
77//! column alone. It is also **one pack's** table and not the store's: only the
78//! pack `gix-odb` searched most recently is consulted, so a repository with many
79//! small packs holds one table per pack it actually touched rather than one per
80//! pack it has. A repository whose pack dwarfs the transfer is where this costs
81//! most and where it saves least, and `.rev`-file support in `gix-pack` — which
82//! would let the table be read rather than built — is the answer if that ever
83//! shows up.
84//!
85//! Since the thin-pack ruling the client's-closure row is **live for longer**
86//! rather than larger. It used to die at the end of `select_objects`; it is
87//! now carried out as `graph::Selection::client_has`, because vouching for the
88//! base of a thin delta is what lets a fetch ship the deltas it already had
89//! instead of expanding them. There is no second copy — the same set is moved,
90//! and `graph::tests::a_fetch_holds_the_clients_object_closure_once` still
91//! holds the peak to one — but it now overlaps the last three rows instead of
92//! preceding them.
93//!
94//! What bounds it is `Exclusion::Edges`, the shipping default: that walk opens
95//! up the frontier rather than the client's history, so the set carried to the
96//! writer is frontier-sized. The same test asserts it, as live ids, against the
97//! `Exclusion::Full` figure. Under `GUNNAR_EXCLUSION=full` the row is the
98//! client's whole closure and does overlap the write.
99//!
100//! `crate::DEFAULT_BUDGET` bounds all of them at once, but it bounds them in
101//! **objects**, and ten million objects of that bookkeeping is on the order of a
102//! gigabyte before a single byte of content is read. That is the honest reading
103//! of the budget, and the `git` package's reachability bitmaps are what removes the first two
104//! rows rather than caps them.
105//!
106//! [`IndexOptions::alloc_limit_bytes`]: gunnar_pack::IndexOptions::alloc_limit_bytes
107//! [`read`]: gunnar_pack::DEFAULT_ALLOC_LIMIT_BYTES
108
109/// Ceiling on the bytes held for one peer-supplied request: 8 MiB.
110///
111/// The size being guarded is **the peer's to choose** and costs it one pkt-line
112/// header per 65 516 bytes, so the ratio between what an attacker spends and
113/// what the server spends is about 1:1 — which is why the ceiling has to be an
114/// absolute number rather than a multiple of anything.
115///
116/// 8 MiB is roughly 200 000 object ids at v2's `want <oid>` / `have <oid>`
117/// line width. Stock `git` sends its `have`s in windows and gives up after 256
118/// unproductive rounds, so a real negotiation is three orders of magnitude
119/// under this; a partial clone's lazy fetch, the largest legitimate `want` list
120/// gunnar can be shown, batches its requests and is two orders under.
121pub const DEFAULT_REQUEST_LIMIT_BYTES: usize = 8 << 20;
122
123/// Ceiling on a single object gunnar will materialise while serving: 1 GiB.
124///
125/// The **same number** `gunnar-pack` applies to an entry decoded out of an
126/// untrusted pack. It is stated once, in [`crate::pack`], rather than twice
127/// here and there (one implementation) — and because a `pub const` naming
128/// `gunnar_pack` is enough on its own to link the whole pack pipeline into the
129/// appliance, which is what [`crate::pack`]'s header is about. The two are
130/// held equal by
131/// `pack::tests::the_allocation_ceiling_matches_the_one_gunnar_pack_enforces`,
132/// where `gunnar-pack` is a dev-dependency and costs the shipping graph
133/// nothing. An object that could not be received cannot usefully be served.
134pub const DEFAULT_OBJECT_LIMIT_BYTES: usize = crate::pack::DEFAULT_ALLOC_LIMIT_BYTES;
135
136/// What one object id held by a traversal costs in resident bytes: 384.
137///
138/// # It is measured, and it is a rate, not a struct size
139///
140/// The tempting number is `size_of::<ObjectId>()`, which is 33. It is wrong by
141/// an order of magnitude, because one id in flight is not one id: it is an
142/// entry in the walk's `seen` set, a slot in the selection vector, the pack
143/// writer's de-duplicated copy of that vector, gix's count list and its
144/// `ofs-delta` offset table — the five rows this module's own table above
145/// lists, plus hashbrown's load-factor slack on the two that are sets.
146///
147/// So it is measured end to end instead: `VmHWM` for a process that serves
148/// exactly one bitmap-less clone and does nothing else, divided by the objects
149/// it sent. `bench/examples/walkbudget-probe.rs`, 2026-08-05:
150///
151/// | corpus | objects | wire | B/object |
152/// |---|---|---|---|
153/// | `h2h-linear-sha1-2048c` | 36 172 | 6.2 MB | 791.1 |
154/// | `h2h-linear-sha256-2048c` | 36 172 | 6.8 MB | 808.9 |
155/// | `forge-year-8a-240b-12c` | 28 893 | 24.5 MB | 1 572.9 |
156/// | `h2h-linear-sha1-20480c` | 352 396 | 64.9 MB | 627.6 |
157/// | `kernelscale` (14M, past the kernel) | 14 042 000 | 599.0 MB | **393.8** |
158///
159/// **The rate falls as the corpus grows, and this constant is the value at the
160/// scale the ceiling is about.** The small-corpus rows are dominated by a fixed
161/// intercept — the process, the mmap'd pack index, gix's caches — which does
162/// not repeat per request-object and would make the charge nonsense if it were
163/// baked in. Fitting the two largest rows gives a marginal cost of ~390 B and
164/// the largest row's whole-process rate is 393.8, so 384 (a multiple of 64
165/// under both) is the charge.
166///
167/// # Two things it does not claim
168///
169/// **It is not hash-dependent.** sha1 and sha256 at the same object count
170/// measured 791.1 and 808.9 B/object — 2.2% apart, where `size_of` reasoning
171/// predicts 47%. The id is a third of one row of five and the rest do not
172/// change width, so charging two rates would be precision the measurement does
173/// not support.
174///
175/// **It does not cover content.** `forge-year-8a-240b-12c` sits at 1 572.9
176/// B/object, 4× this rate, and the reason is in its wire column: 848 bytes per
177/// object of content against `h2h-linear-sha1-20480c`'s 184. That term is the
178/// source pack resident under gix's mmap and the writer's per-object buffer,
179/// and it is bounded by [`Limits::object_bytes`] and by the fact that the
180/// writer streams — not by counting ids. Fitting the ladder as
181/// `a + 390·objects + 1.05·wire_bytes` predicts every row within 13%, which is
182/// the honest statement of what an id-counted ceiling can and cannot bound.
183pub const WALK_BYTES_PER_OBJECT: usize = 384;
184
185/// Ceiling on the object bookkeeping **one fetch** may hold: 8 GiB.
186///
187/// # Why this is in bytes when the thing it replaces counted objects
188///
189/// `crate::DEFAULT_BUDGET` counted objects, and an operator sizing a machine
190/// does not have an object count — they have `free -g`. Worse, an object count
191/// cannot be *pooled*: adding one request's ten million to another's ten
192/// million produces a number that means nothing about the box, which is why the
193/// server-wide half of this ([`WalkPool`]) could not exist until the unit
194/// changed.
195///
196/// # Why 8 GiB and not less
197///
198/// Because the bar is that the kernel clones. MEASURED 2026-08-05,
199/// `bench/examples/walkbudget-probe.rs --scale` on a 14 042 000-object fixture
200/// with no `.bitmap` — 1.6% past the kernel's 13 816 503 — the whole clone was
201/// served in **5 460 816 kB** of `VmHWM` and 64 seconds. At
202/// [`WALK_BYTES_PER_OBJECT`] this ceiling admits 22.4M objects, which is that
203/// repository with 60% to spare, and the same run establishes that
204/// `DEFAULT_BUDGET`'s ten million **refuses** it.
205///
206/// # Why 8 GiB is not, by itself, a bound on the server
207///
208/// It is a per-*request* ceiling, and `gunnar serve` admits 256 concurrent
209/// transfers by default. 256 × 8 GiB is 2 TB, so this number on its own bounds
210/// nothing that matters; [`WalkPool`] is the half that does.
211pub const DEFAULT_WALK_LIMIT_BYTES: usize = 8 << 30;
212
213/// The ceilings one connection serves under.
214///
215/// Every field defaults to `Some`. See the module docs for why that is the
216/// whole point.
217#[derive(Debug, Clone, Copy, PartialEq, Eq)]
218pub struct Limits {
219 /// Bytes of pkt-line payload gunnar will hold for one request before it
220 /// refuses. `None` removes the ceiling.
221 pub request_bytes: Option<usize>,
222 /// Bytes of a single object gunnar will put in a pack. `None` removes the
223 /// ceiling.
224 pub object_bytes: Option<usize>,
225 /// Bytes of object bookkeeping one traversal may hold. `None` removes the
226 /// ceiling.
227 ///
228 /// Charged at [`WALK_BYTES_PER_OBJECT`] against the ids **live** in the
229 /// walk, which is not the same as the ids it has emitted — see
230 /// [`crate::DEFAULT_BUDGET`] for the three separate accumulators the object
231 /// count used to charge and the fourth it charged not at all.
232 pub walk_bytes: Option<usize>,
233}
234
235impl Default for Limits {
236 fn default() -> Self {
237 Limits {
238 request_bytes: Some(DEFAULT_REQUEST_LIMIT_BYTES),
239 object_bytes: Some(DEFAULT_OBJECT_LIMIT_BYTES),
240 walk_bytes: Some(DEFAULT_WALK_LIMIT_BYTES),
241 }
242 }
243}
244
245impl Limits {
246 /// The defaults — both ceilings on.
247 pub fn new() -> Self {
248 Limits::default()
249 }
250
251 /// Cap the bytes held for one peer-supplied request.
252 pub fn request_bytes(mut self, limit: usize) -> Self {
253 self.request_bytes = Some(limit);
254 self
255 }
256
257 /// Cap a single object's size.
258 pub fn object_bytes(mut self, limit: usize) -> Self {
259 self.object_bytes = Some(limit);
260 self
261 }
262
263 /// Cap the object bookkeeping one traversal may hold.
264 pub fn walk_bytes(mut self, limit: usize) -> Self {
265 self.walk_bytes = Some(limit);
266 self
267 }
268
269 /// Remove **all** the ceilings entirely.
270 ///
271 /// Only for a peer whose bytes are trusted and a repository whose contents
272 /// are known: a local `gunnar mirror` between two directories on the same
273 /// box, or a benchmark that is measuring what the ceiling costs.
274 /// **Never on a path that answers the network.** Both sizes this guards are
275 /// chosen by somebody other than the server — one by the peer framing the
276 /// request, one by whoever pushed the object — which is the same reason
277 /// `gunnar_pack::IndexOptions::no_alloc_limit` carries the same warning.
278 pub fn no_alloc_limit(mut self) -> Self {
279 self.request_bytes = None;
280 self.object_bytes = None;
281 self.walk_bytes = None;
282 self
283 }
284}
285
286/// **The ceiling on what the whole server holds at once**, in bytes of walk
287/// bookkeeping, shared by every in-flight transfer.
288///
289/// # Why a per-request ceiling was never a bound on the box
290///
291/// [`DEFAULT_WALK_LIMIT_BYTES`] is generous on purpose: the bar is that a
292/// kernel-sized repository with no `.bitmap` clones, and that costs 5.59 GB
293/// (MEASURED, see the constant). `gunnar serve`'s admission gate then admits
294/// **256** concurrent transfers by default — a number chosen against CPU, in
295/// `gunnar_ssh::admission`, with nothing in it about memory. Multiply them and
296/// the shipped server will cheerfully begin 256 clones that between them need
297/// two terabytes, on a box that has not got it. The failure is the OOM killer
298/// picking a victim, which is the one refusal an operator cannot read.
299///
300/// So the ceiling has to be spent from a **shared** purse, and that is the
301/// reason the unit had to become bytes: object counts from two differently
302/// shaped repositories do not add up to anything.
303///
304/// # It admits by size, so small transfers never queue behind a big one
305///
306/// A reservation is taken as the walk grows, not at the door, so a request is
307/// only ever charged for what it is actually holding. A clone of a small
308/// repository never reaches [`WalkPool::FLOOR_BYTES`] and so never touches the
309/// pool at all — the atomic is left for the transfers that can hurt the
310/// machine. Above it, reservations move in [`WalkPool::CHUNK_BYTES`] steps, so
311/// a fourteen-million-object clone costs about 700 atomic operations across
312/// sixty seconds rather than one per object.
313///
314/// # What happens at the ceiling
315///
316/// [`crate::Error::WalkPoolFull`], which reaches the client through
317/// `UploadPack::serve_session`'s error seam as a sentence naming both numbers.
318/// A refusal a client can read and retry is the whole difference between this
319/// and the accidental ceiling it replaces.
320///
321/// # What it does, MEASURED
322///
323/// `bench/examples/walkpool-probe.rs`, 2026-08-05, thirty-two concurrent
324/// bitmap-less clones of a 352 396-object repository in one process, three runs:
325///
326/// | | `VmHWM` | served | refused |
327/// |---|---|---|---|
328/// | one clone alone | 219 MB | 1 | — |
329/// | 32 at once, no pool | **4.66–4.73 GB** | 32 | — |
330/// | 32 at once, ~900 MB pool | **1.09–1.33 GB** | 3–5 | 27–29 |
331///
332/// 21.2–21.6× one clone against 4.97–6.09×, so the fan-out's peak comes down
333/// 3.5–4.3× and every refusal was `WalkPoolFull`. The pooled peak is above the
334/// purse because the purse charges the **walk**: the two or three clones that
335/// got in are also running a pack writer, and the twenty-nine that did not each
336/// held up to [`WalkPool::FLOOR_BYTES`] before being refused.
337///
338/// # This alone does not guarantee progress, and the reason is not fixable here
339///
340/// A walk's size is not known at the door, so reservations are taken **as it
341/// grows**. A purse that is tight against a single walk therefore lets every
342/// walk in a fan-out climb together and then refuses all of them with nobody
343/// finished. MEASURED the same day: four concurrent 14 042 000-object clones
344/// against a purse of two served **zero**, where thirty-two 352 396-object
345/// clones against the same ratio served two — thirty-two threads on
346/// thirty-two cores do not advance in lockstep and four do.
347///
348/// That is bin-packing without knowing the sizes. Three alternative reservation
349/// policies — coarser bites, doubling bites, and refusing a *first* reservation
350/// above a fraction of the purse — were each worked through on paper against
351/// the four-clone case above and each still has a fan-out that jams it; none
352/// was implemented and measured, which is why that is written as reasoning and
353/// not as a result.
354/// The answer is the architecture gunnar already has. **This is the backstop
355/// and `--max-concurrent-transfers` is the primary gate**: the slot cap keeps
356/// the fan-out narrow enough that the purse is not tight against it, and the
357/// purse is what stops N admitted slots from meaning N times the largest
358/// repository. [`WalkPool::for_this_machine`] sizing the purse to `MemTotal`
359/// rather than to a constant is the other half of keeping that true on a box
360/// nobody measured in advance.
361#[derive(Debug)]
362pub struct WalkPool {
363 limit: usize,
364 in_flight: std::sync::atomic::AtomicUsize,
365 peak: std::sync::atomic::AtomicUsize,
366 refusals: std::sync::atomic::AtomicU64,
367}
368
369impl WalkPool {
370 /// Below this much held bookkeeping a request does not touch the pool.
371 ///
372 /// 8 MiB is about 21 800 objects at [`WALK_BYTES_PER_OBJECT`]. Every
373 /// repository in gunnar's own bench corpus except the two largest sits under
374 /// it, which is the point: metering an ordinary clone would put an atomic in
375 /// front of a transfer that could not exhaust anything.
376 pub const FLOOR_BYTES: usize = 8 << 20;
377
378 /// How much a growing walk reserves at a time, above [`Self::FLOOR_BYTES`].
379 pub const CHUNK_BYTES: usize = 8 << 20;
380
381 /// A pool of `limit_bytes`.
382 pub fn new(limit_bytes: usize) -> std::sync::Arc<Self> {
383 std::sync::Arc::new(WalkPool {
384 limit: limit_bytes,
385 in_flight: std::sync::atomic::AtomicUsize::new(0),
386 peak: std::sync::atomic::AtomicUsize::new(0),
387 refusals: std::sync::atomic::AtomicU64::new(0),
388 })
389 }
390
391 /// Half of this machine's `MemTotal`, or [`DEFAULT_WALK_LIMIT_BYTES`] if
392 /// `/proc/meminfo` cannot be read.
393 ///
394 /// Half rather than all: the pool bounds *one* of the things a git server
395 /// holds. The pack writer's buffers, the receive path's index, the page
396 /// cache the mmap'd packs live in and whatever else shares the box are not
397 /// in it, and a memory ceiling set to the whole machine is a ceiling that
398 /// fires after the machine is already in trouble.
399 ///
400 /// The fallback is deliberately the *per-request* number, so a machine whose
401 /// size cannot be read gets a pool that admits exactly one large clone
402 /// rather than an accidentally unbounded one.
403 pub fn for_this_machine() -> std::sync::Arc<Self> {
404 WalkPool::new(Self::half_of_memtotal().unwrap_or(DEFAULT_WALK_LIMIT_BYTES))
405 }
406
407 fn half_of_memtotal() -> Option<usize> {
408 let text = std::fs::read_to_string("/proc/meminfo").ok()?;
409 let kb: usize = text
410 .lines()
411 .find_map(|l| l.strip_prefix("MemTotal:")?.trim().strip_suffix(" kB"))
412 .and_then(|v| v.trim().parse().ok())?;
413 kb.checked_mul(1024).map(|bytes| bytes / 2)
414 }
415
416 /// The ceiling this pool was built with.
417 pub fn limit_bytes(&self) -> usize {
418 self.limit
419 }
420
421 /// Bytes reserved by transfers in flight right now.
422 pub fn in_flight_bytes(&self) -> usize {
423 self.in_flight.load(std::sync::atomic::Ordering::Relaxed)
424 }
425
426 /// The most this pool has ever had reserved at once.
427 pub fn peak_bytes(&self) -> usize {
428 self.peak.load(std::sync::atomic::Ordering::Relaxed)
429 }
430
431 /// How many walks this pool has refused. Monotonic.
432 pub fn refusals(&self) -> u64 {
433 self.refusals.load(std::sync::atomic::Ordering::Relaxed)
434 }
435
436 /// Take `bytes` from the pool, or refuse.
437 ///
438 /// A compare-and-swap loop rather than `fetch_add` and a check: adding first
439 /// would let two transfers that each fit push the total past the ceiling and
440 /// then both back it out, which is a pool that is briefly over its own limit
441 /// exactly when it is busiest.
442 pub(crate) fn try_reserve(&self, bytes: usize) -> crate::Result<()> {
443 use std::sync::atomic::Ordering;
444 let mut current = self.in_flight.load(Ordering::Relaxed);
445 loop {
446 let Some(next) = current.checked_add(bytes).filter(|n| *n <= self.limit) else {
447 self.refusals.fetch_add(1, Ordering::Relaxed);
448 return Err(crate::Error::WalkPoolFull {
449 wanted: bytes,
450 in_flight: current,
451 limit: self.limit,
452 });
453 };
454 match self.in_flight.compare_exchange_weak(
455 current,
456 next,
457 Ordering::AcqRel,
458 Ordering::Relaxed,
459 ) {
460 Ok(_) => {
461 self.peak.fetch_max(next, Ordering::Relaxed);
462 return Ok(());
463 }
464 Err(seen) => current = seen,
465 }
466 }
467 }
468
469 /// Give `bytes` back.
470 ///
471 /// Saturating, and that is not defensiveness: a release that underflowed
472 /// would wrap to `usize::MAX` and permanently close a pool that is supposed
473 /// to be self-healing. `walk_pool_never_goes_negative` is the guard.
474 pub(crate) fn release(&self, bytes: usize) {
475 use std::sync::atomic::Ordering;
476 let _ = self
477 .in_flight
478 .fetch_update(Ordering::AcqRel, Ordering::Relaxed, |current| {
479 Some(current.saturating_sub(bytes))
480 });
481 }
482}
483
484/// A byte budget that is spent as a request is framed.
485///
486/// One of these per request rather than a per-line check, because the shape
487/// being guarded is a **million small lines**, not one large one — pkt-line
488/// framing already caps a single line at 65 520 bytes, so a per-line ceiling
489/// would have nothing left to catch.
490#[derive(Debug)]
491pub(crate) struct Budget {
492 remaining: Option<usize>,
493 limit: Option<usize>,
494}
495
496impl Budget {
497 pub(crate) fn new(limit: Option<usize>) -> Self {
498 Budget {
499 remaining: limit,
500 limit,
501 }
502 }
503
504 /// Charge `bytes` against the budget, or fail.
505 pub(crate) fn spend(&mut self, bytes: usize, what: &'static str) -> crate::Result<()> {
506 let (Some(remaining), Some(limit)) = (self.remaining.as_mut(), self.limit) else {
507 return Ok(());
508 };
509 match remaining.checked_sub(bytes) {
510 Some(left) => {
511 *remaining = left;
512 Ok(())
513 }
514 None => Err(crate::Error::RequestTooLarge { limit, what }),
515 }
516 }
517}