Go Gorilla panic handler to respond with custom status - go

In Gorilla, using RecoveryHandler we could suppress the panics. However is there a handler or a library method to respond with a specific Http status code and message for given error type.
For example, in case of a Panic for Mandatory field missing error, one would want to respond with Http 400 and a meaningful message of what exactly is wrong with the payload.
What is the recommended approach to do this?
UPDATE
In code: 2 approaches are listed
Handle errors returned at each method call and build the response.
Instead of returning errors, panic with custom error types and defer the error recovery to a func to build the response. This makes the code easy to read and less repetitive.
func fooHandler(w http.ResponseWriter, r *http.Request) {
//decode the request body into a struct instance
if err := decode(r, myInstance); err != nil {
sendErrorResponse(w,err,http.StatusBadRequest)
return
}
//validate the struct instance for all mandatory keys presence
if err := file.validate(); err != nil {
sendErrorResponse(w,err,http.StatusBadRequest)
return
}
//call DB and validate the response and handle the error
//do some computation and again handle error.
//finally construct response
}
func barHandler(w http.ResponseWriter, r *http.Request) {
//similar to above handler many funcs are called before the response is contruscted
}
func tomHandler(w http.ResponseWriter, r *http.Request) {
//similar to above handler many funcs are called before the response is contruscted
}
func differentHandler(w http.ResponseWriter, r *http.Request) {
defer recoverForErrors(w,r)
// call as many funcs as you need.
// validation, decoding etc will panic instead of returning errors.
// This will avoid the repetitive boiler plate code of handling error and converting to meaningful error response
// instead all this logic is pushed to recoverForErrors func. Which retrieves the error from panic and checks for
// specific error type to construct the error http response
}

It is idiomatic to lean on the interfaces provided by the standard library as much as possible. In this case, the http.Handler interface from the net/http package.
In your case, you can create a new type that allows your handlers to return an error type, and handle all of those error cases centrally.
// StatusError wraps an existing error with a HTTP status code.
type StatusError struct {
Status int
// Allows you to wrap another error
Err error
}
func (e *StatusError) Error() string {
return e.Error()
}
type AppHandler func(w http.ResponseWriter, r *http.Request) error
// Satisfies the http.Handler interface
func (ah AppHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Centralises your error handling
err := ah(w, r)
if err != nil {
switch e := a.(type) {
case *StatusError:
switch e.Status {
case 400:
http.Error(w, e.Err.Error(), 400)
return
case 404:
http.NotFound(w, r)
return
default:
http.Error(w, http.StatusText(500), 500)
return
}
default:
http.Error(w, http.StatusText(500), 500)
return
}
}
// Your handlers will look like this
func SomeHandler(w http.ResponseWriter, r *http.Request) error {
err := decode(r, myInstance)
if err != nil {
return &StatusError{400, err}
}
err := file.validate()
if err != nil {
return &StatusError{400, err}
}
// Continue on...
return nil
}
The benefits you get here include:
No panicking for errors that can be handled
You can centralise your error handling in your ServeHTTP method - i.e. for 400 errors, you might write the error reason to the response. For 500 errors, you might return a generic message since a HTTP 500 isn't something the user can be expected to solve.
Your handler functions return errors explicitly, and you no longer need to remember to use naked return statements to avoid continued execution.
Your StatusError type wraps the error with a status code, but still allows you to inspect/log/write out the wrapped error easily.
Further reading:
http://blog.golang.org/error-handling-and-go
http://elithrar.github.io/article/http-handler-error-handling-revisited/
http://dave.cheney.net/2014/12/24/inspecting-errors

Related

Go - errors.As() not recognising type

I am using a helper function to decode JSON. It returns a custom error type that is populated with the reason why it could not parse the JSON and the HTTP code I should return.
package dto
type MalformedRequestError struct {
Status int
Message string
}
func (mr *MalformedRequestError) Error() string {
return mr.Message
}
One of the first things I do when decoding the body is to check that the client has correctly set the Content-Type header.
package webhandlers
func decodeJSONBody(w http.ResponseWriter, r *http.Request, dst interface{}) error {
if r.Header.Get("Content-Type") != "" {
value, _ := header.ParseValueAndParams(r.Header, "Content-Type")
if value != "application/json" {
Message := "Content-Type header is not application/json"
return &dto.MalformedRequestError{Status: http.StatusUnsupportedMediaType, Message: Message}
}
}
... etc ...
I try to use errors.As() to check that the function is returning my custom error, but it is not working.
package webhandlers
func (i *InternalTokenHandler) Post(w http.ResponseWriter, r *http.Request) {
type postRequest struct {
Google_id_token string
}
// parse request data
var parsedRequest postRequest
err := decodeJSONBody(w, r, &parsedRequest)
if err != nil {
// outputs *dto.MalformedRequestError
fmt.Println(fmt.Sprintf("%T", err))
var mr *dto.MalformedRequestError
if errors.As(err, &mr) {
http.Error(w, mr.Message, mr.Status)
} else {
log.Println(err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
return
}
.... more code ...
I have checked that the type of the error is *dto.MalformedRequestError, but my code is always reaching the else block and returning the generic server 500 error.
What am I missing - why is errors.As() not recognizing the error type?
This works: https://go.dev/play/p/CWe9mVp7QOz.
The only reason I can think of that would cause your code to fail, if it really does fail, is that the dto package used by decodeJSONBody is not the same as the dto package used by InternalTokenHandler.Post, hence the two error types would be different.
Note that even different versions of the same package count as different packages and types declared in those are not identical.

Gokit: Validate request/payload in transport layer

I am using go-kit to create an RPC endpoint. I am creating an endpoint like this
httptransport.NewServer(
endPoint.MakeGetBlogEndPoint(blogService),
transport.DecodeGetBlogRequest,
transport.EncodeGetBlogResponse
Below is my DecodeGetBlogRequest function
func DecodeGetBlogRequest(c context.Context, r *http.Request) (interface{}, error) {
vars := mux.Vars(r)
id, err := strconv.Atoi(vars["id"])
if err != nil {
return nil, err
}
req := endPoint.GetBlogRequest{
ID: id,
}
return req, nil
}
What I want to do is validate the HTTP request in this function and if found invalid, send a response with a valid error code from here only, without passing it to the service layer. i.e. If ID is not a valid number, return 400 Bad Request response from here.
But as I don't have a ResponseWriter reference in this function, I am not sure how to do it.
I am following this example from go-kit docs
https://gokit.io/examples/stringsvc.html
Is it a valid assumption that request/payload should be validated in the transport layer only and the service layer should only be called if the request/payload is valid? If yes, how to do so in this example?
You could use ServerErrorEncoder which returns Server options (can be found in github.com/go-kit/kit/transport/server.go).
Basically in your transport layer, apart from the Decode and Encode functions, you can define an YourErrorEncoderFunc() function which could look like the following. This will catch any error thrown in the transport layer.
YourErrorEncoderFunc(_ context.Context, err error, w http.ResponseWriter).
You will need to attach this function as an option in your endpoint registration like:
ABCOpts := []httptransport.ServerOption{
httptransport.ServerErrorEncoder(YourErrorEncoderFunc),
}
r.Methods("GET").Path("/api/v1/abc/def").Handler(httptransport.NewServer(
endpoints.GetDataEndpoint,
DecodeGetRequest,
EncodeGetResponse,
ABCOpts...,
))
This will stop at transport layer if your request validation is invalid and throw and error in the http response based of whatever format you've written in YourErrorEncoderFunc().
Not 100% sure if this applies to go-kit grpc as well:
You have an error return variable. Use that to indicate there was a problem. In the go grpc module there is a status package to return errors with status codes. If you return an error with a status code, the grpc layer will take the code from the error and send it back.
For example:
func DecodeGetBlogRequest(c context.Context, r *http.Request) (interface{}, error) {
vars := mux.Vars(r)
id, err := strconv.Atoi(vars["id"])
if err != nil {
return nil, status.Error(codes.InvalidArgument, err.Error())
}
req := endPoint.GetBlogRequest{
ID: id,
}
return req, nil
}
Note also that grpc uses different status codes. In Go they are located in the codes package.

Can you return json in golang http.Error?

Can you return json when http.Error is called?
myObj := MyObj{
MyVar: myVar}
data, err := json.Marshal(myObj)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Write(data)
w.Header().Set("Content-Type", "application/json")
http.Error(w, "some error happened", http.StatusInternalServerError)
I see that it returns 200 with no json but the json is embed in text
I've discovered that it's really easy to read the Go source. If you click on the function in the docs, you will be taken to the source for the Error function: https://golang.org/src/net/http/server.go?s=61907:61959#L2006
// Error replies to the request with the specified error message and HTTP code.
// It does not otherwise end the request; the caller should ensure no further
// writes are done to w.
// The error message should be plain text.
func Error(w ResponseWriter, error string, code int) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Header().Set("X-Content-Type-Options", "nosniff")
w.WriteHeader(code)
fmt.Fprintln(w, error)
}
So if you want to return JSON, it's easy enough to write your own Error function.
func JSONError(w http.ResponseWriter, err interface{}, code int) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Header().Set("X-Content-Type-Options", "nosniff")
w.WriteHeader(code)
json.NewEncoder(w).Encode(err)
}
It should be plain text only.
From docs
func Error(w ResponseWriter, error string, code int)
Error replies to the request with the specified error message and HTTP
code. It does not otherwise end the request; the caller should ensure
no further writes are done to w. The error message should be plain
text.
Also I think your usage of http.Error is not correct. When you call w.Write(data), the response is sent and response body will be closed. That is why you are getting 200 status instead of 500 from http.Error.
Instead of using http.Error, you can send your own error response with json just like how you would send any other response by setting the status code to an error code.
Like #ShashankV said, you are writing the response in a wrong way.
As an example, the following is what I did during learning about writing RESTful API serving in Golang:
type Response struct {
StatusCode int
Msg string
}
func respond(w http.ResponseWriter, r Response) {
// if r.StatusCode == http.StatusUnauthorized {
// w.Header().Add("WWW-Authenticate", `Basic realm="Authorization Required"`)
// }
data, err := json.Marshal(r)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintf(w, err.Error())
return
}
w.WriteHeader(r.StatusCode)
fmt.Fprintf(w, r.Msg)
}
func Hello(w http.ResponseWriter, r *http.Request) {
resp := Response{http.StatusOK, welcome}
respond(w, resp)
}
Ref: https://github.com/shudipta/Book-Server/blob/master/book_server/book_server.go
Hope, this will help.
My answer is a bit late and there are some good answers already. Here are my 2 cents.
If you want to return JSON in case of error there are multiple ways to do so. I can list two:
Write your own Error handler method
Use the go-boom library
1. Writing your own error handler method
One way is what #craigmj has suggested, i.e. create your own method, for eg.:
func JSONError(w http.ResponseWriter, err interface{}, code int) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Header().Set("X-Content-Type-Options", "nosniff")
w.WriteHeader(code)
json.NewEncoder(w).Encode(err)
}
2. Use the go-boom library
Another approach is using the go-boom library. For eg., in case the err relates to resource not found, you can do:
err := errors.New("User doesn't exist")
...
boom.NotFound(w, err)
And the response will be:
{
"error": "Not Found",
"message": ",
"statusCode": 404
}
For more check the documentation of the go-boom.
Hope that helps.

Returning an error from HandlerFunc - need a new type

Right now I have this:
type AppError struct{
Status int
Message string
}
func (h NearbyHandler) makeUpdate(v NearbyInjection) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
item, ok := v.Nearby[params["id"]]
if !ok {
return AppError{
500, "Missing item in map.",
}
}
}
}
the problem is that if I do this:
func (h NearbyHandler) makeUpdate(v NearbyInjection) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) AppError { // <<< return AppError
}
}
that won't compile b/c http.HandlerFunc doesn't return a function which returns AppError.
Another question I have, how can I avoid explicitly returning nil if if I use AppError as the return value?
Note that I get this error:
cannot use func literal (type func(http.ResponseWriter, *http.Request)
AppError) as type http.HandlerFunc in return argument
So instead of returning the status for the request the designers of go give you the ResponseWriter. This is your main interaction with the client. For example to set a status code, do WriteHeader(500).
that won't compile b/c http.HandlerFunc doesn't return a function which returns AppError.
Why you don't handle error directly in makeUpdate method?
how can I avoid explicitly returning nil if if I use AppError as the return value?
Cannot use 'nil' as type AppError in return argument, you can use the initial value, like this:
func test() AppError {
ret := AppError{
200, "OK",
}
condition := true // some condition
if !condition {
ret.Status = 500
ret.Message = "internal error"
}
return ret
}
It's a server and a handler shouldn't return an error, as it was said you should simply notify the client that an error was encountered while processing the request. Determine the error type and output the corresponding http code and optional message body.
Now, if by chance, your server has some other goroutines that need to be notified of the error you can signal to them through a channel (for metrics or things like that), so you can that way use the error in any way you wish outside the handler scope.

Chaining middleware in net/http golang

I am trying to add context to Authorization middleware. The ContextHandler is a handler which will be passed to api handlers to take care of connections and config variables. A struct Method ServeHTTP also has been added to the ContextHandler so that it satisfies the net/Http interface for handling requests properly.
CheckAuth is the middle ware which takes in the request to check token validation etc, If token is valid, executes the ServeHTTP method and if not, Returns the appropriate error in the response.
Code compiles, but i am getting error in the ServeHTTP method.
type ContextHandler struct {
*AppContext
Handler func(*AppContext, http.ResponseWriter, *http.Request)(int, error)
}
type AppContext struct {
Db *mgo.Session
Config *simplejson.Json
}
func (ah *ContextedHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
status, err := ah.handler(ah.AppContext, w, r)
if err != nil {
switch status {
case http.StatusNotFound:
http.NotFound(w, r)
case http.StatusInternalServerError:
http.Error(w, http.StatusText(status), status)
default:
http.Error(w, http.StatusText(405), 405)
}}}
func CheckAuth(h http.Handler) http.Handler {
log.Println("Entered in CheckAuth")
f := func( w http.ResponseWriter, r *http.Request) {
authorizationToken := r.Header.Get("Authorization")
if authorizationToken != ""{
secret := []byte("somejunk")
var credentials authorization
token, err := jwt.ParseWithClaims(authorizationToken, &credentials, func(t *jwt.Token) (interface{}, error) {
return []byte(secret), nil
})
if err == nil && token.Valid {
//If everything is fine serve the Http request
h.ServeHTTP( w, r)
return
} else {
//Some response returned
json.NewEncoder(w).Encode(response)
return
}
//Check if user exists in the database
if dberr != nil {
//SOmeresponse to be returned
json.NewEncoder(w).Encode(response)
return
}
}else{
response := simplejson.New()
//response authorization header is missing
json.NewEncoder(w).Encode(response)
return
}
}
return http.HandlerFunc(f)
}
func Initdb(configfile *simplejson.Json) *mgo.Session {
//return mongodbsession, copy and close while using it
}
In main.go file in the parent package
func main() {
var FileUploadContextHandler *ContextedHandler = &ContextedHandler{&context, filesystem.FileUpload}
router.Methods("POST").Path("/decentralizefilesystem/fileupload").Name("FileUpload").Handler(CheckAuth(FileUploadContextHandler))
}
I am getting this error
2018/07/08 20:45:38 http: panic serving 127.0.0.1:52732: runtime error: invalid memory address or nil pointer dereference
goroutine 35 [running]:
net/http.(*conn).serve.func1(0xc4202ce140)
/usr/local/go/src/net/http/server.go:1726 +0xd0
panic(0x6fe680, 0x92cb10)
/usr/local/go/src/runtime/panic.go:502 +0x229
gitlab.com/mesha/Gofeynmen/vendor/gopkg.in/mgo%2ev2.(*Session).Copy(0x0, 0x7ff9485fb060)
/home/feynman/goworkspace/src/gitlab.com/mesha/Gofeynmen/vendor/gopkg.in/mgo.v2/session.go:1589 +0x22
gitlab.com/mesha/Gofeynmen/appsettings.CheckAuth.func1(0x7ff9485fb060, 0xc420276300, 0xc4202e4200)
/home/feynman/goworkspace/src/gitlab.com/mesha/Gofeynmen/appsettings/appsettings.go:115 +0x361
net/http.HandlerFunc.ServeHTTP(0xc420290180, 0x7ff9485fb060, 0xc420276300, 0xc4202e4200)
/usr/local/go/src/net/http/server.go:1947 +0x44
github.com/gorilla/mux.(*Router).ServeHTTP(0xc42024a310, 0x7ff9485fb060, 0xc420276300, 0xc4202e4200)
/home/feynman/goworkspace/src/github.com/gorilla/mux/mux.go:162 +0xed
github.com/gorilla/handlers.loggingHandler.ServeHTTP(0x7a8120, 0xc42000e018, 0x7a7b20, 0xc42024a310, 0x7aad60, 0xc4202f0000, 0xc4202e4000)
/home/feynman/goworkspace/src/github.com/gorilla/handlers/handlers.go:69 +0x123
github.com/gorilla/handlers.(*cors).ServeHTTP(0xc4202c4090, 0x7aad60, 0xc4202f0000, 0xc4202e4000)
/home/feynman/goworkspace/src/github.com/gorilla/handlers/cors.go:52 +0xa3b
net/http.serverHandler.ServeHTTP(0xc4202da0d0, 0x7aad60, 0xc4202f0000, 0xc4202e4000)
/usr/local/go/src/net/http/server.go:2694 +0xbc
net/http.(*conn).serve(0xc4202ce140, 0x7ab120, 0xc42025e100)
/usr/local/go/src/net/http/server.go:1830 +0x651
created by net/http.(*Server).Serve
/usr/local/go/src/net/http/server.go:2795 +0x27b
It's likely an attempt to dereference ah from (ah *ContextedHandler), when ah is not a pointer to a ContextedHandler.
The types in this assignment don't match up:
var FileUploadContextHandler *ContextedHandler =
ContextedHandler{&context, filesystem.FileUpload}
On the left side you have type *ContextedHandler. On the right side you have type ContextedHandler.
Did you mean
var FileUploadContextHandler *ContextedHandler =
&ContextedHandler{&context, filesystem.FileUpload}
Or did you mean
var FileUploadContextHandler ContextedHandler =
ContextedHandler{&context, filesystem.FileUpload}
?
The argument passed to the CheckAuth function appears to not match the function signature either:
CheckAuth(FileUploadContextHandler)
FileUploadContextHandler is type *ContextedHandler. The function signature is:
func CheckAuth(h contextHandlerFunc) contextHandlerFunc
The type definition of contextHandlerFunc does not appear to be part of the code you shared.
A problem with this line:
router.Methods("POST").Path("/decentralizefilesystem/fileupload").Name("FileUpload").Handler(CheckAuth(FileUploadContextHandler))
...would be easier to track down if you broke it up into variable assignments on several lines and then figured out which line the panic pointed to.

Resources