Context Cancellation With Cause
February 10, 2023
Go 1.20 already released, and 1 of many interesting features is WithCancelCause
function that simplify passing error when cancelling context. Previously when working with http server request, I passing error from the inner most handler to middleware that handle logger with creating custom handler with error for example func(w http.ResponseWriter, r *http.Request) error
. The main problem is I must rewrite all middleware that follow the signature so I can called compose it like this middlewareA(middlewareB(handler))
.
Fortunately using WithCancelCause
we just simply call the cancellation with the reason (non-nill error) like cancel(customErr)
and got the error cause.
Here the simple demo https://go.dev/play/p/K3_RcvniXxZ (opens in a new tab)
import ( "context" "errors" "fmt")func main() { customErr := errors.New("not found") parent := context.Background() ctx, cancel := context.WithCancelCause(parent) cancel(customErr) fmt.Println(ctx.Err()) // returns context.Canceled fmt.Println(context.Cause(ctx)) // returns customErr}// output:// context canceled// not found
Implement with simple http server
package mainimport ( "context" "errors" "fmt" "log" "net/http" "time")var ( errGotEven = errors.New("ups we got even"))type RequestKey stringconst ( CancelKey RequestKey = "cancel")func cancelContext(ctx context.Context, err error) { cancel, ok := ctx.Value(CancelKey).(context.CancelCauseFunc) if ok { cancel(err) }}func getOdd(w http.ResponseWriter, r *http.Request) { now := time.Now() if now.Minute()%2 == 1 { fmt.Fprintf(w, "now %v \n", now) return } cancelContext(r.Context(), errGotEven)}func logger(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() defer func() { ctx := r.Context() msg := "OK" if err := context.Cause(ctx); err != nil { msg = err.Error() } log.Printf("%s - %s %s %s %s %v", r.RemoteAddr, r.Proto, r.Method, r.URL.RequestURI(), msg, time.Since(start)) }() ctx := r.Context() ctx, cancel := context.WithCancelCause(ctx) ctx = context.WithValue(ctx, CancelKey, cancel) r = r.WithContext(ctx) next.ServeHTTP(w, r) })}func main() { srv := http.Server{ Addr: ":8888", WriteTimeout: 5 * time.Second, Handler: http.TimeoutHandler(logger(http.HandlerFunc(getOdd)), 3*time.Second, "Timeout!\n"), } if err := srv.ListenAndServe(); err != nil { fmt.Printf("Server failed: %s\n", err) }}
Try run with "go run main.go" then call curl localhost:8888
several times. Here's the logs server: