diff --git a/transport/httprp/README.md b/transport/httprp/README.md new file mode 100644 index 0000000..519791e --- /dev/null +++ b/transport/httprp/README.md @@ -0,0 +1,48 @@ +# package transport/httprp + +`package transport/httprp` provides an HTTP reverse-proxy transport. + +## Rationale + +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. + +## Usage + +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: + +```go +import ( + "net/http" + "net/url" + + kithttp "github.com/go-kit/kit/transport/http" + kithttprp "github.com/go-kit/kit/transport/httprp" + "github.com/gorilla/mux" + "golang.org/x/net/context" +) + +func main() { + router := mux.NewRouter() + + // server HTTP endpoint handled here + router.Handle("/foo", + kithttp.NewServer( + context.Background(), + func(context.Context, interface{}) (interface{}, error) { return struct{}{}, nil }, + func(*http.Request) (interface{}, error) { return struct{}{}, nil }, + func(http.ResponseWriter, interface{}) error { return nil }, + )).Methods("GET") + + // proxy endpoint, forwards requests to http://other.service.local/base/bar + remoteServiceURL, _ := url.Parse("http://other.service.local/base") + router.Handle("/bar", + kithttprp.NewServer( + context.Background(), + remoteServiceURL, + )).Methods("GET") + + http.ListenAndServe(":8080", router) +} +``` + +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). diff --git a/transport/httprp/server.go b/transport/httprp/server.go index 815adf0..636e223 100644 --- a/transport/httprp/server.go +++ b/transport/httprp/server.go @@ -9,9 +9,8 @@ ) // RequestFunc may take information from an HTTP request and put it into a -// request context. In Servers, BeforeFuncs are executed prior to invoking the -// endpoint. In Clients, BeforeFuncs are executed after creating the request -// but prior to invoking the HTTP client. +// request context. BeforeFuncs are executed prior to invoking the +// endpoint. type RequestFunc func(context.Context, *http.Request) context.Context // Server wraps an endpoint and implements http.Handler. @@ -22,16 +21,18 @@ errorEncoder func(w http.ResponseWriter, err error) } -// NewServer constructs a new server, which implements http.Server and wraps -// the provided endpoint. +// NewServer constructs a new server that implements http.Server and will proxy +// requests to the given base URL using its scheme, host, and base path. +// If the target's path is "/base" and the incoming request was for "/dir", +// the target request will be for /base/dir. func NewServer( ctx context.Context, - target *url.URL, + baseURL *url.URL, options ...ServerOption, ) *Server { s := &Server{ ctx: ctx, - proxy: httputil.NewSingleHostReverseProxy(target), + proxy: httputil.NewSingleHostReverseProxy(baseURL), } for _, option := range options { option(s) diff --git a/transport/httprp/server_test.go b/transport/httprp/server_test.go new file mode 100644 index 0000000..d93a421 --- /dev/null +++ b/transport/httprp/server_test.go @@ -0,0 +1,115 @@ +package httprp_test + +import ( + "io/ioutil" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "golang.org/x/net/context" + + httptransport "github.com/go-kit/kit/transport/httprp" +) + +func TestServerHappyPathSingleServer(t *testing.T) { + originServer := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte("hey")) + })) + defer originServer.Close() + originURL, _ := url.Parse(originServer.URL) + + handler := httptransport.NewServer( + context.Background(), + originURL, + ) + proxyServer := httptest.NewServer(handler) + defer proxyServer.Close() + + resp, _ := http.Get(proxyServer.URL) + if want, have := http.StatusOK, resp.StatusCode; want != have { + t.Errorf("want %d, have %d", want, have) + } + + responseBody, _ := ioutil.ReadAll(resp.Body) + if want, have := "hey", string(responseBody); want != have { + t.Errorf("want %d, have %d", want, have) + } +} + +func TestServerHappyPathSingleServerWithServerOptions(t *testing.T) { + originServer := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if want, have := "go-kit-proxy", r.Header.Get("X-TEST-HEADER"); want != have { + t.Errorf("want %d, have %d", want, have) + } + + w.WriteHeader(http.StatusOK) + w.Write([]byte("hey")) + })) + defer originServer.Close() + originURL, _ := url.Parse(originServer.URL) + + handler := httptransport.NewServer( + context.Background(), + originURL, + httptransport.ServerBefore(func(ctx context.Context, r *http.Request) context.Context { + r.Header.Add("X-TEST-HEADER", "go-kit-proxy") + return ctx + }), + ) + proxyServer := httptest.NewServer(handler) + defer proxyServer.Close() + + resp, _ := http.Get(proxyServer.URL) + if want, have := http.StatusOK, resp.StatusCode; want != have { + t.Errorf("want %d, have %d", want, have) + } + + responseBody, _ := ioutil.ReadAll(resp.Body) + if want, have := "hey", string(responseBody); want != have { + t.Errorf("want %d, have %d", want, have) + } +} + +func TestServerOriginServerNotFoundResponse(t *testing.T) { + originServer := httptest.NewServer(http.NotFoundHandler()) + defer originServer.Close() + originURL, _ := url.Parse(originServer.URL) + + handler := httptransport.NewServer( + context.Background(), + originURL, + ) + proxyServer := httptest.NewServer(handler) + defer proxyServer.Close() + + resp, _ := http.Get(proxyServer.URL) + if want, have := http.StatusNotFound, resp.StatusCode; want != have { + t.Errorf("want %d, have %d", want, have) + } +} + +func TestServerOriginServerUnreachable(t *testing.T) { + // create a server, then promptly shut it down + originServer := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + originURL, _ := url.Parse(originServer.URL) + originServer.Close() + + handler := httptransport.NewServer( + context.Background(), + originURL, + ) + proxyServer := httptest.NewServer(handler) + defer proxyServer.Close() + + resp, _ := http.Get(proxyServer.URL) + if want, have := http.StatusInternalServerError, resp.StatusCode; want != have { + t.Errorf("want %d, have %d", want, have) + } +}