Published on

Context Cancellation With Cause

Authors

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


_18
import (
_18
"context"
_18
"errors"
_18
"fmt"
_18
)
_18
_18
func 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


_71
package main
_71
_71
import (
_71
"context"
_71
"errors"
_71
"fmt"
_71
"log"
_71
"net/http"
_71
"time"
_71
)
_71
_71
var (
_71
errGotEven = errors.New("ups we got even")
_71
)
_71
_71
type RequestKey string
_71
_71
const (
_71
CancelKey RequestKey = "cancel"
_71
)
_71
_71
func cancelContext(ctx context.Context, err error) {
_71
cancel, ok := ctx.Value(CancelKey).(context.CancelCauseFunc)
_71
if ok {
_71
cancel(err)
_71
}
_71
}
_71
_71
func 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
_71
func 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
_71
func 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:

main
success
fail

_7
❯ go run main.go
_7
2023/02/10 01:39:03 127.0.0.1:56144 - HTTP/1.1 GET / OK 64.823µs
_7
2023/02/10 01:39:08 127.0.0.1:51530 - HTTP/1.1 GET / OK 23.485µs
_7
2023/02/10 01:39:11 127.0.0.1:51536 - HTTP/1.1 GET / OK 32.462µs
_7
2023/02/10 01:39:14 127.0.0.1:51540 - HTTP/1.1 GET / OK 24.417µs
_7
2023/02/10 01:39:56 127.0.0.1:50684 - HTTP/1.1 GET / OK 26.29µs
_7
2023/02/10 01:40:07 127.0.0.1:34338 - HTTP/1.1 GET / ups we got even 3.156µs