// LogRequests logs the requests to the embedded logger. func LogRequests(h httpx.Handler) httpx.Handler { return httpx.HandlerFunc(func(ctx context.Context, w http.ResponseWriter, r *http.Request) error { logger.Info(ctx, "request.start", "method", r.Method, "path", r.URL.Path, ) err := h.ServeHTTPContext(ctx, w, r) logger.Info(ctx, "request.complete") return err }) }
func TestTracing(t *testing.T) { tracerTests := []tracerTest{ // simple path { routes: func(r *httpx.Router) { r.Handle("/path", httpx.HandlerFunc(func(ctx context.Context, w http.ResponseWriter, r *http.Request) error { return nil })).Methods("GET") }, req: newRequest("GET", "/path"), expectedTransactionName: "GET /path", expectedUrl: "/path", }, // path with variables { routes: func(r *httpx.Router) { r.Handle("/users/{user_id}", httpx.HandlerFunc(func(ctx context.Context, w http.ResponseWriter, r *http.Request) error { return nil })).Methods("DELETE") }, req: newRequest("DELETE", "/users/23"), expectedTransactionName: "DELETE /users/{user_id}", expectedUrl: "/users/23", }, // path with regexp variables { routes: func(r *httpx.Router) { r.Handle("/articles/{category}/{id:[0-9]+}", httpx.HandlerFunc(func(ctx context.Context, w http.ResponseWriter, r *http.Request) error { return nil })).Methods("PUT") }, req: newRequest("PUT", "/articles/tech/123"), expectedTransactionName: "PUT /articles/{category}/{id:[0-9]+}", expectedUrl: "/articles/tech/123", }, // using Path().Handler() style { routes: func(r *httpx.Router) { r.Path("/articles/{category}/{id}").HandlerFunc(func(ctx context.Context, w http.ResponseWriter, r *http.Request) error { return nil }).Methods("GET") }, req: newRequest("GET", "/articles/tech/456"), expectedTransactionName: "GET /articles/{category}/{id}", expectedUrl: "/articles/tech/456", }, // no route { routes: func(r *httpx.Router) { }, req: newRequest("GET", "/non_existent"), expectedTransactionName: "GET /non_existent", expectedUrl: "/non_existent", }, } for _, tt := range tracerTests { traceTest(t, &tt) } }
func TestAuthentication(t *testing.T) { m := &Authentication{ findAccessToken: func(token string) (*empire.AccessToken, error) { return &empire.AccessToken{ User: &empire.User{ Name: "ehjolmes", }, }, nil }, handler: httpx.HandlerFunc(func(ctx context.Context, w http.ResponseWriter, r *http.Request) error { _, ok := empire.UserFromContext(ctx) if !ok { t.Fatal("Expected a user to be present in the context") } return nil }), } ctx := context.Background() resp := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/apps", nil) req.SetBasicAuth("", "token") if err := m.ServeHTTPContext(ctx, resp, req); err != nil { t.Fatal(err) } }
func TestError(t *testing.T) { h := &Error{ handler: httpx.HandlerFunc(func(ctx context.Context, w http.ResponseWriter, r *http.Request) error { return errors.New("boom") }), } ctx := context.Background() req, _ := http.NewRequest("GET", "/path", nil) resp := httptest.NewRecorder() err := h.ServeHTTPContext(ctx, resp, req) if err != nil { t.Fatal("Expected no error to be returned because it was handled") } if got, want := resp.Body.String(), "boom\n"; got != want { t.Fatalf("Body => %s; want %s", got, want) } if got, want := resp.Code, 500; got != want { t.Fatalf("Status => %v; want %v", got, want) } }
// InsertLogger returns an httpx.Handler middleware that will call f to generate // a logger, then insert it into the context. func InsertLogger(h httpx.Handler, f func(context.Context, *http.Request) logger.Logger) httpx.Handler { return httpx.HandlerFunc(func(ctx context.Context, w http.ResponseWriter, r *http.Request) error { l := f(ctx, r) ctx = logger.WithLogger(ctx, l) return h.ServeHTTPContext(ctx, w, r) }) }
func TestRecoveryPanicString(t *testing.T) { h := &Recovery{ Reporter: reporter.ReporterFunc(func(ctx context.Context, err error) error { return nil }), handler: httpx.HandlerFunc(func(ctx context.Context, w http.ResponseWriter, r *http.Request) error { panic("boom") }), } ctx := context.Background() req, _ := http.NewRequest("GET", "/", nil) resp := httptest.NewRecorder() defer func() { if err := recover(); err != nil { t.Fatal("Expected the panic to be handled.") } }() err := h.ServeHTTPContext(ctx, resp, req) if got, want := err.Error(), "boom"; got != want { t.Fatalf("err => %v; want %v", got, want) } }
// PrefixRequestID adds the request as a prefix to the log15.Logger. func PrefixRequestID(h httpx.Handler) httpx.Handler { return httpx.HandlerFunc(func(ctx context.Context, w http.ResponseWriter, r *http.Request) error { if l, ok := logger.FromContext(ctx); ok { if l, ok := l.(log15.Logger); ok { ctx = logger.WithLogger(ctx, l.New("request_id", httpx.RequestID(ctx))) } } return h.ServeHTTPContext(ctx, w, r) }) }
// WithRequest adds information about the http.Request to reported errors. func WithRequest(h httpx.Handler) httpx.Handler { return httpx.HandlerFunc(func(ctx context.Context, w http.ResponseWriter, r *http.Request) error { ctx = httpx.WithRequest(ctx, r) // Add the request to the context. reporter.AddRequest(ctx, r) // Add the request id reporter.AddContext(ctx, "request_id", httpx.RequestID(ctx)) return h.ServeHTTPContext(ctx, w, r) }) }
func withMetrics(handlerName string, h httpx.HandlerFunc) httpx.Handler { return httpx.HandlerFunc(func(ctx context.Context, w http.ResponseWriter, r *http.Request) error { tags := []string{ fmt.Sprintf("handler:%s", handlerName), fmt.Sprintf("user:%s", auth.UserFromContext(ctx).Name), } start := time.Now() err := h(ctx, w, r) d := time.Since(start) stats.Timing(ctx, fmt.Sprintf("heroku.request"), d, 1.0, tags) stats.Timing(ctx, fmt.Sprintf("heroku.request.%s", handlerName), d, 1.0, tags) return err }) }
func TestRecovery(t *testing.T) { var ( called bool errBoom = errors.New("boom") ) h := &Recovery{ Reporter: reporter.ReporterFunc(func(ctx context.Context, err error) error { called = true e := err.(*reporter.Error) if e.Err != errBoom { t.Fatalf("err => %v; want %v", err, errBoom) } if got, want := e.Context["request_id"], "1234"; got != want { t.Fatalf("RequestID => %s; want %s", got, want) } return nil }), handler: httpx.HandlerFunc(func(ctx context.Context, w http.ResponseWriter, r *http.Request) error { panic(errBoom) }), } ctx := context.Background() req, _ := http.NewRequest("GET", "/", nil) req.Header.Set("X-Request-ID", "1234") resp := httptest.NewRecorder() ctx = httpx.WithRequest(ctx, req) defer func() { if err := recover(); err != nil { t.Fatal("Expected the panic to be handled.") } }() err := h.ServeHTTPContext(ctx, resp, req) if err != errBoom { t.Fatalf("err => %v; want %v", err, errBoom) } }
// handle adds a new handler to the router, which also increments a counter. func (s *Server) handle(method, path string, h httpx.HandlerFunc) { name := handlerName(h) fn := httpx.HandlerFunc(func(ctx context.Context, w http.ResponseWriter, r *http.Request) error { tags := []string{ fmt.Sprintf("handler:%s", name), fmt.Sprintf("user:%s", UserFromContext(ctx).Name), } start := time.Now() err := h(ctx, w, r) d := time.Since(start) stats.Timing(ctx, fmt.Sprintf("heroku.request"), d, 1.0, tags) stats.Timing(ctx, fmt.Sprintf("heroku.request.%s", name), d, 1.0, tags) return err }) s.mux.HandleFunc(path, fn).Methods(method) }
func TestLogger(t *testing.T) { b := new(bytes.Buffer) h := LogTo(httpx.HandlerFunc(func(ctx context.Context, w http.ResponseWriter, r *http.Request) error { w.WriteHeader(201) return nil }), stdLogger(b)) ctx := context.Background() resp := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/", nil) if err := h.ServeHTTPContext(ctx, resp, req); err != nil { t.Fatal(err) } if got, want := b.String(), "request_id= request.start method=GET path=/\nrequest_id= request.complete status=201\n"; got != want { t.Fatalf("%s; want %s", got, want) } }
func TestBackground(t *testing.T) { m := &Background{ handler: httpx.HandlerFunc(func(ctx context.Context, w http.ResponseWriter, r *http.Request) error { if ctx == nil { t.Fatal("Expected a context to be generated") } io.WriteString(w, `Ok`) return nil }), } req, _ := http.NewRequest("GET", "/", nil) resp := httptest.NewRecorder() m.ServeHTTP(resp, req) if got, want := resp.Body.String(), `Ok`; got != want { t.Fatalf("Body => %s; want %s", got, want) } }
func TestErrorWithHandler(t *testing.T) { var called bool h := &Error{ ErrorHandler: func(err error, w http.ResponseWriter, r *http.Request) { called = true }, handler: httpx.HandlerFunc(func(ctx context.Context, w http.ResponseWriter, r *http.Request) error { return errors.New("boom") }), } ctx := context.Background() req, _ := http.NewRequest("GET", "/path", nil) resp := httptest.NewRecorder() h.ServeHTTPContext(ctx, resp, req) if !called { t.Fatal("Expected the error handler to be called") } }
func TestRequestID(t *testing.T) { m := &RequestID{ handler: httpx.HandlerFunc(func(ctx context.Context, w http.ResponseWriter, r *http.Request) error { requestID := httpx.RequestID(ctx) if got, want := requestID, "1234"; got != want { t.Fatalf("RequestID => %s; want %s", got, want) } return nil }), } ctx := context.Background() resp := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/", nil) req.Header.Set("X-Request-ID", "1234") if err := m.ServeHTTPContext(ctx, resp, req); err != nil { t.Fatal(err) } }
// ensureUserInContext returns and httpx.Handler that raises an error if the // user isn't set in the context. func ensureUserInContext(t testing.TB) httpx.Handler { return httpx.HandlerFunc(func(ctx context.Context, w http.ResponseWriter, r *http.Request) error { auth.UserFromContext(ctx) // Panics if user is not set. return nil }) }
// errHandler returns an httpx.Handler that responds with the given error. func errHandler(err error) httpx.Handler { return httpx.HandlerFunc(func(ctx context.Context, w http.ResponseWriter, r *http.Request) error { return err }) }
// New creates the API routes and returns a new http.Handler to serve them. func New(e *empire.Empire, authenticator auth.Authenticator) httpx.Handler { r := httpx.NewRouter() // Apps r.Handle("/apps", &GetApps{e}).Methods("GET") // hk apps r.Handle("/apps/{app}", &GetAppInfo{e}).Methods("GET") // hk info r.Handle("/apps/{app}", &DeleteApp{e}).Methods("DELETE") // hk destroy r.Handle("/apps/{app}", &PatchApp{e}).Methods("PATCH") // hk destroy r.Handle("/apps/{app}/deploys", &DeployApp{e}).Methods("POST") // Deploy an image to an app r.Handle("/apps", &PostApps{e}).Methods("POST") // hk create r.Handle("/organizations/apps", &PostApps{e}).Methods("POST") // hk create // Domains r.Handle("/apps/{app}/domains", &GetDomains{e}).Methods("GET") // hk domains r.Handle("/apps/{app}/domains", &PostDomains{e}).Methods("POST") // hk domain-add r.Handle("/apps/{app}/domains/{hostname}", &DeleteDomain{e}).Methods("DELETE") // hk domain-remove // Deploys r.Handle("/deploys", &PostDeploys{e}).Methods("POST") // Deploy an app // Releases r.Handle("/apps/{app}/releases", &GetReleases{e}).Methods("GET") // hk releases r.Handle("/apps/{app}/releases/{version}", &GetRelease{e}).Methods("GET") // hk release-info r.Handle("/apps/{app}/releases", &PostReleases{e}).Methods("POST") // hk rollback // Configs r.Handle("/apps/{app}/config-vars", &GetConfigs{e}).Methods("GET") // hk env, hk get r.Handle("/apps/{app}/config-vars", &PatchConfigs{e}).Methods("PATCH") // hk set, hk unset // Processes r.Handle("/apps/{app}/dynos", &GetProcesses{e}).Methods("GET") // hk dynos r.Handle("/apps/{app}/dynos", &PostProcess{e}).Methods("POST") // hk run r.Handle("/apps/{app}/dynos", &DeleteProcesses{e}).Methods("DELETE") // hk restart r.Handle("/apps/{app}/dynos/{ptype}.{pid}", &DeleteProcesses{e}).Methods("DELETE") // hk restart web.1 r.Handle("/apps/{app}/dynos/{pid}", &DeleteProcesses{e}).Methods("DELETE") // hk restart web // Formations r.Handle("/apps/{app}/formation", &GetFormation{e}).Methods("GET") // hk scale -l r.Handle("/apps/{app}/formation", &PatchFormation{e}).Methods("PATCH") // hk scale // OAuth r.Handle("/oauth/authorizations", &PostAuthorizations{e}).Methods("POST") // SSL sslRemoved := errHandler(ErrSSLRemoved) r.Handle("/apps/{app}/ssl-endpoints", sslRemoved).Methods("GET") // hk ssl r.Handle("/apps/{app}/ssl-endpoints", sslRemoved).Methods("POST") // hk ssl-cert-add r.Handle("/apps/{app}/ssl-endpoints/{cert}", sslRemoved).Methods("PATCH") // hk ssl-cert-add, hk ssl-cert-rollback r.Handle("/apps/{app}/ssl-endpoints/{cert}", sslRemoved).Methods("DELETE") // hk ssl-destroy // Logs r.Handle("/apps/{app}/log-sessions", &PostLogs{e}).Methods("POST") // hk log api := Authenticate(r, authenticator) return httpx.HandlerFunc(func(ctx context.Context, w http.ResponseWriter, r *http.Request) error { err := api.ServeHTTPContext(ctx, w, r) if err != nil { Error(w, err, http.StatusInternalServerError) reporter.Report(ctx, err) } return nil }) }