If you’ve been following along, we’ve already seen why HTTP/2 makes it easy to spin up work and why MadeYouReset can push servers into unbounded in‑flight requests. In this post, we’ll talk about the thing that sounds like it should save the day but usually doesn’t: end‑to‑end request cancellation.
What is End-to-end Cancellation
By “end‑to‑end cancellation” I mean: once a stream closes (client cancel or server RST_STREAM
), all work for that request actually stops across layers.
- Signal flows from the HTTP/2 module to the handler, its spawned tasks, and downstream calls.
- Drop queued work, undo safely, and free resources quickly.
It is not “Just don’t write the response” or “Return early while a DB query keeps running” - those are exactly the situations in which MadeYouReset shines.
End-to-end Cancellation is Hard
There are common problems and complications that need to be addressed when supporting end-to-end cancellation:
- You have to forward the “stop” signal everywhere. Miss one place and that bit keeps running.
- Many libraries don’t know how to stop mid‑way (DB calls, RPCs, file reads), so they just keep going.
- Stopping at the wrong moment can leave a mess (open handles, half‑done writes, weird state, leaked memory).
Usually, cancellation is treated as optional. Some frameworks expose a signal (like Node.js HTTP/2 aborted
). During disclosure I heard variations of: “we expose it; it’s not our job to propagate it”.
To be clear: that’s a product stance, which is totally fine - The signal exists, but ownership is pushed to app developers. The problem is that the feature is framed as an optional performance issue, not security. MadeYouReset shows cancellation isn’t a nice‑to‑have, it has real security implications. If frameworks present it as optional rather than security‑relevant, developers will follow suit. It’s on frameworks to fix the framing.
Adding Support For End-to-end Cancallation is Also Hard
Even if you want to do it, it’s a very heavy lift:
- Legacy assumptions: most servers were built around the HTTP/1.1 “do the whole thing and finish” model. There was no cancel button. Adding one means safe stop points, cleanup paths, and a way to put things back the way they were.
- Third‑party dependencies: your system is only as cancellable as its slowest dependency. If a DB driver can’t cancel an in‑flight query or a storage SDK can’t abort a read, work keeps running after you “cancel.”
- Interruption‑safety: code needs to be fault‑tolerant. Assume a request can stop at any moment and clean up memory, drop queued work, cancel I/O, roll back partial changes, propagate it to other components, etc…
- Users mindset shift: the users of the framework, which use it to develop their own services needs to change the way they write code to support the fault tolerance.
End-to-end Cancellation In The Wild - Hyper
In my research, the only stack I found that actually stops handler execution mid‑way when an HTTP/2 stream is reset is Rust’s Hyper.
That’s really awesome - but there still might be issues with that. We’ll see in demos that “significant impact” can still show up even when end‑to‑end cancellation exists.
If you prefer to jump straight to the demos, feel free. Otherwise, here’s a short overview of how cancellation works in Hyper and Rust. I’m not a Rust expert, so here are some solid references from people who really know Rust:
How Hyper Handles Cancellation
In Rust, a future represents an operation that will eventually resolve with a value or an error. It’s a way of handling asynchronous tasks without blocking other operations. When you call .await
on a future, you are asking the system to check whether the task is finished. If it isn’t, the system will keep checking it at intervals.
In Hyper, when an HTTP/2 stream is reset (via a RST_STREAM frame), the future associated with that stream is dropped. This cancellation mechanism ensures that all ongoing operations tied to that stream are immediately stopped and cleaned up.
How does Hyper stop ongoing work?
-
Polling: In Rust, futures are polled to see if the task they represent has finished. When a stream is reset, Hyper stops polling the associated future. This immediately interrupts the task - no more work is done on that stream, such as reading data from the client or writing a response.
-
Propagating the Cancellation: When the future is dropped (because the stream is reset), Hyper propagates the cancellation to other tasks and other futures that the future depended on. In simpler terms, if the future is waiting on other operations (like file I/O, network calls, or background tasks), these dependent tasks are also canceled. This is crucial because in an asynchronous system, a task might be waiting for another task to finish. If the task it’s waiting on is canceled (like the HTTP/2 stream being reset), then it must also be stopped. Hyper leverages Rust’s async model to ensure that the cancellation propagates throughout the entire chain of dependent operations, ensuring that no unnecessary work continues.
Significant Impact Even With Built In End-to-end Cancellation
First, to confirm that Hyper (and our server) really supports mid‑execution request cancellation, run the PoC on a simple Hyper server that handles a request like this:
- Append “1” to
/tmp/hello1
- Sleep for 200 milliseconds
- Append “2” to
/tmp/hello2
If requests are being canceled mid‑execution, you should see significantly more 1s than 2s.
Now that we’ve confirmed requests are actually canceled, how do we find the significant impact we’re after?
We will see that end‑to‑end cancellation is still hard, because of the two facts we mentioned earlier:
- The “stop” signal needs to be forwarded everywhere.
- Many libraries don’t know how to stop mid‑way, so they just keep going.
Significant Impact Using Sqlx
Now we understand why the demo I showed in the previous post is so interesting. We learned that SQL has sequential behaviors that can bottleneck concurrency.
No we also understand that in sqlx’s case, it also queues work that doesn’t cancel when the originating future in Rust is canceled - that’s double trouble!
Here is a video of running the PoC on a hyper server that queries a MySQL db using sqlx:
An important side note: Hyper’s HTTP/2 library (h2) was patched in june, and now includes a mitigation for MadeYouReset by not allowing many server initiated RST_STREAMs per connection. This protects h2 based servers that have upgraded. Deployments pinned to older h2 versions may still be exposed. No CVE was issued for h2 (appropriately - it’s a protocol library, not a standalone server), so you might not have seen the usual advisories.
I interacted with sean - one of the maintainers of h2 and hyper - which was one of the firsts to address the issue. We also had some very fruitful discussions on the matter, I highly recommend checking his blog.
Summary
Even with built‑in request cancellation, making it truly end‑to‑end is hard. Signals often don’t propagate everywhere, and many libraries won’t fully abort mid‑flight. Cancellation is valuable, but it isn’t a complete defense against MadeYouReset. My recommendation is to mitigate MadeYouReset at the protocol level, as I described in the Technical Details post.