Honoring Cancellation
The Fundamentals context lesson introduced context.Context as a way to
carry deadlines, cancellation signals, and request-scoped values through a
call chain. Chapter 5's request-ID middleware used it to carry a value —
stashing an ID that handlers further down could read back out. This lesson
uses the same context.Context, but for the job it was originally built
for: knowing when to stop.
Every request has one
net/http gives every incoming request a context automatically, reachable
through r.Context(). The server cancels it the moment the underlying
connection goes away — the client closed the tab, a proxy timed out, the
TCP connection dropped — and, if a deadline was set upstream, it cancels
itself when that deadline passes. A handler never has to construct this
context; it's already there, already wired to the one thing that matters:
whether anyone is still going to see the response.
ctx.Err() is the signal
context.Context has an Err() method that returns nil while the
context is still live, and a non-nil error once it's done — either
context.Canceled (something cancelled it explicitly) or
context.DeadlineExceeded (a deadline passed). Checking Err() is the
"should I stop?" question, and it costs nothing to ask:
if err := r.Context().Err(); err != nil {
http.Error(w, "request cancelled", http.StatusServiceUnavailable)
return
}
A request whose context is already done isn't going to get a response read
by anyone. Doing the work anyway burns CPU, database connections, and
whatever else the handler touches, for no benefit — so the check belongs
before the work starts, and, for anything expensive, between its steps too.
A handler that loops over batches of work, or takes a slow round trip to an
external system, can check ctx.Err() again after each step and bail early
rather than pushing on toward a client that's no longer listening.
Passing ctx down
Cancellation only helps if it propagates. r.Context() is meant to be
passed into whatever the handler calls next — a database query, an HTTP
client request, a call into the store — so that work started on behalf of
the request stops when the request does. Our in-memory store from earlier
chapters completes instantly, so there's nothing for it to check; a real
database driver's query methods take a context.Context as their first
argument for exactly this reason, and stop the query when it's cancelled
instead of running to completion for no one.
Setting deadlines, not just reacting to them
Everything above reacts to cancellation that already happened. Go also
gives handlers tools for imposing a deadline in the first place:
context.WithTimeout derives a child context that cancels itself after a
duration, and http.TimeoutHandler wraps a handler so that any request
running past a limit gets cut off with a 503 automatically. Both are
worth knowing by name, even without building on them here — turning a real
timer into a graded exercise invites exactly the kind of flakiness a
deterministic test suite is built to avoid.
Part 4 picks up from here with structured logging, testing handlers and middleware in earnest, and graceful shutdown — the context signal this lesson introduced is also what a server listens for when it's told to stop accepting new work.
Practice
1. Honoring Cancellation
IntermediateEvery request carries a
contextthat gets cancelled when the client disconnects or a deadline passes. A handler that does real work should check it — no sense computing a response nobody's waiting for.Implement
process(w http.ResponseWriter, r *http.Request):- If
r.Context().Err()is non-nil (the context is already done), respond503 Service Unavailablewith a short message and return. - Otherwise do the "work" and respond
200with the bodydone.
The tests drive it once with a fresh request (proceeds) and once with an already-cancelled context (bails). Real handlers check
ctxbetween steps of long work and pass it down to the database.package main import ( "net/http" ) // process does some work on behalf of the request, but should check the // request's context first: if the client has already gone away (or a // deadline passed), there's no point doing the work. func process(w http.ResponseWriter, r *http.Request) { // TODO: check r.Context().Err(); if it's non-nil, respond // http.StatusServiceUnavailable and return. Otherwise, write "done" to w. } func main() {}Sign in to write and run your solution in the browser.
Sign in for hints and the solution.
- If