A handler runs to completion regardless of client behaviour. If the client TCP-disconnects mid-handler, the handler has no signal until the next _send returns EPIPE. By then the handler has already done its expensive work (DB query, downstream HTTP fan-out, JSON serialisation).

the reactor allocates one CancelCell per connection (heap-allocated Int). Cancel is a thin handle the reactor passes to a CancelHandler.serve(req, cancel); the handler polls cancel.cancelled() between expensive steps and short-circuits when True.

Cancellation is cooperative, not preemptive — Mojo can't preempt synchronous code, and synchronous preemption would defeat the reactor's per-thread invariant anyway. The contract is: the handler checks the flag at boundaries it owns; the reactor sets it at one of:

  • CancelReason.PEER_CLOSED — recv == 0 (peer FIN) observed before the response was queued.
  • CancelReason.TIMEOUT — a per-request, per-handler, or per-body-read deadline fired.
  • CancelReason.SHUTDOWN — HttpServer.drain(timeout_ms) was called.

The default-initialised Cancel returned by Cancel.never() is a sentinel that never fires; tests and synthetic CancelHandler calls that don't have a real reactor cell behind them use it.

Implementation

A CancelCell heap-allocates a single Int and owns its lifetime. Cancel carries the cell's address as an Int and rebuilds a fresh UnsafePointer[Int, MutExternalOrigin] per access — the same pattern the multicore Scheduler uses for the stopping flag, and the only one that survives Mojo's current (current Mojo nightly) origin / aliasing model when passing the cancel handle across function-call boundaries:

  • Storing a typed UnsafePointer[Int, MutExternalOrigin] as a struct field in Cancel produces stale reads after the struct is passed through a function (verified empirically: reads at a numerically-correct address returned the pointer struct size, 8, instead of the live byte).
  • An inline UInt8 cell on the owning struct (instead of heap) does not have a stable address across the synchronous call chain the reactor builds, again per direct testing.

The combination here — heap allocation for cell stability + Int address in Cancel for transport stability — is the only one that passes the test suite and the cell.flip(reason); h.serve(req^, cell.handle()) round-trip.

Why Int instead of UInt8 for the cell value: a byte-sized heap cell hit the same aliasing failure as the typed-pointer field, suggesting Mojo's aliasing model treats sub-word-aligned loads through MutExternalOrigin differently than full-word loads. A machine word avoids the path the bug appeared to depend on. The cell is one cache line per connection — acceptable cost for the cancel infrastructure.

Structs

struct CancelReason Reason the cancel cell was flipped.
struct CancelCell Heap-allocated per-connection cancel cell.
struct Cancel A handle to a per-request cancel cell owned by the reactor.
Detail Documentation

Structs

struct CancelReason §

struct CancelReason

Reason the cancel cell was flipped.

Stored in the cell as an Int; NONE (zero) means "still live." The handler can branch on the reason via cancel.reason() == CancelReason.X when it wants to log or take a different short-circuit path depending on why it was cancelled (for example, returning a 503 on shutdown vs. a 408 on timeout).

struct CancelCell §

struct CancelCell

Heap-allocated per-connection cancel cell.

Owned by the reactor for the lifetime of a connection (the reactor keeps one of these on each ConnHandle). Cancel handles point at this cell. Reset to CancelReason.NONE between pipelined requests so a peer-FIN on one request doesn't bleed into the next.

Methods

fn __init__ Allocate a fresh cell initialised to ``NONE``.
fn __del__
fn flip Set the cell's reason.
fn reset Reset the cell to ``NONE``.
fn handle Hand out a ``Cancel`` value bound to this cell.

fn __init__ static §

__init__(out self)

Allocate a fresh cell initialised to ``NONE``.

Args
self out Self
Returns
Self
Raises

May raise an exception.

fn __del__ §

__del__(deinit self)
Args
self deinit Self

fn flip §

flip(mut self, reason: Int)

Set the cell's reason.

Args
self mut Self
reason Int

fn reset §

reset(mut self)

Reset the cell to ``NONE``.

Args
self mut Self

fn handle §

handle(mut self) -> Cancel

Hand out a ``Cancel`` value bound to this cell.

Args
self mut Self
Returns
Cancel

struct Cancel §

struct Cancel

A handle to a per-request cancel cell owned by the reactor.

Passed to CancelHandler.serve(req, cancel) by the reactor. Value-copy semantics: copies share the underlying cell.

A handler that ignores cancellation is correct (the request shape is unchanged), but the reactor still flips the cell and the body the handler returns will be the result of work the reactor knew the client had hung up on.

Example:

from flare.http import CancelHandler, Cancel, Request, Response, ok

@fieldwise_init
struct SlowHandler(CancelHandler, Copyable, Movable):
    fn serve(self, req: Request, cancel: Cancel) raises -> Response:
        for i in range(100):
            if cancel.cancelled():
                return ok("partial: " + String(i))
            # ... one expensive step ...
        return ok("done")

Methods

fn __init__
fn never Return a sentinel ``Cancel`` whose ``cancelled()`` is always ``False``.
fn cancelled Return True once the cell is non-zero.
fn reason Return the reason code currently in the cell.

fn __init__ static §

__init__(out self, addr: Int)
Args
addr Int
self out Self
Returns
Self

fn never static §

never() -> Self

Return a sentinel ``Cancel`` whose ``cancelled()`` is always ``False``.

Returns
Self

fn cancelled §

cancelled(self) -> Bool

Return True once the cell is non-zero.

Args
self Self
Returns
Bool

fn reason §

reason(self) -> Int

Return the reason code currently in the cell.

Args
self Self
Returns
Int