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
–
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.