douhu2370
2018-07-25 14:20
浏览 72
已采纳

如何在保留原始请求上下文值的同时将上下文传递给r.WithContext?

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?

  • 写回答
  • 关注问题
  • 收藏
  • 邀请回答

3条回答 默认 最新

  • duancaishun4812 2018-07-26 13:03
    已采纳

    It appears as though there is no out-of-the-box solution for this, but github.com/SentimensRG/ctx provides a mergectx subpackage specifically for this purpose.

    The solution is to use mergectx.Merge.

    已采纳该答案
    打赏 评论
  • doucang6914 2018-07-25 16:35

    You should not replace the context on incoming request with an unrelated context. For starters:

    Package context defines the Context type, which carries deadlines, cancelation signals, and other request-scoped values across API boundaries and between processes.

    sigctx.New() is called before any request happens and is therefore by definition not request scoped. Pretty much all code expects the request context to be canceled when a) the request finishes, or b) the client aborts the request (usually because it is no longer interested in the response). You are breaking that assumption by replacing the context. You are also removing any values that other middlewares may have added to the context earlier.

    It seems like you wish to abort requests on SIGINT or SIGTERM. You should add that cancelation condition to the request context instead of replacing it completely. Perhaps like so:

    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()
                            ctx, cancel := context.WithCancel(rCtx)
    
                            go func() {
                                    select {
                                    case <-c.Done(): // SIGINT/SIGTERM
                                    case <-rCtx.Done(): // Request finished or client aborted
                                    }
                                    cancel()
                            }()
    
                            h.ServeHTTP(w, r.WithContext(ctx))
                    })
            }
    }
    

    Update:

    To let users configure the context, accept a function that derives a new context from the request context (although users might as well supply a middleware that does this directly):

    func WithContext(new func(context.Context) context.Context) func(http.Handler) http.Handler {
        return func(h http.Handler) http.Handler {
            return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
                r = r.WithContext(new(r.Context()))
    
                h.ServeHTTP(w, r)
            })  
        }   
    }
    
    打赏 评论
  • dongwei3172 2019-02-26 11:02

    I faced the same problem and was able to resolve this by creating the new context using chi.NewRouteContext.

    The request is being made using httptest. You can update the request its context using r.WithContext.

    Example

    w := httptest.NewRecorder()
    r := httptest.NewRequest("GET", "/", nil)
    
    rctx := chi.NewRouteContext()
    rctx.URLParams.Add("key", "value")
    
    r = r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, rctx))
    
    handler := func(w http.ResponseWriter, r *http.Request) {
        key := chi.URLParam(r, "key") // "value"
    }
    
    handler(w, r)
    

    See the folling Gist from aapolkovsky: https://gist.github.com/aapolkovsky/1375348cab941e36c62da24a32fbebe7

    打赏 评论

相关推荐 更多相似问题