Use context in your HTTP handlers

I am a software engineer based in Cyprus with over 20 years of experience in the industry. My background in Computer Science has led me to work with PHP, Python, and more recently, with a focus on Golang.
Originally from Greece, my career has taken me across Europe, and I now call Cyprus home. I've attended numerous conferences, continually expanding my knowledge and network.
Recently, I started blogging to share my insights and experiences with the tech community. I'm passionate about engaging with fellow developers and contributing to the field through my writing and future projects.
Thank you for visiting my blog.
Let's consider a simple webserver that runs a task. The task can be anything like a time consuming computation, a database query.
You need to be able to do two things:
when the client drops the connection while the task is running terminate the task so resources are released.
when the task takes too much time return a proper HTTP status code to the client
Let's see a practical example
package main
import (
"context"
"errors"
"fmt"
"math/rand"
"net/http"
"time"
)
func main() {
mux := http.NewServeMux()
// Register the handler with the middleware, setting the timeout to 5 seconds
mux.HandleFunc("/", longRunningTaskHandler)
const timeout = time.Second * 5
wrappedMux := contextMiddleware(timeout)(mux)
// Start the server
http.ListenAndServe(":8080", wrappedMux)
}
// contextMiddleware is a middleware that sets a timeout for the request context.
// If the request takes longer than the timeout, the middleware will cancel the context.
func contextMiddleware(timeout time.Duration) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Set a timeout for the request context
ctx, cancel := context.WithTimeout(r.Context(), timeout)
defer cancel()
// Create a new request with the updated context
r = r.WithContext(ctx)
// Call the next handler with the new context
next.ServeHTTP(w, r)
})
}
}
func longRunningTaskHandler(w http.ResponseWriter, r *http.Request) {
err := longRunningTask(r.Context())
if err != nil {
// if the context expired, return a 504 Gateway Timeout
if errors.Is(err, context.DeadlineExceeded) {
w.WriteHeader(http.StatusGatewayTimeout)
return
}
// if the task failed for some other reason, return a 500 Internal Server Error
w.WriteHeader(http.StatusInternalServerError)
return
}
// if the task completed successfully, return a 200 OK
w.WriteHeader(http.StatusOK)
}
// longRunningTask is a dummy function that simulates a long running task.
// the task will take 10 seconds to complete.
// If the context is cancelled before the task completes, it will return the error.
func longRunningTask(ctx context.Context) error {
var dur time.Duration
// for ~50% of the time, the task will take 10 seconds to complete
if rand.Float64() > 0.5 {
dur = 10 * time.Second
} else {
dur = 1 * time.Second // and for the other ~50% of the time, the task will take 1 second to complete
}
// simulate the task by sleeping for the duration
select {
case <-time.After(dur):
return nil
case <-ctx.Done():
fmt.Println("task cancelled")
return ctx.Err()
}
}
Run the webserver:
go run main.go
and in another terminal:
curl -i http://localhost:8080
Curl command will return at most in 5 seconds, even though the task can take up to 10 seconds.
See some runs:
➜ ~ curl -i http://localhost:8080/
HTTP/1.1 200 OK
Date: Wed, 12 Jun 2024 05:52:18 GMT
Content-Length: 0
➜ ~ curl -i http://localhost:8080/
HTTP/1.1 504 Gateway Timeout
Date: Wed, 12 Jun 2024 05:52:24 GMT
Content-Length: 0
➜ ~ curl -i http://localhost:8080/
HTTP/1.1 200 OK
Date: Wed, 12 Jun 2024 05:52:26 GMT
Content-Length: 0
As you notice in the second request the server returned 504.
Please run this in your computer and hit CTL-C when the request is running.
Notice the output of the webserver and you will see a message like:
task cancelled
See the related github repo



