Add unit tests and README.md for httprp package
Scott Kidder
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 associated 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). |
8 | 8 | ) |
9 | 9 | |
10 | 10 | // RequestFunc may take information from an HTTP request and put it into a |
11 | // request context. In Servers, BeforeFuncs are executed prior to invoking the | |
12 | // endpoint. In Clients, BeforeFuncs are executed after creating the request | |
13 | // but prior to invoking the HTTP client. | |
11 | // request context. BeforeFuncs are executed prior to invoking the | |
12 | // endpoint. | |
14 | 13 | type RequestFunc func(context.Context, *http.Request) context.Context |
15 | 14 | |
16 | 15 | // Server wraps an endpoint and implements http.Handler. |
21 | 20 | errorEncoder func(w http.ResponseWriter, err error) |
22 | 21 | } |
23 | 22 | |
24 | // NewServer constructs a new server, which implements http.Server and wraps | |
25 | // the provided endpoint. | |
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. | |
26 | 27 | func NewServer( |
27 | 28 | ctx context.Context, |
28 | target *url.URL, | |
29 | baseURL *url.URL, | |
29 | 30 | options ...ServerOption, |
30 | 31 | ) *Server { |
31 | 32 | s := &Server{ |
32 | 33 | ctx: ctx, |
33 | proxy: httputil.NewSingleHostReverseProxy(target), | |
34 | proxy: httputil.NewSingleHostReverseProxy(baseURL), | |
34 | 35 | } |
35 | 36 | for _, option := range options { |
36 | 37 | option(s) |
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 | originServer := httptest.NewServer( | |
43 | http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
44 | if want, have := "go-kit-proxy", r.Header.Get("X-TEST-HEADER"); want != have { | |
45 | t.Errorf("want %d, have %d", want, have) | |
46 | } | |
47 | ||
48 | w.WriteHeader(http.StatusOK) | |
49 | w.Write([]byte("hey")) | |
50 | })) | |
51 | defer originServer.Close() | |
52 | originURL, _ := url.Parse(originServer.URL) | |
53 | ||
54 | handler := httptransport.NewServer( | |
55 | context.Background(), | |
56 | originURL, | |
57 | httptransport.ServerBefore(func(ctx context.Context, r *http.Request) context.Context { | |
58 | r.Header.Add("X-TEST-HEADER", "go-kit-proxy") | |
59 | return ctx | |
60 | }), | |
61 | ) | |
62 | proxyServer := httptest.NewServer(handler) | |
63 | defer proxyServer.Close() | |
64 | ||
65 | resp, _ := http.Get(proxyServer.URL) | |
66 | if want, have := http.StatusOK, resp.StatusCode; want != have { | |
67 | t.Errorf("want %d, have %d", want, have) | |
68 | } | |
69 | ||
70 | responseBody, _ := ioutil.ReadAll(resp.Body) | |
71 | if want, have := "hey", string(responseBody); want != have { | |
72 | t.Errorf("want %d, have %d", want, have) | |
73 | } | |
74 | } | |
75 | ||
76 | func TestServerOriginServerNotFoundResponse(t *testing.T) { | |
77 | originServer := httptest.NewServer(http.NotFoundHandler()) | |
78 | defer originServer.Close() | |
79 | originURL, _ := url.Parse(originServer.URL) | |
80 | ||
81 | handler := httptransport.NewServer( | |
82 | context.Background(), | |
83 | originURL, | |
84 | ) | |
85 | proxyServer := httptest.NewServer(handler) | |
86 | defer proxyServer.Close() | |
87 | ||
88 | resp, _ := http.Get(proxyServer.URL) | |
89 | if want, have := http.StatusNotFound, resp.StatusCode; want != have { | |
90 | t.Errorf("want %d, have %d", want, have) | |
91 | } | |
92 | } | |
93 | ||
94 | func TestServerOriginServerUnreachable(t *testing.T) { | |
95 | // create a server, then promptly shut it down | |
96 | originServer := httptest.NewServer( | |
97 | http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
98 | w.WriteHeader(http.StatusOK) | |
99 | })) | |
100 | originURL, _ := url.Parse(originServer.URL) | |
101 | originServer.Close() | |
102 | ||
103 | handler := httptransport.NewServer( | |
104 | context.Background(), | |
105 | originURL, | |
106 | ) | |
107 | proxyServer := httptest.NewServer(handler) | |
108 | defer proxyServer.Close() | |
109 | ||
110 | resp, _ := http.Get(proxyServer.URL) | |
111 | if want, have := http.StatusInternalServerError, resp.StatusCode; want != have { | |
112 | t.Errorf("want %d, have %d", want, have) | |
113 | } | |
114 | } |