restgogo-chi

Request created with http.NewRequestWithContext() looses context when passed to middleware


In program bellow I have two routers. One is working at localhost:3000 and acts like a public access point. It also may send requests with data to another local address which is localhost:8000 where data is being processed. Second router is working at localhost:8000 and handles processing requests for the first router.

Problem

The first router sends a request with context to the second using http.NewRequestWithContext() function. The value is being added to the context and the context is added to request. When request arrives to the second router it does not have value that was added previously.

Some things like error handling are not being written to not post a wall of code here.

package main
import (
    "bytes"
    "context"
    "net/http"
    "github.com/go-chi/chi"
    "github.com/go-chi/chi/middleware"
)

func main() {
    go func() {
        err := http.ListenAndServe(
            "localhost:3000",
            GetDataAndSolve(),
        )
        if err != nil {
            panic(err)
        }
    }()

    go func() {
        err := http.ListenAndServe( // in GetDataAndSolve() we send requests
            "localhost:8000", // with data for processing
            InternalService(),
        )
        if err != nil {
            panic(err)
        }
    }()

    // interrupt := make(chan os.Signal, 1) 
    // signal.Notify(interrupt, syscall.SIGTERM, syscall.SIGINT)
    // <-interrupt // just a cool way to close the program, uncomment if you need it
}
func GetDataAndSolve() http.Handler {
    r := chi.NewRouter()
    r.Use(middleware.Logger)

    r.Get("/tasks/str", func(rw http.ResponseWriter, r *http.Request) {
        // receiving data for processing...
        taskCtx := context.WithValue(r.Context(), "str", "strVar") // the value is being
        postReq, err := http.NewRequestWithContext(                // stored to context
            taskCtx, // context is being given to request
            "POST",
            "http://localhost:8000/tasks/solution",
            bytes.NewBuffer([]byte("something")),
        )
        postReq.Header.Set("Content-Type", "application/json") // specifying for endpoint
        if err != nil {                                        // what we are sending
            return
        }

        resp, err := http.DefaultClient.Do(postReq) // running actual request
        // pls, proceed to Solver()

        // do stuff to resp
        // also despite arriving to middleware without right context
        // here resp contains a request with correct context
    })

    return r
}
func Solver(next http.Handler) http.Handler { // here we end up after sending postReq
    return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
        if r.Context().Value("str").(string) == "" {
            return // the request arrive without "str" in its context
        }

        ctxWithResult := context.WithValue(r.Context(), "result", mockFunc(r.Context()))
        next.ServeHTTP(rw, r.Clone(ctxWithResult))
    })
}

func InternalService() http.Handler {
    r := chi.NewRouter()
    r.Use(middleware.Logger)

    r.With(Solver).Post("/tasks/solution", emptyHandlerFunc)

    return r
}

Solution

  • Your understanding of context is not correct.

    Context (simplifying to an extent and in reference to NewRequestWithContext API), is just an in-memory object using which you can control the lifetime of the request (Handling/Triggering cancellations).

    However your code is making a HTTP call, which goes over the wire (marshaled) using HTTP protocol. This protocol doesn't understand golang's context or its values. In your scenario, both /tasks/str and /tasks/solution are being run on the same server. What if they were on different servers, probably different languages and application servers as well, So the context cannot be sent across.

    Since the APIs are within the same server, maybe you can avoid making a full blown HTTP call and resort to directly invoking the API/Method. It might turn out to be faster as well.

    If you still want to send additional values from context, then you'll have to make use of other attributes like HTTP Headers, Params, Body to send across the required information. This can provide more info on how to serialize data from context over HTTP.