Skip to Content
Course content

138: Context Propagation in HTTP Handlers

Click on the "Edit" button in the top corner of the screen to edit your slide content.

If you've been building HTTP servers in Go, you've probably noticed that http.Request carries a Context. For a while, it feels like boilerplate—something you just pass along because the function signature requires it. But context propagation is actually your primary tool for preventing "ghost" work from eating your server's resources.

Imagine a user hits an endpoint that triggers a heavy database query. Halfway through, the user gets impatient and closes their browser tab. Without proper propagation, your server will keep churning away on that query until it finishes, even though there's no one left to receive the answer. That's a waste of CPU and memory.

Simulating a slow data fetch

Let's build a small example. We'll create a handler that fetches a user's profile. To make it realistic, I'll simulate a database call that takes a few seconds to complete. I want this function to be "context-aware," meaning it should stop working the moment the request is cancelled.

type User struct {
    ID   int
    Name string
}

func fetchUserFromDB(ctx context.Context, id int) (*User, error) {
    // Simulate a slow DB query with a timer
    select {
    case <-time.After(2 * time.Second):
        return &User{ID: id, Name: "Jane Doe"}, nil
    case <-ctx.Done():
        // This is the magic part: we stop if the context is cancelled
        return nil, ctx.Err()
    }
}

The mistake I almost made

Now, I need to call this from my HTTP handler. Here is where I often slipped up early in my career. I'll write the handler, but I'm going to intentionally do it wrong first. I'll use context.Background() inside the handler instead of the request's own context.

func userHandler(w http.ResponseWriter, r *http.Request) {
    id := 123
    
    // WRONG: I'm creating a fresh context instead of using the request's context
    user, err := fetchUserFromDB(context.Background(), id)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    
    fmt.Fprintf(w, "User: %s", user.Name)
}

At first glance, this works perfectly. If you hit the endpoint, you get the user back after two seconds. But there's a hidden bug. If you trigger this request and then immediately hit "Stop" in your browser, the server keeps running fetchUserFromDB for the full two seconds. I've effectively severed the link between the client's connection and the backend work.

Connecting the dots with r.Context()

To fix this, I need to propagate the context that Go's net/http package already manages for me. Every incoming request comes with a context that is automatically cancelled the moment the client disconnects or the connection times out.

I'll swap out context.Background() for r.Context(). This is the core of context propagation: passing the context from the outermost layer (the handler) down to the innermost layer (the database driver or API client).

func userHandler(w http.ResponseWriter, r *http.Request) {
    id := 123
    
    // RIGHT: we pass the request context down the chain
    user, err := fetchUserFromDB(r.Context(), id)
    if err != nil {
        // If the user cancelled, ctx.Err() will return context.Canceled
        log.Printf("Request cancelled or failed: %v", err)
        return 
    }
    
    fmt.Fprintf(w, "User: %s", user.Name)
}

Now, if you test this and kill the request mid-flight, you'll see the log.Printf trigger almost immediately. The select block in fetchUserFromDB catches the ctx.Done() signal, stops the timer, and returns. No wasted cycles.

A quick word on middleware

In a real production app, you wouldn't just rely on the client disconnecting. You'd likely use a middleware to wrap r.Context() with a timeout (using context.WithTimeout). This ensures that even if the client stays connected, a hung database query won't hold a request open for ten minutes. The propagation pattern remains exactly the same: you create a derived context in the middleware, and it flows down into your handlers and services.




📋 Practical Task

Build a Request-Aware Weather and News Aggregator

You are building a dashboard handler that fetches data from two simulated external services: a Weather API and a News API. Both are slow.

Your task:

  • Create two functions, getWeather(ctx context.Context) and getNews(ctx context.Context). Both should simulate a 3-second delay using time.After and a select block that listens for ctx.Done().
  • Create an HTTP handler that calls both functions.
  • Ensure that you propagate the request context (r.Context()) into both service calls.
  • Test your implementation: start the request and cancel it (or use a tool like curl with a max-time limit). Verify via logs that both services stop processing immediately when the request is cancelled.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.