# 🛣️ net/http: Handlers, Middleware, JSON, and Graceful Shutdown

## Learning Objectives

By the end of this lesson you can:

- Implement typed JSON handlers with the standard library only, and know where the write order matters.
- Compose middleware as `func(http.Handler) http.Handler` and reason about chain order.
- Cap request bodies with `http.MaxBytesReader` and map overflow to a clean `413`.
- Apply server-level timeouts and per-request deadlines so a slow client cannot hold a goroutine forever.
- Shut down with `srv.Shutdown` so in-flight requests finish instead of being cut mid-write.

## Quick Review

In lesson 2.1 you built a skeleton where `cmd/api` owns construction and shutdown, while `internal/` holds the implementation. Dependencies arrive as explicit arguments, not package-level globals. This lesson fills in the thing that skeleton serves: `http.Handler` values, wired the same explicit way (dependencies go into a struct, the struct owns methods that satisfy `http.HandlerFunc`).

## Theory

### Handlers are just an interface

```go
type Handler interface {
	ServeHTTP(ResponseWriter, *Request)
}
```

Everything else is sugar. `http.HandlerFunc` adapts a plain function to that interface. A method on a struct holding your dependencies is the idiomatic way to inject services:

```go
type API struct {
	store OrderStore
	log   *slog.Logger
}

func (a *API) handleCreateOrder(w http.ResponseWriter, r *http.Request) { /* uses a.store */ }
```

If you are on Go 1.22 or newer, `ServeMux` understands method and wildcard patterns: `mux.Handle("POST /v1/orders", ...)` and `mux.HandleFunc("GET /v1/orders/{id}", ...)` with `r.PathValue("id")`. That removes most hand-rolled routing and path-splitting.

### Middleware is a handler decorator

The canonical shape is `func(http.Handler) http.Handler`. It receives the next handler, returns a new one, and can act before and after the call.

```mermaid
flowchart LR
    Req["📥<br/>Request"] --> Rec["🛡️<br/>recover"]
    Rec --> Log["📝<br/>logging"]
    Log --> Mux["🧭<br/>ServeMux"]
    Mux --> H["⚙️<br/>handler"]
    H --> Log
    Log --> Rec
    Rec --> Res["📤<br/>Response"]
```

Read the chain outside-in: `withRecover(withLogging(mux))` means logging runs *inside* recover, so a panic that logging never sees still produces a 500. Ordering is a design decision, not a detail. Recovery outermost, request ID early, auth after logging (you want the 401 in the log), and anything that needs a deadline close to the handler.

```go
func withLogging(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		start := time.Now()
		rec := &statusRecorder{ResponseWriter: w, status: http.StatusOK}
		next.ServeHTTP(rec, r)
		log.Printf("method=%s path=%s status=%d dur=%s",
			r.Method, r.URL.Path, rec.status, time.Since(start))
	})
}
```

`statusRecorder` exists because `http.ResponseWriter` does not expose the status you wrote. You have to capture it in `WriteHeader`.

### JSON: three rules that cover most bugs

1. Set `Content-Type`, then `WriteHeader(status)`, then encode. Once the status line is flushed, later `Header().Set` calls are silently ignored.
2. Encode, never marshal-then-write, for responses. On the decode side, use `json.NewDecoder(r.Body)` and call `dec.DisallowUnknownFields()` so typos in client payloads fail loudly instead of being dropped.
3. Pick one error envelope and use it everywhere: `{"error": {"code": "invalid_payload", "message": "..."}}`. Clients should never have to parse two shapes.

### Cap the body, cap the time

`http.MaxBytesReader` wraps the body with a hard limit. When the limit is exceeded the read returns a `*http.MaxBytesError`, and you map that to `413`. Without it, a client can stream gigabytes into your decoder.

On the server side, set all four timeouts. `ReadHeaderTimeout` is the one people skip and the one that stops slowloris clients from pinning connections open:

```go
srv := &http.Server{
	Addr:              ":8080",
	Handler:           withRecover(withLogging(mux)),
	ReadHeaderTimeout: 2 * time.Second,
	ReadTimeout:       5 * time.Second,
	WriteTimeout:      10 * time.Second,
	IdleTimeout:       60 * time.Second,
}
```

Read this carefully: these are *connection* deadlines. When `WriteTimeout` fires, writes fail, but the handler goroutine keeps running. Only a context deadline actually stops work, which is why `r.Context()` should be passed into every database call and downstream request.

> [!WARNING]
> `http.TimeoutHandler` is convenient but it buffers the entire response before sending, so it breaks streaming and long polls. Prefer a context deadline derived from `r.Context()` for those, and keep `TimeoutHandler` for small JSON endpoints.

### Graceful shutdown

```steps
1. Create a context canceled by SIGINT/SIGTERM with `signal.NotifyContext`.
2. Start `ListenAndServe` in a goroutine and send only real errors to a buffered channel.
3. Block on `select` over that error channel and `ctx.Done()`.
4. On signal, call `srv.Shutdown` with a *fresh* context that has a timeout (10s is a good default).
5. Treat `http.ErrServerClosed` from `ListenAndServe` as success, not failure.
```

```go
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()

errCh := make(chan error, 1)
go func() {
	if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
		errCh <- err
	}
}()

select {
case err := <-errCh:
	log.Fatalf("serve: %v", err)
case <-ctx.Done():
	stop() // a second Ctrl+C now kills the process immediately
}

shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
	log.Printf("shutdown: %v", err)
}
```

