// ErrorHandler turns a Go error into an JSONAPI HTTP response. It should be placed in the middleware chain // below the logger middleware so the logger properly logs the HTTP response. ErrorHandler // understands instances of goa.ServiceError and returns the status and response body embodied in // them, it turns other Go error types into a 500 internal error response. // If verbose is false the details of internal errors is not included in HTTP responses. // If you use github.com/pkg/errors then wrapping the error will allow a trace to be printed to the logs func ErrorHandler(service *goa.Service, verbose bool) goa.Middleware { return func(h goa.Handler) goa.Handler { return func(ctx context.Context, rw http.ResponseWriter, req *http.Request) error { e := h(ctx, rw, req) if e == nil { return nil } cause := errs.Cause(e) status := http.StatusInternalServerError var respBody interface{} respBody, status = ErrorToJSONAPIErrors(e) rw.Header().Set("Content-Type", ErrorMediaIdentifier) if err, ok := cause.(goa.ServiceError); ok { status = err.ResponseStatus() //respBody = err goa.ContextResponse(ctx).ErrorCode = err.Token() //rw.Header().Set("Content-Type", ErrorMediaIdentifier) } else { //respBody = e.Error() //rw.Header().Set("Content-Type", "text/plain") } if status >= 500 && status < 600 { //reqID := ctx.Value(reqIDKey) reqID := ctx.Value(1) // TODO remove this hack if reqID == nil { reqID = shortID() //ctx = context.WithValue(ctx, reqIDKey, reqID) ctx = context.WithValue(ctx, 1, reqID) // TODO remove this hack } log.Error(ctx, map[string]interface{}{ "msg": respBody, "err": fmt.Sprintf("%+v", e), }, "uncaught error detected in ErrorHandler") if !verbose { rw.Header().Set("Content-Type", goa.ErrorMediaIdentifier) msg := errors.NewInternalError(fmt.Sprintf("%s [%s]", http.StatusText(http.StatusInternalServerError), reqID)) //respBody = goa.ErrInternal(msg) respBody, status = ErrorToJSONAPIErrors(msg) // Preserve the ID of the original error as that's what gets logged, the client // received error ID must match the original // TODO for JSONAPI this won't work I guess. if origErrID := goa.ContextResponse(ctx).ErrorCode; origErrID != "" { respBody.(*goa.ErrorResponse).ID = origErrID } } } return service.Send(ctx, status, respBody) } } }
// ErrorHandler turns a Go error into an HTTP response. It should be placed in the middleware chain // below the logger middleware so the logger properly logs the HTTP response. ErrorHandler // understands instances of goa.Error and returns the status and response body embodied in them, // it turns other Go error types into a 500 internal error response. // If verbose is false the details of internal errors is not included in HTTP responses. func ErrorHandler(service *goa.Service, verbose bool) goa.Middleware { return func(h goa.Handler) goa.Handler { return func(ctx context.Context, rw http.ResponseWriter, req *http.Request) error { e := h(ctx, rw, req) if e == nil { return nil } status := http.StatusInternalServerError var respBody interface{} if err, ok := e.(*goa.Error); ok { status = err.Status respBody = err goa.ContextResponse(ctx).ErrorCode = err.Code rw.Header().Set("Content-Type", goa.ErrorMediaIdentifier) } else { respBody = e.Error() rw.Header().Set("Content-Type", "text/plain") } if status >= 500 && status < 600 { reqID := ctx.Value(reqIDKey) if reqID == nil { reqID = shortID() ctx = context.WithValue(ctx, reqIDKey, reqID) } goa.LogError(ctx, "uncaught error", "id", reqID, "msg", respBody) if !verbose { rw.Header().Set("Content-Type", goa.ErrorMediaIdentifier) respBody = goa.ErrInternal("internal error [%s]", reqID) } } return service.Send(ctx, status, respBody) } } }
// Middleware encodes the response using Gzip encoding and sets all the appropriate // headers. If the Content-Type is not set, it will be set by calling // http.DetectContentType on the data being written. func Middleware(level int) goa.Middleware { gzipPool := sync.Pool{ New: func() interface{} { gz, err := gzip.NewWriterLevel(ioutil.Discard, level) if err != nil { panic(err) } return gz }, } return func(h goa.Handler) goa.Handler { return func(ctx context.Context, rw http.ResponseWriter, req *http.Request) (err error) { // Skip compression if the client doesn't accept gzip encoding, is // requesting a WebSocket or the data is already compressed. if !strings.Contains(req.Header.Get(headerAcceptEncoding), encodingGzip) || len(req.Header.Get(headerSecWebSocketKey)) > 0 || req.Header.Get(headerContentEncoding) == encodingGzip { return h(ctx, rw, req) } // Set the appropriate gzip headers. resp := goa.ContextResponse(ctx) resp.Header().Set(headerContentEncoding, encodingGzip) resp.Header().Set(headerVary, headerAcceptEncoding) // Retrieve gzip writer from the pool. Reset it to use the ResponseWriter. // This allows us to re-use an already allocated buffer rather than // allocating a new buffer for every request. gz := gzipPool.Get().(*gzip.Writer) // Get the original http.ResponseWriter w := resp.SwitchWriter(nil) // Reset our gzip writer to use the http.ResponseWriter gz.Reset(w) // Wrap the original http.ResponseWriter with our gzipResponseWriter grw := gzipResponseWriter{ ResponseWriter: w, gzw: gz, } // Set the new http.ResponseWriter resp.SwitchWriter(grw) // Call the next handler supplying the gzipResponseWriter instead of // the original. err = h(ctx, rw, req) if err != nil { return } // Delete the content length after we know we have been written to. grw.Header().Del(headerContentLength) gz.Close() gzipPool.Put(gz) return } } }
// NewCreateBottleContext parses the incoming request URL and body, performs validations and creates the // context used by the bottle controller create action. func NewCreateBottleContext(ctx context.Context, service *goa.Service) (*CreateBottleContext, error) { var err error resp := goa.ContextResponse(ctx) resp.Service = service req := goa.ContextRequest(ctx) rctx := CreateBottleContext{Context: ctx, ResponseData: resp, RequestData: req} return &rctx, err }
// LogRequest creates a request logger middleware. // This middleware is aware of the RequestID middleware and if registered after it leverages the // request ID for logging. // If verbose is true then the middlware logs the request and response bodies. func LogRequest(verbose bool) goa.Middleware { return func(h goa.Handler) goa.Handler { return func(ctx context.Context, rw http.ResponseWriter, req *http.Request) error { reqID := ctx.Value(reqIDKey) if reqID == nil { reqID = shortID() } ctx = goa.WithLogContext(ctx, "req_id", reqID) startedAt := time.Now() r := goa.ContextRequest(ctx) goa.LogInfo(ctx, "started", r.Method, r.URL.String(), "from", from(req), "ctrl", goa.ContextController(ctx), "action", goa.ContextAction(ctx)) if verbose { if len(r.Params) > 0 { logCtx := make([]interface{}, 2*len(r.Params)) i := 0 for k, v := range r.Params { logCtx[i] = k logCtx[i+1] = interface{}(strings.Join(v, ", ")) i = i + 2 } goa.LogInfo(ctx, "params", logCtx...) } if r.ContentLength > 0 { if mp, ok := r.Payload.(map[string]interface{}); ok { logCtx := make([]interface{}, 2*len(mp)) i := 0 for k, v := range mp { logCtx[i] = k logCtx[i+1] = interface{}(v) i = i + 2 } goa.LogInfo(ctx, "payload", logCtx...) } else { // Not the most efficient but this is used for debugging js, err := json.Marshal(r.Payload) if err != nil { js = []byte("<invalid JSON>") } goa.LogInfo(ctx, "payload", "raw", string(js)) } } } err := h(ctx, rw, req) resp := goa.ContextResponse(ctx) if code := resp.ErrorCode; code != "" { goa.LogInfo(ctx, "completed", "status", resp.Status, "error", code, "bytes", resp.Length, "time", time.Since(startedAt).String()) } else { goa.LogInfo(ctx, "completed", "status", resp.Status, "bytes", resp.Length, "time", time.Since(startedAt).String()) } return err } } }
// ErrorHandler turns a Go error into an HTTP response. It should be placed in the middleware chain // below the logger middleware so the logger properly logs the HTTP response. ErrorHandler // understands instances of goa.ServiceError and returns the status and response body embodied in // them, it turns other Go error types into a 500 internal error response. // If verbose is false the details of internal errors is not included in HTTP responses. // If you use github.com/pkg/errors then wrapping the error will allow a trace to be printed to the logs func ErrorHandler(service *goa.Service, verbose bool) goa.Middleware { return func(h goa.Handler) goa.Handler { return func(ctx context.Context, rw http.ResponseWriter, req *http.Request) error { e := h(ctx, rw, req) if e == nil { return nil } cause := errors.Cause(e) status := http.StatusInternalServerError var respBody interface{} if err, ok := cause.(goa.ServiceError); ok { status = err.ResponseStatus() respBody = err goa.ContextResponse(ctx).ErrorCode = err.Token() rw.Header().Set("Content-Type", goa.ErrorMediaIdentifier) } else { respBody = e.Error() rw.Header().Set("Content-Type", "text/plain") } if status >= 500 && status < 600 { reqID := ctx.Value(reqIDKey) if reqID == nil { reqID = shortID() ctx = context.WithValue(ctx, reqIDKey, reqID) } goa.LogError(ctx, "uncaught error", "err", fmt.Sprintf("%+v", e), "id", reqID, "msg", respBody) if !verbose { rw.Header().Set("Content-Type", goa.ErrorMediaIdentifier) msg := fmt.Sprintf("%s [%s]", http.StatusText(http.StatusInternalServerError), reqID) respBody = goa.ErrInternal(msg) // Preserve the ID of the original error as that's what gets logged, the client // received error ID must match the original if origErrID := goa.ContextResponse(ctx).ErrorCode; origErrID != "" { respBody.(*goa.ErrorResponse).ID = origErrID } } } return service.Send(ctx, status, respBody) } } }
// LogResponse creates a response logger middleware. // Only Logs the raw response data without accumulating any statistics. func LogResponse() goa.Middleware { return func(h goa.Handler) goa.Handler { return func(ctx context.Context, rw http.ResponseWriter, req *http.Request) error { // chain a new logging writer to the current response writer. resp := goa.ContextResponse(ctx) resp.SwitchWriter( &loggingResponseWriter{ ResponseWriter: resp.SwitchWriter(nil), ctx: ctx, }) // next return h(ctx, rw, req) } } }
// NewShowBottleContext parses the incoming request URL and body, performs validations and creates the // context used by the bottle controller show action. func NewShowBottleContext(ctx context.Context, service *goa.Service) (*ShowBottleContext, error) { var err error resp := goa.ContextResponse(ctx) resp.Service = service req := goa.ContextRequest(ctx) rctx := ShowBottleContext{Context: ctx, ResponseData: resp, RequestData: req} paramID := req.Params["id"] if len(paramID) > 0 { rawID := paramID[0] if id, err2 := strconv.Atoi(rawID); err2 == nil { rctx.ID = id } else { err = goa.MergeErrors(err, goa.InvalidParamTypeError("id", rawID, "integer")) } } return &rctx, err }
) var _ = Describe("ResponseData", func() { var data *goa.ResponseData var rw http.ResponseWriter var req *http.Request var params url.Values BeforeEach(func() { var err error req, err = http.NewRequest("GET", "google.com", nil) Ω(err).ShouldNot(HaveOccurred()) rw = &TestResponseWriter{Status: 42} params = url.Values{"query": []string{"value"}} ctx := goa.NewContext(context.Background(), rw, req, params) data = goa.ContextResponse(ctx) }) Context("SwitchWriter", func() { var rwo http.ResponseWriter It("sets the response writer and returns the previous one", func() { Ω(rwo).Should(BeNil()) rwo = data.SwitchWriter(&TestResponseWriter{Status: 43}) Ω(rwo).ShouldNot(BeNil()) Ω(rwo).Should(BeAssignableToTypeOf(&TestResponseWriter{})) trw := rwo.(*TestResponseWriter) Ω(trw.Status).Should(Equal(42)) }) }) })
payload := map[string]interface{}{"payload": 42} BeforeEach(func() { var err error req, err = http.NewRequest("POST", "/foo/bar", strings.NewReader(`{"payload":42}`)) req.Header.Set("Accept-Encoding", "gzip") Ω(err).ShouldNot(HaveOccurred()) rw = &TestResponseWriter{ParentHeader: make(http.Header)} ctx = goa.NewContext(nil, rw, req, nil) goa.ContextRequest(ctx).Payload = payload }) It("encodes response using gzip", func() { h := func(ctx context.Context, rw http.ResponseWriter, req *http.Request) error { resp := goa.ContextResponse(ctx) resp.Write([]byte("gzip me!")) resp.WriteHeader(http.StatusOK) return nil } t := gzm.Middleware(gzip.BestCompression)(h) err := t(ctx, rw, req) Ω(err).ShouldNot(HaveOccurred()) resp := goa.ContextResponse(ctx) Ω(resp.Status).Should(Equal(http.StatusOK)) gzr, err := gzip.NewReader(bytes.NewReader(rw.Body)) Ω(err).ShouldNot(HaveOccurred()) buf := bytes.NewBuffer(nil) io.Copy(buf, gzr) Ω(err).ShouldNot(HaveOccurred())
It("matches a header value", func() { req.Header.Set(headerName, "some value") var newCtx context.Context h := func(ctx context.Context, rw http.ResponseWriter, req *http.Request) error { newCtx = ctx return service.Send(ctx, http.StatusOK, "ok") } t := middleware.RequireHeader( service, regexp.MustCompile("^/foo"), headerName, regexp.MustCompile("^some value$"), http.StatusUnauthorized)(h) err := t(ctx, rw, req) Ω(err).ShouldNot(HaveOccurred()) Ω(goa.ContextResponse(newCtx).Status).Should(Equal(http.StatusOK)) }) It("responds with failure on mismatch", func() { req.Header.Set(headerName, "some other value") h := func(ctx context.Context, rw http.ResponseWriter, req *http.Request) error { panic("unreachable") } t := middleware.RequireHeader( service, regexp.MustCompile("^/foo"), headerName, regexp.MustCompile("^some value$"), http.StatusUnauthorized)(h) err := t(ctx, rw, req) Ω(err).ShouldNot(HaveOccurred())
var params url.Values responseText := "some response data to be logged" BeforeEach(func() { logger = new(testLogger) service := newService(logger) var err error req, err = http.NewRequest("POST", "/goo", strings.NewReader(`{"payload":42}`)) Ω(err).ShouldNot(HaveOccurred()) rw = new(testResponseWriter) params = url.Values{"query": []string{"value"}} ctx = newContext(service, rw, req, params) }) It("logs responses", func() { h := func(ctx context.Context, rw http.ResponseWriter, req *http.Request) error { goa.ContextResponse(ctx).WriteHeader(200) goa.ContextResponse(ctx).Write([]byte(responseText)) return nil } lg := middleware.LogResponse()(h) Ω(lg(ctx, rw, req)).ShouldNot(HaveOccurred()) Ω(logger.InfoEntries).Should(HaveLen(1)) Ω(logger.InfoEntries[0].Data).Should(HaveLen(2)) Ω(logger.InfoEntries[0].Data[0]).Should(Equal("body")) Ω(logger.InfoEntries[0].Data[1]).Should(Equal(responseText)) }) })
Context("with a context", func() { var service *goa.Service var req *http.Request var rw http.ResponseWriter var ctx context.Context BeforeEach(func() { service = goa.New("test") ctrl := service.NewController("foo") var err error req, err = http.NewRequest("GET", "/goo", nil) Ω(err).ShouldNot(HaveOccurred()) rw = new(TestResponseWriter) ctx = goa.NewContext(ctrl.Context, rw, req, nil) Ω(goa.ContextResponse(ctx).Status).Should(Equal(0)) }) Context("using a goa handler", func() { BeforeEach(func() { var goaHandler goa.Handler = func(ctx context.Context, rw http.ResponseWriter, req *http.Request) error { service.Send(ctx, 200, "ok") return nil } input = goaHandler }) It("wraps it in a middleware", func() { Ω(mErr).ShouldNot(HaveOccurred()) h := func(ctx context.Context, rw http.ResponseWriter, req *http.Request) error { return nil } Ω(middleware(h)(ctx, rw, req)).ShouldNot(HaveOccurred())