diff --git a/README.md b/README.md index b57663d..a89a744 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ - [Force HTTPS](#forcessl) - [Status](#status) - [Status Auth](#status-auth) + - [Custom Error Responses](#custom-error-responses) - [Advanced](#advanced) - [JWT](#jwt) - [Streaming](#streaming) @@ -444,6 +445,79 @@ func (u *Users) DeleteUser(w rest.ResponseWriter, r *rest.Request) { ``` +#### Custom Error Responses + +Demonstrate how to send custom error responses in go-json-rest + +curl demo: +```sh +curl -i http://127.0.0.1:8080/square/8675309 +``` + + +code: +``` go +package main + +import ( + "./rest" + "log" + "net/http" + "strconv" +) + +func MyCustomError(r *rest.Request, error string, code int) interface{} { + var header string + switch code { + case 400: + header = "Bad Input" + break + default: + header = "API Error" + } + + // do whatever needed with the caught error + go log.Println("Error from", r.RemoteAddr, r.Method, r.URL) + + return map[string]interface{}{ + "error": map[string]interface{}{ + "header": header, + "code": code, + "message": error, + }, + } +} + +func main() { + api := rest.NewApi() + api.Use(rest.DefaultDevStack...) + router, err := rest.MakeRouter( + rest.Get("/square/:number", Square), + ) + if err != nil { + log.Fatal(err) + } + + api.SetApp(router) + rest.ErrorFunc = MyCustomError + + log.Fatal(http.ListenAndServe(":8081", api.MakeHandler())) +} + +func Square(w rest.ResponseWriter, r *rest.Request) { + // parse 8-bit signed decimal + // -128 <= n <= 127 + n, err := strconv.ParseInt(r.PathParam("number"), 10, 8) + if err != nil { + rest.ErrorWithRequest(r, w, err.Error(), http.StatusBadRequest) + return + } + + w.WriteJson(n * n) +} + +``` + ### Applications Common use cases, found in many applications. diff --git a/rest/response.go b/rest/response.go index 52529f1..e1124a1 100644 --- a/rest/response.go +++ b/rest/response.go @@ -11,7 +11,6 @@ import ( // Note, the responseWriter object instantiated by the framework also implements many other interfaces // accessible by type assertion: http.ResponseWriter, http.Flusher, http.CloseNotifier, http.Hijacker. type ResponseWriter interface { - // Identical to the http.ResponseWriter interface Header() http.Header @@ -34,12 +33,34 @@ type ResponseWriter interface { // eg: rest.ErrorFieldName = "errorMessage" var ErrorFieldName = "Error" +// This allows to customize the error messages used in the error response payload. +// It defaults to the ErrorFieldName method for compatibility reasons (only recieves error string and http code), +// but can be changed before starting the server. +// +// Sends a json payload of the struct given. Be sure to define the json keys in the struct. +// eg: rest.CustomErrorStruct = &MyCustomStruct{} +var ErrorFunc func(*Request, string, int) interface{} + // Error produces an error response in JSON with the following structure, '{"Error":"My error message"}' // The standard plain text net/http Error helper can still be called like this: // http.Error(w, "error message", code) func Error(w ResponseWriter, error string, code int) { + // Call new method to support backwards compat + ErrorWithRequest(nil, w, error, code) +} + +// Error produces an error response in JSON with context of the request +func ErrorWithRequest(r *Request, w ResponseWriter, error string, code int) { w.WriteHeader(code) - err := w.WriteJson(map[string]string{ErrorFieldName: error}) + + var errPayload interface{} + if ErrorFunc != nil { + errPayload = ErrorFunc(r, error, code) + } else { + errPayload = map[string]string{ErrorFieldName: error} + } + + err := w.WriteJson(errPayload) if err != nil { panic(err) } diff --git a/rest/response_test.go b/rest/response_test.go index ba13f38..ac072e7 100644 --- a/rest/response_test.go +++ b/rest/response_test.go @@ -6,6 +6,29 @@ import ( "github.com/ant0ine/go-json-rest/rest/test" ) +func CustomError(r *Request, error string, code int) interface{} { + // r = nil when using test requests + var header string + switch code { + case 400: + header = "Bad Input" + break + case 404: + header = "Not Found" + break + default: + header = "API Error" + } + + return map[string]interface{}{ + "error": map[string]interface{}{ + "header": header, + "code": code, + "message": error, + }, + } +} + func TestResponseNotIndent(t *testing.T) { writer := responseWriter{ @@ -24,7 +47,7 @@ func TestResponseNotIndent(t *testing.T) { } } -// The following tests could instantiate only the reponseWriter, +// The following tests could instantiate only the responseWriter, // but using the Api object allows to use the rest/test utilities, // and make the tests easier to write. @@ -54,6 +77,24 @@ func TestErrorResponse(t *testing.T) { recorded.BodyIs("{\"Error\":\"test\"}") } +func TestCustomErrorResponse(t *testing.T) { + + api := NewApi() + ErrorFunc = CustomError + + api.SetApp(AppSimple(func(w ResponseWriter, r *Request) { + Error(w, "test", 500) + })) + + recorded := test.RunRequest(t, api.MakeHandler(), test.MakeSimpleRequest("GET", "http://localhost/", nil)) + recorded.CodeIs(500) + recorded.ContentTypeIsJson() + recorded.BodyIs(`{"error":{"code":500,"header":"API Error","message":"test"}}`) + + // reset the package variable to not effect other tests + ErrorFunc = nil +} + func TestNotFoundResponse(t *testing.T) { api := NewApi() @@ -66,3 +107,21 @@ func TestNotFoundResponse(t *testing.T) { recorded.ContentTypeIsJson() recorded.BodyIs("{\"Error\":\"Resource not found\"}") } + +func TestCustomNotFoundResponse(t *testing.T) { + + api := NewApi() + ErrorFunc = CustomError + + api.SetApp(AppSimple(func(w ResponseWriter, r *Request) { + NotFound(w, r) + })) + + recorded := test.RunRequest(t, api.MakeHandler(), test.MakeSimpleRequest("GET", "http://localhost/", nil)) + recorded.CodeIs(404) + recorded.ContentTypeIsJson() + recorded.BodyIs(`{"error":{"code":404,"header":"Not Found","message":"Resource not found"}}`) + + // reset the package variable to not effect other tests + ErrorFunc = nil +}