Merge pull request #214 from skidder/httprp-transport
HTTP Reverse Proxy Transport
Peter Bourgon
8 years ago
0 | # package transport/httprp | |
1 | ||
2 | `package transport/httprp` provides an HTTP reverse-proxy transport. | |
3 | ||
4 | ## Rationale | |
5 | ||
6 | HTTP server applications often associate multiple handlers with a single HTTP listener, each handler differentiated by the request URI and/or HTTP method. Handlers that perform business-logic in the app can implement the `Endpoint` interface and be exposed using the `package transport/http` server. Handlers that need to proxy the request to another HTTP endpoint can do so with this package by simply specifying the base URL to forward the request to. | |
7 | ||
8 | ## Usage | |
9 | ||
10 | The following example uses the [Gorilla Mux](https://github.com/gorilla/mux) router to illustrate how a mixture of proxying and non-proxying request handlers can be used with a single listener: | |
11 | ||
12 | ```go | |
13 | import ( | |
14 | "net/http" | |
15 | "net/url" | |
16 | ||
17 | kithttp "github.com/go-kit/kit/transport/http" | |
18 | kithttprp "github.com/go-kit/kit/transport/httprp" | |
19 | "github.com/gorilla/mux" | |
20 | "golang.org/x/net/context" | |
21 | ) | |
22 | ||
23 | func main() { | |
24 | router := mux.NewRouter() | |
25 | ||
26 | // server HTTP endpoint handled here | |
27 | router.Handle("/foo", | |
28 | kithttp.NewServer( | |
29 | context.Background(), | |
30 | func(context.Context, interface{}) (interface{}, error) { return struct{}{}, nil }, | |
31 | func(*http.Request) (interface{}, error) { return struct{}{}, nil }, | |
32 | func(http.ResponseWriter, interface{}) error { return nil }, | |
33 | )).Methods("GET") | |
34 | ||
35 | // proxy endpoint, forwards requests to http://other.service.local/base/bar | |
36 | remoteServiceURL, _ := url.Parse("http://other.service.local/base") | |
37 | router.Handle("/bar", | |
38 | kithttprp.NewServer( | |
39 | context.Background(), | |
40 | remoteServiceURL, | |
41 | )).Methods("GET") | |
42 | ||
43 | http.ListenAndServe(":8080", router) | |
44 | } | |
45 | ``` | |
46 | ||
47 | You can also supply a set of `RequestFunc` functions to be run before proxying the request. This can be useful for adding request headers required by the backend system (e.g. API tokens). |
0 | package httprp | |
1 | ||
2 | import ( | |
3 | "net/http" | |
4 | "net/http/httputil" | |
5 | "net/url" | |
6 | ||
7 | "golang.org/x/net/context" | |
8 | ) | |
9 | ||
10 | // RequestFunc may take information from an HTTP request and put it into a | |
11 | // request context. BeforeFuncs are executed prior to invoking the | |
12 | // endpoint. | |
13 | type RequestFunc func(context.Context, *http.Request) context.Context | |
14 | ||
15 | // Server is a proxying request handler. | |
16 | type Server struct { | |
17 | ctx context.Context | |
18 | proxy http.Handler | |
19 | before []RequestFunc | |
20 | errorEncoder func(w http.ResponseWriter, err error) | |
21 | } | |
22 | ||
23 | // NewServer constructs a new server that implements http.Server and will proxy | |
24 | // requests to the given base URL using its scheme, host, and base path. | |
25 | // If the target's path is "/base" and the incoming request was for "/dir", | |
26 | // the target request will be for /base/dir. | |
27 | func NewServer( | |
28 | ctx context.Context, | |
29 | baseURL *url.URL, | |
30 | options ...ServerOption, | |
31 | ) *Server { | |
32 | s := &Server{ | |
33 | ctx: ctx, | |
34 | proxy: httputil.NewSingleHostReverseProxy(baseURL), | |
35 | } | |
36 | for _, option := range options { | |
37 | option(s) | |
38 | } | |
39 | return s | |
40 | } | |
41 | ||
42 | // ServerOption sets an optional parameter for servers. | |
43 | type ServerOption func(*Server) | |
44 | ||
45 | // ServerBefore functions are executed on the HTTP request object before the | |
46 | // request is decoded. | |
47 | func ServerBefore(before ...RequestFunc) ServerOption { | |
48 | return func(s *Server) { s.before = before } | |
49 | } | |
50 | ||
51 | // ServeHTTP implements http.Handler. | |
52 | func (s Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { | |
53 | ctx, cancel := context.WithCancel(s.ctx) | |
54 | defer cancel() | |
55 | ||
56 | for _, f := range s.before { | |
57 | ctx = f(ctx, r) | |
58 | } | |
59 | ||
60 | s.proxy.ServeHTTP(w, r) | |
61 | } |
0 | package httprp_test | |
1 | ||
2 | import ( | |
3 | "io/ioutil" | |
4 | "net/http" | |
5 | "net/http/httptest" | |
6 | "net/url" | |
7 | "testing" | |
8 | ||
9 | "golang.org/x/net/context" | |
10 | ||
11 | httptransport "github.com/go-kit/kit/transport/httprp" | |
12 | ) | |
13 | ||
14 | func TestServerHappyPathSingleServer(t *testing.T) { | |
15 | originServer := httptest.NewServer( | |
16 | http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
17 | w.WriteHeader(http.StatusOK) | |
18 | w.Write([]byte("hey")) | |
19 | })) | |
20 | defer originServer.Close() | |
21 | originURL, _ := url.Parse(originServer.URL) | |
22 | ||
23 | handler := httptransport.NewServer( | |
24 | context.Background(), | |
25 | originURL, | |
26 | ) | |
27 | proxyServer := httptest.NewServer(handler) | |
28 | defer proxyServer.Close() | |
29 | ||
30 | resp, _ := http.Get(proxyServer.URL) | |
31 | if want, have := http.StatusOK, resp.StatusCode; want != have { | |
32 | t.Errorf("want %d, have %d", want, have) | |
33 | } | |
34 | ||
35 | responseBody, _ := ioutil.ReadAll(resp.Body) | |
36 | if want, have := "hey", string(responseBody); want != have { | |
37 | t.Errorf("want %d, have %d", want, have) | |
38 | } | |
39 | } | |
40 | ||
41 | func TestServerHappyPathSingleServerWithServerOptions(t *testing.T) { | |
42 | const ( | |
43 | headerKey = "X-TEST-HEADER" | |
44 | headerVal = "go-kit-proxy" | |
45 | ) | |
46 | ||
47 | originServer := httptest.NewServer( | |
48 | http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
49 | if want, have := headerVal, r.Header.Get(headerKey); want != have { | |
50 | t.Errorf("want %d, have %d", want, have) | |
51 | } | |
52 | ||
53 | w.WriteHeader(http.StatusOK) | |
54 | w.Write([]byte("hey")) | |
55 | })) | |
56 | defer originServer.Close() | |
57 | originURL, _ := url.Parse(originServer.URL) | |
58 | ||
59 | handler := httptransport.NewServer( | |
60 | context.Background(), | |
61 | originURL, | |
62 | httptransport.ServerBefore(func(ctx context.Context, r *http.Request) context.Context { | |
63 | r.Header.Add(headerKey, headerVal) | |
64 | return ctx | |
65 | }), | |
66 | ) | |
67 | proxyServer := httptest.NewServer(handler) | |
68 | defer proxyServer.Close() | |
69 | ||
70 | resp, _ := http.Get(proxyServer.URL) | |
71 | if want, have := http.StatusOK, resp.StatusCode; want != have { | |
72 | t.Errorf("want %d, have %d", want, have) | |
73 | } | |
74 | ||
75 | responseBody, _ := ioutil.ReadAll(resp.Body) | |
76 | if want, have := "hey", string(responseBody); want != have { | |
77 | t.Errorf("want %d, have %d", want, have) | |
78 | } | |
79 | } | |
80 | ||
81 | func TestServerOriginServerNotFoundResponse(t *testing.T) { | |
82 | originServer := httptest.NewServer(http.NotFoundHandler()) | |
83 | defer originServer.Close() | |
84 | originURL, _ := url.Parse(originServer.URL) | |
85 | ||
86 | handler := httptransport.NewServer( | |
87 | context.Background(), | |
88 | originURL, | |
89 | ) | |
90 | proxyServer := httptest.NewServer(handler) | |
91 | defer proxyServer.Close() | |
92 | ||
93 | resp, _ := http.Get(proxyServer.URL) | |
94 | if want, have := http.StatusNotFound, resp.StatusCode; want != have { | |
95 | t.Errorf("want %d, have %d", want, have) | |
96 | } | |
97 | } | |
98 | ||
99 | func TestServerOriginServerUnreachable(t *testing.T) { | |
100 | // create a server, then promptly shut it down | |
101 | originServer := httptest.NewServer( | |
102 | http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
103 | w.WriteHeader(http.StatusOK) | |
104 | })) | |
105 | originURL, _ := url.Parse(originServer.URL) | |
106 | originServer.Close() | |
107 | ||
108 | handler := httptransport.NewServer( | |
109 | context.Background(), | |
110 | originURL, | |
111 | ) | |
112 | proxyServer := httptest.NewServer(handler) | |
113 | defer proxyServer.Close() | |
114 | ||
115 | resp, _ := http.Get(proxyServer.URL) | |
116 | if want, have := http.StatusInternalServerError, resp.StatusCode; want != have { | |
117 | t.Errorf("want %d, have %d", want, have) | |
118 | } | |
119 | } |