2

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
}
mcv_dev
  • 338
  • 3
  • 14
  • 2
    Context in client requests is not for data sending to the server, it's for cancelation. You can not do what you want with context. For sending data over to the other server use URL query parameters, or headers, or the body. Not the context, that just won't work, no matter how much you wish it would. – mkopriva Nov 04 '21 at 19:53
  • 2
    [`Context`](https://pkg.go.dev/net/http#Request.Context) - *"For outgoing client requests, the context controls cancellation."* / [`WithContext`](https://pkg.go.dev/net/http#Request.WithContext) - *"For outgoing client request, the context controls the entire lifetime of a request and its response: obtaining a connection, sending the request, and reading the response headers and body. "* – mkopriva Nov 04 '21 at 19:58

1 Answers1

6

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.

Kishore Bandi
  • 5,537
  • 2
  • 31
  • 52
  • Thank you. I have a side question. You said that contexts are used to control lifetime of a requests thus WithCancel or WithDeadline are being used. Then what is WithValue used for when it comes to http requests? – mcv_dev Nov 04 '21 at 20:19
  • 1
    @mcv_dev context's `withValue` is used to pass around value to any subcalls to which that context or its child context will be passed to. For HTTP Requests, it won't play any role (unless that HTTP library uses this value, which in this case its not, so its of no use) – Kishore Bandi Nov 04 '21 at 20:23
  • 1
    The example at https://go.dev/blog/context uses `WithValue` to pass along data scoped to the request: "The set of goroutines working on a request typically needs access to request-specific values such as the identity of the end user, authorization tokens, and the request’s deadline." So perfectly valid to use WithValue in an HTTP setting - but like the answer says, that Context applies only to the code handing that side of the http request. A client request and a server request each have a context, but they are in no way the same context; any similarity in values is purely incidental. – erik258 Nov 05 '21 at 02:55