feat(kit/feature): Add HTTP request proxy.
parent
9bde937c54
commit
73ae2122aa
|
@ -0,0 +1,65 @@
|
|||
package feature
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// HTTPProxy is an HTTP proxy that's guided by a feature flag. If the feature flag
|
||||
// presented to it is enabled, it will perform the proxying behavior. Otherwise
|
||||
// it will be a no-op.
|
||||
type HTTPProxy struct {
|
||||
proxy *httputil.ReverseProxy
|
||||
logger *zap.Logger
|
||||
enabler ProxyEnabler
|
||||
}
|
||||
|
||||
// NewHTTPProxy returns a new Proxy.
|
||||
func NewHTTPProxy(dest *url.URL, logger *zap.Logger, enabler ProxyEnabler) *HTTPProxy {
|
||||
return &HTTPProxy{
|
||||
proxy: newReverseProxy(dest, enabler.Key()),
|
||||
logger: logger,
|
||||
enabler: enabler,
|
||||
}
|
||||
}
|
||||
|
||||
// Do performs the proxying. It returns whether or not the request was proxied.
|
||||
func (p *HTTPProxy) Do(w http.ResponseWriter, r *http.Request) bool {
|
||||
if p.enabler.Enabled(r.Context()) {
|
||||
p.proxy.ServeHTTP(w, r)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const (
|
||||
// headerProxyFlag is the HTTP header for enriching the request and response
|
||||
// with the feature flag key that precipitated the proxying behavior.
|
||||
headerProxyFlag = "X-Proxy-Flag"
|
||||
)
|
||||
|
||||
// newReverseProxy creates a new single-host reverse proxy.
|
||||
func newReverseProxy(dest *url.URL, enablerKey string) *httputil.ReverseProxy {
|
||||
proxy := httputil.NewSingleHostReverseProxy(dest)
|
||||
|
||||
defaultDirector := proxy.Director
|
||||
proxy.Director = func(r *http.Request) {
|
||||
defaultDirector(r)
|
||||
r.Header.Set(headerProxyFlag, enablerKey)
|
||||
}
|
||||
proxy.ModifyResponse = func(r *http.Response) error {
|
||||
r.Header.Set(headerProxyFlag, enablerKey)
|
||||
return nil
|
||||
}
|
||||
return proxy
|
||||
}
|
||||
|
||||
// ProxyEnabler is a boolean feature flag.
|
||||
type ProxyEnabler interface {
|
||||
Key() string
|
||||
Enabled(ctx context.Context, fs ...Flagger) bool
|
||||
}
|
|
@ -0,0 +1,137 @@
|
|||
package feature
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zaptest"
|
||||
)
|
||||
|
||||
const (
|
||||
destBody = "hello from destination"
|
||||
srcBody = "hello from source"
|
||||
flagKey = "fancy-feature"
|
||||
)
|
||||
|
||||
func TestHTTPProxy_Proxying(t *testing.T) {
|
||||
en := enabler{key: flagKey, state: true}
|
||||
logger := zaptest.NewLogger(t)
|
||||
resp, err := testHTTPProxy(logger, en)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
proxyFlag := resp.Header.Get("X-Proxy-Flag")
|
||||
if proxyFlag != flagKey {
|
||||
t.Error("X-Proxy-Flag header not populated")
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
bodyStr := string(body)
|
||||
if bodyStr != destBody {
|
||||
t.Errorf("expected body of destination handler, but got: %q", bodyStr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPProxy_DefaultBehavior(t *testing.T) {
|
||||
en := enabler{key: flagKey, state: false}
|
||||
logger := zaptest.NewLogger(t)
|
||||
resp, err := testHTTPProxy(logger, en)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
proxyFlag := resp.Header.Get("X-Proxy-Flag")
|
||||
if proxyFlag != "" {
|
||||
t.Error("X-Proxy-Flag header populated")
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
bodyStr := string(body)
|
||||
if bodyStr != srcBody {
|
||||
t.Errorf("expected body of source handler, but got: %q", bodyStr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPProxy_RequestHeader(t *testing.T) {
|
||||
h := func(w http.ResponseWriter, r *http.Request) {
|
||||
proxyFlag := r.Header.Get("X-Proxy-Flag")
|
||||
if proxyFlag != flagKey {
|
||||
t.Error("expected X-Proxy-Flag to contain feature flag key")
|
||||
}
|
||||
}
|
||||
|
||||
s := httptest.NewServer(http.HandlerFunc(h))
|
||||
defer s.Close()
|
||||
|
||||
sURL, err := url.Parse(s.URL)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
logger := zaptest.NewLogger(t)
|
||||
en := enabler{key: flagKey, state: true}
|
||||
proxy := NewHTTPProxy(sURL, logger, en)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("GET", "http://example.com/foo", nil)
|
||||
srcHandler(proxy)(w, r)
|
||||
}
|
||||
|
||||
func testHTTPProxy(logger *zap.Logger, enabler ProxyEnabler) (*http.Response, error) {
|
||||
s := httptest.NewServer(http.HandlerFunc(destHandler))
|
||||
defer s.Close()
|
||||
|
||||
sURL, err := url.Parse(s.URL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
proxy := NewHTTPProxy(sURL, logger, enabler)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("GET", "http://example.com/foo", nil)
|
||||
srcHandler(proxy)(w, r)
|
||||
|
||||
return w.Result(), nil
|
||||
}
|
||||
|
||||
func destHandler(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprint(w, destBody)
|
||||
}
|
||||
|
||||
func srcHandler(proxy *HTTPProxy) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if proxy.Do(w, r) {
|
||||
return
|
||||
}
|
||||
fmt.Fprint(w, srcBody)
|
||||
}
|
||||
}
|
||||
|
||||
type enabler struct {
|
||||
key string
|
||||
state bool
|
||||
}
|
||||
|
||||
func (e enabler) Key() string {
|
||||
return e.key
|
||||
}
|
||||
|
||||
func (e enabler) Enabled(context.Context, ...Flagger) bool {
|
||||
return e.state
|
||||
}
|
Loading…
Reference in New Issue