github.com/zak-blake/goa@v1.4.1/middleware/error_handler_test.go (about) 1 package middleware_test 2 3 import ( 4 "bytes" 5 "errors" 6 "fmt" 7 "net/http" 8 "regexp" 9 "strings" 10 11 pErrors "github.com/pkg/errors" 12 13 "context" 14 15 "github.com/goadesign/goa" 16 "github.com/goadesign/goa/middleware" 17 . "github.com/onsi/ginkgo" 18 . "github.com/onsi/gomega" 19 ) 20 21 // errorResponse contains the details of a error response. It implements ServiceError. 22 type errorResponse struct { 23 // ID is the unique error instance identifier. 24 ID string `json:"id" yaml:"id" xml:"id" form:"id"` 25 // Code identifies the class of errors. 26 Code string `json:"code" yaml:"code" xml:"code" form:"code"` 27 // Status is the HTTP status code used by responses that cary the error. 28 Status int `json:"status" yaml:"status" xml:"status" form:"status"` 29 // Detail describes the specific error occurrence. 30 Detail string `json:"detail" yaml:"detail" xml:"detail" form:"detail"` 31 // Meta contains additional key/value pairs useful to clients. 32 Meta map[string]interface{} `json:"meta,omitempty" yaml:"meta,omitempty" xml:"meta,omitempty" form:"meta,omitempty"` 33 } 34 35 // Error returns the error occurrence details. 36 func (e *errorResponse) Error() string { 37 msg := fmt.Sprintf("[%s] %d %s: %s", e.ID, e.Status, e.Code, e.Detail) 38 for k, v := range e.Meta { 39 msg += ", " + fmt.Sprintf("%s: %v", k, v) 40 } 41 return msg 42 } 43 44 var _ = Describe("ErrorHandler", func() { 45 var service *goa.Service 46 var h goa.Handler 47 var verbose bool 48 49 var rw *testResponseWriter 50 51 BeforeEach(func() { 52 service = nil 53 h = nil 54 verbose = true 55 rw = nil 56 }) 57 58 JustBeforeEach(func() { 59 rw = newTestResponseWriter() 60 eh := middleware.ErrorHandler(service, verbose)(h) 61 req, err := http.NewRequest("GET", "/foo", nil) 62 Ω(err).ShouldNot(HaveOccurred()) 63 ctx := newContext(service, rw, req, nil) 64 err = eh(ctx, rw, req) 65 Ω(err).ShouldNot(HaveOccurred()) 66 }) 67 68 Context("with a handler returning a Go error", func() { 69 70 BeforeEach(func() { 71 service = newService(nil) 72 h = func(ctx context.Context, rw http.ResponseWriter, req *http.Request) error { 73 return errors.New("boom") 74 } 75 }) 76 77 It("turns Go errors into HTTP 500 responses", func() { 78 Ω(rw.Status).Should(Equal(500)) 79 Ω(rw.ParentHeader["Content-Type"]).Should(Equal([]string{"text/plain"})) 80 Ω(string(rw.Body)).Should(Equal(`"boom"` + "\n")) 81 }) 82 83 Context("not verbose", func() { 84 BeforeEach(func() { 85 verbose = false 86 }) 87 88 It("hides the error details", func() { 89 var decoded errorResponse 90 Ω(rw.Status).Should(Equal(500)) 91 Ω(rw.ParentHeader["Content-Type"]).Should(Equal([]string{goa.ErrorMediaIdentifier})) 92 err := service.Decoder.Decode(&decoded, bytes.NewBuffer(rw.Body), "application/json") 93 Ω(err).ShouldNot(HaveOccurred()) 94 msg := goa.ErrInternal(`Internal Server Error [zzz]`).Error() 95 msg = regexp.QuoteMeta(msg) 96 msg = strings.Replace(msg, "zzz", ".+", 1) 97 endIDidx := strings.Index(msg, "]") 98 msg = `\[.*\]` + msg[endIDidx+1:] 99 Ω(fmt.Sprintf("%v", decoded.Error())).Should(MatchRegexp(msg)) 100 }) 101 102 Context("and goa 500 error", func() { 103 var origID string 104 105 BeforeEach(func() { 106 h = func(ctx context.Context, rw http.ResponseWriter, req *http.Request) error { 107 e := goa.ErrInternal("goa-500-boom") 108 origID = e.(goa.ServiceError).Token() 109 return e 110 } 111 }) 112 113 It("preserves the error ID from the original error", func() { 114 var decoded errorResponse 115 Ω(origID).ShouldNot(Equal("")) 116 Ω(rw.Status).Should(Equal(500)) 117 Ω(rw.ParentHeader["Content-Type"]).Should(Equal([]string{goa.ErrorMediaIdentifier})) 118 err := service.Decoder.Decode(&decoded, bytes.NewBuffer(rw.Body), "application/json") 119 Ω(err).ShouldNot(HaveOccurred()) 120 Ω(decoded.ID).Should(Equal(origID)) 121 }) 122 }) 123 124 Context("and goa 504 error", func() { 125 BeforeEach(func() { 126 meaningful := goa.NewErrorClass("goa-504-with-info", http.StatusGatewayTimeout) 127 h = func(ctx context.Context, rw http.ResponseWriter, req *http.Request) error { 128 return meaningful("gatekeeper says no") 129 } 130 }) 131 132 It("passes the response", func() { 133 var decoded errorResponse 134 Ω(rw.Status).Should(Equal(http.StatusGatewayTimeout)) 135 Ω(rw.ParentHeader["Content-Type"]).Should(Equal([]string{goa.ErrorMediaIdentifier})) 136 err := service.Decoder.Decode(&decoded, bytes.NewBuffer(rw.Body), "application/json") 137 Ω(err).ShouldNot(HaveOccurred()) 138 Ω(decoded.Code).Should(Equal("goa-504-with-info")) 139 Ω(decoded.Detail).Should(Equal("gatekeeper says no")) 140 }) 141 }) 142 }) 143 }) 144 145 Context("with a handler returning a goa error", func() { 146 var gerr error 147 148 BeforeEach(func() { 149 service = newService(nil) 150 gerr = goa.NewErrorClass("code", 418)("teapot", "foobar", 42) 151 h = func(ctx context.Context, rw http.ResponseWriter, req *http.Request) error { 152 return gerr 153 } 154 }) 155 156 It("maps goa errors to HTTP responses", func() { 157 var decoded errorResponse 158 Ω(rw.Status).Should(Equal(gerr.(goa.ServiceError).ResponseStatus())) 159 Ω(rw.ParentHeader["Content-Type"]).Should(Equal([]string{goa.ErrorMediaIdentifier})) 160 err := service.Decoder.Decode(&decoded, bytes.NewBuffer(rw.Body), "application/json") 161 Ω(err).ShouldNot(HaveOccurred()) 162 Ω(decoded.Error()).Should(Equal(gerr.Error())) 163 }) 164 }) 165 166 Context("with a handler returning a pkg errors wrapped error", func() { 167 var wrappedError error 168 var logger *testLogger 169 verbose = true 170 BeforeEach(func() { 171 logger = new(testLogger) 172 service = newService(logger) 173 wrappedError = pErrors.Wrap(goa.ErrInternal("something crazy happened"), "an error") 174 h = func(ctx context.Context, rw http.ResponseWriter, req *http.Request) error { 175 return wrappedError 176 } 177 }) 178 179 It("maps pkg errors to HTTP responses", func() { 180 var decoded errorResponse 181 cause := pErrors.Cause(wrappedError) 182 Ω(rw.Status).Should(Equal(cause.(goa.ServiceError).ResponseStatus())) 183 Ω(rw.ParentHeader["Content-Type"]).Should(Equal([]string{goa.ErrorMediaIdentifier})) 184 err := service.Decoder.Decode(&decoded, bytes.NewBuffer(rw.Body), "application/json") 185 Ω(err).ShouldNot(HaveOccurred()) 186 Ω(decoded.Error()).Should(Equal(cause.Error())) 187 }) 188 It("logs pkg errors stacktaces", func() { 189 var decoded errorResponse 190 err := service.Decoder.Decode(&decoded, bytes.NewBuffer(rw.Body), "application/json") 191 Ω(err).ShouldNot(HaveOccurred()) 192 Ω(logger.ErrorEntries).Should(HaveLen(1)) 193 data := logger.ErrorEntries[0].Data[1] 194 Ω(data).Should(ContainSubstring("error_handler_test.go")) 195 }) 196 }) 197 })