Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/custom errors #2

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand Down
25 changes: 23 additions & 2 deletions rest/response.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)
}
Expand Down
61 changes: 60 additions & 1 deletion rest/response_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand All @@ -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.

Expand Down Expand Up @@ -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()
Expand All @@ -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
}