`Shutdown` stops accepting new connections, closes idle ones, and waits for active requests. It does **not** wait for goroutines your handler spawned and forgot. That is the same `errgroup` discipline from module 1: track them, wait for them, then exit.

## Worked Examples

**1. Decode with a cap and a typed error**

```go
func decodeJSON(w http.ResponseWriter, r *http.Request, dst any) error {
	r.Body = http.MaxBytesReader(w, r.Body, 1<<20) // 1 MiB
	dec := json.NewDecoder(r.Body)
	dec.DisallowUnknownFields()
	return dec.Decode(dst)
}
```

In the handler, `errors.As(err, new(*http.MaxBytesError))` selects the `413` branch, and everything else becomes `400` with your error envelope.

**2. Write helper used by every endpoint**

```go
func writeJSON(w http.ResponseWriter, status int, v any) {
	w.Header().Set("Content-Type", "application/json; charset=utf-8")
	w.WriteHeader(status)
	_ = json.NewEncoder(w).Encode(v)
}
```

Call it exactly once per request. A second call panics with "superfluous WriteHeader" and corrupts the body.

## Discussion & Reflection

- Your logging middleware sits inside recovery. What information do you lose on a panic, and where would you move it?
- `Shutdown` returns `context.DeadlineExceeded` on a slow request. What should the process do next, and what did the client see?
- Which of your current handlers pass `r.Context()` into every blocking call, and which ones quietly use `context.Background()`?

## Expert Corner

- **Never** wire the signal context into `Server.BaseContext`. Canceling it on SIGTERM kills in-flight request contexts instantly, which is the opposite of graceful.
- Set headers before `WriteHeader`. This is the single most common JSON bug in code review.
- `TimeoutHandler` and `http.Flusher`/`Hijacker` do not mix. Streaming and WebSocket endpoints need the raw writer.
- Read the body once. If you need it twice (signature check plus decode), buffer with `io.ReadAll` under the same `MaxBytesReader`.
- Log status codes through a recorder that also implements `Flush` and `Hijack` when you need streaming. A bare wrapper silently disables both.
- After `Shutdown` returns, close your database pool. Order: HTTP first, dependencies second.

Quick check on the key terms:

The handler interface requires a single method, {{c1::ServeHTTP}}, and `http.HandlerFunc` adapts a plain function to it. Middleware has the signature `func(http.Handler) {{c2::http.Handler}}`, which is why chains compose in either direction. When a body exceeds `http.MaxBytesReader`, decode returns a {{c3::*http.MaxBytesError}} that should become a `413`. `ListenAndServe` reports {{c4::http.ErrServerClosed}} after a graceful shutdown, so treat it as success. Go 1.22 wildcards are read back with `r.{{c5::PathValue}}("id")`. And the timeout that stops slowloris attacks is {{c6::ReadHeaderTimeout}}.

## Summary & Next Steps

Handlers are an interface, middleware is function composition, and both are invisible in behavior until you get the ordering and the write sequence right. Cap the body, set all four server timeouts, pass `r.Context()` everywhere, and shut down with a fresh deadline so in-flight requests finish. `==Graceful shutdown is a context problem, not a signal problem.==`

Next up: you take this skeleton and put Postgres behind it, using `database/sql` and integration tests against real queries. Everything you wired here, the handler struct, the context propagation, the error envelope, becomes the seam those tests press on.

```flashcard
[
  { "front": "What method must a type implement to be an http.Handler?", "back": "ServeHTTP(http.ResponseWriter, *http.Request)." },
  { "front": "What does http.HandlerFunc do?", "back": "Adapts an ordinary function with the ServeHTTP signature into an http.Handler." },
  { "front": "Canonical middleware signature?", "back": "func(http.Handler) http.Handler." },
  { "front": "In withRecover(withLogging(mux)), which runs first on a request?", "back": "withRecover, the outermost wrapper, so logging runs inside it." },
  { "front": "Why must Content-Type be set before WriteHeader?", "back": "Headers are flushed with the status line; later Set calls are ignored." },
  { "front": "What error does http.MaxBytesReader return on overflow, and which status fits?", "back": "*http.MaxBytesError, mapped to 413 Request Entity Too Large." },
  { "front": "What does json.Decoder.DisallowUnknownFields change?", "back": "Unknown JSON fields become a decode error instead of being ignored." },
  { "front": "Which sentinel error does ListenAndServe return after Shutdown?", "back": "http.ErrServerClosed, which should be treated as success." },
  { "front": "Shutdown vs Close on http.Server?", "back": "Shutdown waits for in-flight requests; Close drops connections immediately." },
  { "front": "Which four http.Server timeouts should you set?", "back": "ReadHeaderTimeout, ReadTimeout, WriteTimeout, IdleTimeout." },
  { "front": "Do server connection timeouts cancel the handler goroutine?", "back": "No, writes fail but the handler keeps running; only a context deadline stops work." },
  { "front": "Why avoid http.TimeoutHandler for streaming endpoints?", "back": "It buffers the whole response before writing it out." },
  { "front": "What does srv.Shutdown return when in-flight work outlasts its context?", "back": "The context error, typically context.DeadlineExceeded." }
]
```
