Collectives™ on Stack Overflow

Find centralized, trusted content and collaborate around the technologies you use most.

Learn more about Collectives

Teams

Q&A for work

Connect and share knowledge within a single location that is structured and easy to search.

Learn more about Teams

I'm trying to learn Go and figured a nice little project would be an A/B testing proxy to put in front of a web server. Little did I know Go essentially offers a reverse proxy out of the box, so the setup was easy. I've got it to the point where I'm proxying traffic, but here's the thing, I have trouble implementing the actual functionality because wherever I have access to the response, I don't have access to assigned A/B test variations:

  • In the handleFunc I'm assigning variations of each test to the request, so the upstream server can also be aware of it and use if for implementations in it's backend.
  • I'm setting a cookie with all tests and variations, both on the request that's being proxied to the upstream and on the response that's returned to the client.
  • Tests that consist of a find/replace pair will do mutations on the response body after the response comes back from the upstream server.
  • I'm trying to use the modifyResponse function of httputil.ReverseProxy to do the response mutation.
  • The problem is that I can't figure out how to share the assigned variations between the handleFunc and modifyResponse , without changing the upstream server. I'd like to be able to share this context (basically a map[string]string somehow.

    Code sample:

    Here's a distilled version of my code, where my question basically is, how can modifyRequest know about random assignments that happened in handleFunc ?

    package main
    import (
        config2 "ab-proxy/config"
        "bytes"
        "fmt"
        "io/ioutil"
        "net/http"
        "net/http/httputil"
        "net/url"
        "strconv"
        "strings"
    var config config2.ProxyConfig
    var reverseProxy *httputil.ReverseProxy
    var tests config2.Tests
    func overwriteCookie(req *http.Request, cookie *http.Cookie) {
        // omitted for brevity, will replace a cookie header, instead of adding a second value
    func parseRequestCookiesToAssignedTests(req *http.Request) map[string]string {
        // omitted for brevity, builds a map where the key is the identifier of the test, the value the assigned variant
    func renderCookieForAssignedTests(assignedTests map[string]string) string {
        // omitted for brevity, builds a cookie string
    func main () {
        var err error
        if  config, err = config2.LoadConfig(); err != nil {
            fmt.Println(err)
            return
        if tests, err = config2.LoadTests(); err != nil {
            fmt.Println(err)
            return
        upstreamUrl, _ := url.Parse("0.0.0.0:80")
        reverseProxy = httputil.NewSingleHostReverseProxy(upstreamUrl)
        reverseProxy.ModifyResponse = modifyResponse
        http.HandleFunc("/", handleRequest)
        if err := http.ListenAndServe("0.0.0.0:80", nil); err != nil {
            fmt.Println("Could not start proxy")
    func handleRequest(res http.ResponseWriter, req *http.Request) {
        assigned := parseRequestCookiesToAssignedTests(req)
        newCookies := make(map[string]string)
        for _, test := range tests.Entries {
            val, ok := assigned[test.Identifier]
            if ok {
                newCookies[test.Identifier] = val
            } else {
                newCookies[test.Identifier] = "not-assigned-yet" // this will be replaced by random variation assignment
        testCookie := http.Cookie{Name: config.Cookie.Name, Value: renderCookieForAssignedTests(newCookies)}
        // Add cookie to request to be sent to upstream
        overwriteCookie(req, &testCookie)
        // Add cookie to response to be returned to client
        http.SetCookie(res, &testCookie)
        reverseProxy.ServeHTTP(res, req)
    func modifyResponse (response *http.Response) error {
        body, err := ioutil.ReadAll(response.Body)
        if err != nil {
            return  err
        err = response.Body.Close()
        if err != nil {
            return err
        response.Body = ioutil.NopCloser(bytes.NewReader(body))
        response.ContentLength = int64(len(body))
        response.Header.Set("Content-Length", strconv.Itoa(len(body)))
        return nil
    

    Use a standard context.Context. This is accessible in your handler via the *http.Request. And the request is also accessible via the *http.Response argument to modifyResponse.

    In your handler:

    ctx := req.Context()
    // Set values, deadlines, etc.
    req = req.WithContext(ctx)
    reverseProxy.ServeHTTP(res, req)
    

    Then in modifyResponse:

    ctx := response.Request.Context()
    // fetch values, check for cancellation, etc
                    Ah, amazing! Not even a fancy IDE could help me find out that there's even a .Request property on the response and that there's in fact a generic context.Context class that does exactly what I'm looking for. That helps a lot, thanks!
    – Erik Booij
                    Mar 17, 2019 at 19:30
            

    Thanks for contributing an answer to Stack Overflow!

    • Please be sure to answer the question. Provide details and share your research!

    But avoid

    • Asking for help, clarification, or responding to other answers.
    • Making statements based on opinion; back them up with references or personal experience.

    To learn more, see our tips on writing great answers.