Reputation: 135
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:
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.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.
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
}
Upvotes: 0
Views: 1906
Reputation: 79614
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
Upvotes: 4