Problem statement
I want to tie the lifetime of an HTTP request to a context that was created outside the scope of the web application. Thus, I wrote the following middleware (using github.com/go-chi/chi
):
func BindContext(c context.Context) func(http.Handler) http.Handler {
return func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
h.ServeHTTP(w, r.WithContext(c))
})
}
}
The middleware is used in the following minimal test case:
package main
import (
"context"
"net/http"
"github.com/SentimensRG/ctx"
"github.com/SentimensRG/ctx/sigctx"
"github.com/go-chi/chi"
)
func greet(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}
func BindContext(c context.Context) func(http.Handler) http.Handler {
return func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
h.ServeHTTP(w, r.WithContext(c))
})
}
}
func main() {
r := chi.NewMux()
r.Use(BindContext(ctx.AsContext(sigctx.New())))
r.Get("/", greet)
http.ListenAndServe(":8080", r)
}
The handler panics with the following error:
2018/07/25 14:58:57 http: panic serving [::1]:56527: interface conversion: interface {} is nil, not *chi.Context
goroutine 35 [running]:
net/http.(*conn).serve.func1(0xc42014a0a0)
/usr/local/go/src/net/http/server.go:1726 +0xd0
panic(0x12749c0, 0xc42014c200)
/usr/local/go/src/runtime/panic.go:502 +0x229
github.com/go-chi/chi.(*Mux).routeHTTP(0xc4201180c0, 0x12fcf00, 0xc420166000, 0xc420160200)
/Users/lthibault/go/src/github.com/go-chi/chi/mux.go:400 +0x2f3
github.com/go-chi/chi.(*Mux).(github.com/go-chi/chi.routeHTTP)-fm(0x12fcf00, 0xc420166000, 0xc420160200)
/Users/lthibault/go/src/github.com/go-chi/chi/mux.go:368 +0x48
net/http.HandlerFunc.ServeHTTP(0xc420142010, 0x12fcf00, 0xc420166000, 0xc420160200)
/usr/local/go/src/net/http/server.go:1947 +0x44
main.fail.func1.1(0x12fcf00, 0xc420166000, 0xc420160100)
/Users/lthibault/go/src/github.com/lthibault/mesh/cmd/scratch/main.go:22 +0x77
net/http.HandlerFunc.ServeHTTP(0xc420148000, 0x12fcf00, 0xc420166000, 0xc420160100)
/usr/local/go/src/net/http/server.go:1947 +0x44
github.com/go-chi/chi.(*Mux).ServeHTTP(0xc4201180c0, 0x12fcf00, 0xc420166000, 0xc420160000)
/Users/lthibault/go/src/github.com/go-chi/chi/mux.go:81 +0x221
net/http.serverHandler.ServeHTTP(0xc420150000, 0x12fcf00, 0xc420166000, 0xc420160000)
/usr/local/go/src/net/http/server.go:2694 +0xbc
net/http.(*conn).serve(0xc42014a0a0, 0x12fd1c0, 0xc42014c080)
/usr/local/go/src/net/http/server.go:1830 +0x651
created by net/http.(*Server).Serve
/usr/local/go/src/net/http/server.go:2795 +0x27b
Inelegant solution
The problem appears to come from Mux.routeHTTP, where an attempt is made to recover a *chi.Context
from r.Context()
. It would appear that r.WithContext
does not transfer values stored in the request context to the new context.
The obvious (albeit ugly) fix is:
func BindContext(c context.Context) func(http.Handler) http.Handler {
return func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
rctx := r.Context().Value(chi.RouteCtxKey).(*chi.Context)
c = context.WithValue(c, chi.RouteCtxKey, rctx)
h.ServeHTTP(w, r.WithContext(c))
})
}
}
This works, but leaves me feeling uneasy. Do I really need to manually transfer each relevant value from r.Context()
into the context being passed to r.WithContext()
?
There are several failure cases, here:
- What happens when there are many different values to be transferred?
- What happens when the context keys aren't exported (as is recommended in Go)?
- What happens if the original context terminates before the one I passed in?
(In few words: nothing good!)
My question is as follows
Is there a standard "merge" a context passed to r.WithContext
with the exiting context in r.Context
?