- Published on
Context Cancellation With Cause
- Authors
- Name
- Moch Lutfi
- @kaptenupi
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
_18import (_18 "context"_18 "errors"_18 "fmt"_18)_18_18func main() {_18 customErr := errors.New("not found")_18 parent := context.Background()_18 ctx, cancel := context.WithCancelCause(parent)_18 cancel(customErr)_18 fmt.Println(ctx.Err()) // returns context.Canceled_18 fmt.Println(context.Cause(ctx)) // returns customErr_18}_18_18// output:_18// context canceled_18// not found
Implement with simple http server
_71package main_71_71import (_71 "context"_71 "errors"_71 "fmt"_71 "log"_71 "net/http"_71 "time"_71)_71_71var (_71 errGotEven = errors.New("ups we got even")_71)_71_71type RequestKey string_71_71const (_71 CancelKey RequestKey = "cancel"_71)_71_71func cancelContext(ctx context.Context, err error) {_71 cancel, ok := ctx.Value(CancelKey).(context.CancelCauseFunc)_71 if ok {_71 cancel(err)_71 }_71}_71_71func getOdd(w http.ResponseWriter, r *http.Request) {_71 now := time.Now()_71 if now.Minute()%2 == 1 {_71 fmt.Fprintf(w, "now %v \n", now)_71 return_71 }_71_71 cancelContext(r.Context(), errGotEven)_71}_71_71func logger(next http.Handler) http.Handler {_71 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {_71 start := time.Now()_71 defer func() {_71 ctx := r.Context()_71_71 msg := "OK"_71 if err := context.Cause(ctx); err != nil {_71 msg = err.Error()_71 }_71_71 log.Printf("%s - %s %s %s %s %v", r.RemoteAddr, r.Proto, r.Method, r.URL.RequestURI(), msg, time.Since(start))_71 }()_71 ctx := r.Context()_71 ctx, cancel := context.WithCancelCause(ctx)_71 ctx = context.WithValue(ctx, CancelKey, cancel)_71_71 r = r.WithContext(ctx)_71 next.ServeHTTP(w, r)_71 })_71}_71_71func main() {_71 srv := http.Server{_71 Addr: ":8888",_71 WriteTimeout: 5 * time.Second,_71 Handler: http.TimeoutHandler(logger(http.HandlerFunc(getOdd)), 3*time.Second, "Timeout!\n"),_71 }_71_71 if err := srv.ListenAndServe(); err != nil {_71 fmt.Printf("Server failed: %s\n", err)_71 }_71}
Try run with "go run main.go" then call curl localhost:8888
several times. Here's the logs server: