-
Notifications
You must be signed in to change notification settings - Fork 8k
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
Question: Handling errors #274
Comments
I have something kind of similar. You can handle errors in your own middleware though, after Next() happens. I wouldn't personally use panic like that. error types to feed into c.JSON: my typical error handlers in a controller: |
Thanks for you reply. The way you handle your errors looks better than panicing, also I found, that if I panic I loose the reference to the Error which isn't helpful. I have created an ErrorMessage like yourself: func ErrorMessage(err error, error interface{}) (int, *APIErrors) {
var apiErrors *APIErrors
// This the best way to log?
trace := make([]byte, 1024)
runtime.Stack(trace, true)
log.Printf("ERROR: %s\n%s", err, trace)
switch error.(type) {
case *APIError:
apiError := error.(*APIError)
apiErrors = &APIErrors{
Errors: []*APIError{apiError},
}
case *APIErrors:
apiErrors = error.(*APIErrors)
default:
apiErrors = &APIErrors{
Errors: []*APIError{errUnknown},
}
}
return apiErrors.Status(), apiErrors
} and then call it like such: // Grab the set from storage
set, err := storage.GetSet(set_uuid)
if (err == database.RecordNotFound) {
c.JSON(ErrorMessage(err, errInvalidSet))
return
}
if (err != nil) {
c.JSON(ErrorMessage(err, errDatabase))
return
} This seems much better. Do you think this looks good? Also, what do you think about the way I am logging the error? I want to keep the logging outside of the handler for these kind of errors (to reduce repetition), so I thought logging it when I create ErrorMessage may be the best place, also have the stack trace is helpful for debugging. |
I use a similar, but I suppose more basic approach. For all user-space errors I simply tell the user what's wrong and forget about it. if !c.Bind(&details) {
c.JSON(http.StatusBadRequest, gin.H{"Error": FormDataError.Error()})
return
} If there is an unexpected situation or server error, I also add it to context: if err != nil {
c.Error(err, err.Error())
c.JSON(http.StatusInternalServerError, gin.H{"Error": BraintreeError.Error()})
return
} Which is then picked up by my error handling middleware: func Rollbar() gin.HandlerFunc {
return func(c *gin.Context) {
c.Next()
for _, e := range c.Errors {
rollbar.RequestError(rollbar.ERR, c.Request, errors.New(e.Err))
}
}
} It does need a bit of repetition but for small projects it's more than enough :) |
That looks nice, I was unaware c.Errors worked like that. I am now doing this: // Grab the set from storage
set, err := storage.GetSet(set_uuid)
if (err == database.RecordNotFound) {
c.Error(err, err.Error())
c.JSON(ErrorMessage(errInvalidSet))
return
}
if (err != nil) {
c.Error(err, err.Error())
c.JSON(ErrorMessage(errDatabase))
return
} A bit of repetition but feels like I'm handling the errors better. |
Yea, this is pretty much the same concept. I was thinking about wrapping errors into a struct rather that My main issue is how to condense this: if (err != nil) {
c.Error(err, err.Error())
c.JSON(ErrorMessage(errDatabase))
return
} Into a single line, but I just cant think of a way to set all the variables in one call. Ideal scenario would be something like this: if( err != nil) {
return setErrors(c, err, errDatabase)
}
with
func setErrors(c *gin.Context, internal, public error) bool {
if( internal != nil) {
c.Error(internal, internal.error())
}
c.JSON(ErrorMessage(public))
return true
} but gin handlers don't accept returns, so it's impossible as far as i know to return a result of subfunction in a handler :( So we would still need 1 line to set errors + one line for return, which sucks ballz. |
Yeh that is the same issue I have now. You could do something like this: if (err != nil) {
setErrors(c, err, errDatabase)
return
} but it feels a bit wrong passing the context to that function. Ideally, like you say, it would be nicer if you could return from a handler. |
It may be helpful if there was an additional JSONError func on the context, e.g. func (c *Context) JSONError(err error, code int, obj interface{}) {
if (err != nil) {
c.Error(err, err.Error())
}
c.Render(code, render.JSON, obj)
} that would allow us to do something like c.JSONError(ErrorMessage(err, errDatabase)) |
@elliotlings in my opinion, Gin provides a even better solution! basically c.Error() helps developers to collect errors! it does not abort the request. then, we have c.Fail() just like c.Error(error) but c.Fail(code, error) stops the request. c.Error() can be called many times in the same request. c.Fail() should be called just once. I handle errors like this: func main() {
router := gin.Default()
router.Use(errorHandler)
router.GET("/", func(c *gin.Context) {
c.Fail(500, errors.New("something failed!"))
})
router.Run(":8080")
}
func errorHandler(c *gin.Context) {
c.Next()
if len(c.Errors) {
c.JSON(-1, c.Errors) // -1 == not override the current error code
}
} |
Thanks @manucorporat, that looks really neat. My only issue is that because I am defining errors as variables, e.g. var (
errDatabase = newAPIError(500, "database_error", "Database Error", "An unknown error occurred.", "")
errInvalidSet = newAPIError(404, "invalid_set", "Invalid Set", "The set you requested does not exist.", "")
errInvalidGroup = newAPIError(404, "invalid_group", "Invalid Group", "The group you requested does not exist.", "")
) I have no way of logging the actual error through use of c.Fail. I could, however, always do something like such: c.Error(err, err.Error())
c.Fail(errDatabase.Status, errDatabase) (What would be ideal is a method where I could do something like c.Abort(error, publicError)) |
yes, in fact I will change the API. c.Error will accept an error, not a string any more. |
And some response like that? {
"errors":[{
"id": "internal_server_error",
"status": 500,
"title": "Internal Server Error",
"detail": "Something went wrong."
}]
} or {
"id": "internal_server_error",
"status": 500,
"title": "Internal Server Error",
"detail": "Something went wrong."
} |
I will give you more details soon, I am working to improve the error management. Btw, I posted an issue in Go. golang/go#10748 |
It's definitely a step in the right direction. It's a shame go doesn't allow 'return void' style calls, so even with the above, we are back to square one with 3 lines of code for every error in a handler :D (error + fail + return) |
Yeh, ideally I need a way to pass the error for debugging, but also a public error to pass to the user (e.g. there may be a database error that I need to know about so I can fix it, but at the same time I don't want to expose that to the user). I don't think it should dictate the response format so should just accept an interface like the others. Like I say, something like this would be ideal: func (c *Context) ErrorAndAbort(err error, publicError interface{}) {
c.Error(err, "Operation aborted")
c.PublicError = publicError
c.Abort()
} Then in my application this would allow something like: func errorHandler(c *gin.Context) {
c.Next()
if c.PublicError != nil {
errorMessage := ErrorMessage(c.PublicError)
c.JSON(errorMessage.Status(), errorMessage)
}
} which can be triggered with: c.ErrorAndAbort(err, errDatabase) That way the (type) error gets logged and I can return a nice message to the user, which I can decide the format of. Just a suggestion, not sure what you think? Also, thanks for looking into improving the error handling! :) |
I'm with @elliotlings on this. Even to the point where None of it can be automated too much, as it needs to be up to the user (via middleware) to decide how is that public error then displayed, but a simple separation of internal and public would be super handy. |
And How about a better
And helper function
Use in gin Context:
My
That seem not good idea? |
@nguyenthenguyen @nazwa @elliotlings @techjanitor ok! I just finished a new API for errors. c.Abort() // aborts the handlers chain (no error reported)
c.AbortWithStatus(code) // like Abort() but also writes the headers with the specified status code (no error reported)
c.Error(error) // it appends an error in c.Errors
c.AbortWithError(code, error) // (old c.Fail(): c.AbortWithStatus(code) + c.Error(error) but there are much more improvements under the hoods! c.Error(err) // simple error
c.Error(err).Type(gin.ErrorTypePublic)
c.Error(err).Type(gin.ErrorTypePrivate).Meta("more data") by default c.Error() will store the error as |
Then how to handle the error? easy. you can create a middleware! (Gin will provide a default one) func main() {
router := gin.Default()
router.Use(handleErrors)
// [...]
router.Run(":8080")
}
func handleErrors(c gin.Context) {
c.Next() // execute all the handlers
// at this point, all the handlers finished. Let's read the errors!
// in this example we only will use the **last error typed as public**
// but you could iterate over all them since c.Errors is a slice!
errorToPrint := c.Errors.ByType(gin.ErrorTypePublic).Last()
if errorToPrint != nil {
c.JSON(500, gin.H {
"status": 500,
"message": errorToPrint.Error(),
})
}
} |
Or you guys could use the Meta as the public error. c.Error(err).Meta(gin.H{
"status": "this is bad",
"error": "something really bad happened",
})
// [...]
func handleErrors(c gin.Context) {
c.Next() // execute all the handlers
// at this point, all the handlers finished. Let's read the errors!
// in this example we only will use the **last error typed as public**
// but you could iterate over all them since c.Errors is a slice!
errorToPrint := c.Errors.ByType(gin.ErrorTypePublic).Last()
if errorToPrint != nil && errorToPrint.Meta != nil {
c.JSON(500, errorToPrint.Meta)
}
} |
Nice! Thanks for these new methods. With c.Error will that stop execution or will I need to return void after it? |
@elliotlings Also, retuning void! does not stop the execution, calling return only makes your program jump to the end of the current handler. If you want stop execution (abort). You should explicitly call
If you want to continue the handlers chain. You do not need to call c.Next(). For example, for a latency logger: func latency(c *gin.Context) {
after := time.Now()
c.Next() // the next handlers are execute here
// at this point all the handlers finished
now := time.Now()
latency := now.Sub(after)
fmt.Println(latency)
} |
Would this help me simplify this at all? if (err != nil) {
c.Error(err, err.Error())
c.JSON(ErrorMessage(errDatabase))
return
} err is the internal error which I will log and errDatabase is a public error. ErrorMessage takes an interface and just formats it ready for r.JSON call. func ErrorMessage(error interface{}) (int, *APIErrors) {
var apiErrors *APIErrors
switch error.(type) {
case *APIError:
apiError := error.(*APIError)
apiErrors = &APIErrors{
Errors: []*APIError{apiError},
}
case *APIErrors:
apiErrors = error.(*APIErrors)
default:
apiErrors = &APIErrors{
Errors: []*APIError{errUnknown},
}
}
return apiErrors.Status(), apiErrors
} |
It would be cool if there were 2 more additions, maybe: func (c *Context) ErrorAndAbort(private error, public error) {
c.Error(private).Type(ErrorTypePrivate)
c.Error(public).Type(ErrorTypePublic)
c.Abort()
}
func (c *Context) ErrorAndAbortWithStatus(code int, private error, public error) {
c.Error(private).Type(ErrorTypePrivate)
c.Error(public).Type(ErrorTypePublic)
c.AbortWithStatus(code)
} Which would allow me to do something like this? if (err != nil) {
c.ErrorAndAbort(err, ErrorMessage(errDatabase))
} |
@elliotlings I have the responsibility to create something that matches many people use cases. Maybe you should rethink your error management.
|
looks good to me. c.Error() is a built helper, it is really useful when several things can fail. Remember it is just an array of errors. If you do not need it do not use it. In my case, several things can fail, so we use c.Error(), then we have a errorMiddleware. If there are public errors, we print them. if we only have private errors, we print ref codes: if !isPrivate || r.DebugMode {
return errors.New(report.Message)
} else {
// If the debug mode is disabled and the report is private
// we return error message instead the original one.
// this is very important since there are internal errors that should not be public because of *security reasons*.
return errors.New("internal error. Ref " + report.Id)
} In my case, the error logic is completely separated: func User(c *gin.Context) {
var req models.UserJSON
if c.Bind(&req) {
result, err := MODELS.C(c).UsersByIds(req.Users)
if err == nil {
c.JSON(200, gin.H{"users": result})
}
}
} |
I have a struct with validations: type Transaction struct {
...
Name string `json:"name" binding:"required,max=255"`
...
} I see this in the log if the Name field fails validation:
How can I silence these validation errors? It doesn't seem possible at the moment, and I don't want the log to be filled with validation errors. |
@christianhellsten you are right, validation errors should not be in the logs at all. |
@christianhellsten Done. dde06a0 |
Thank you. That silenced the errors. |
I've got a code where I want to skip error handler with func main() {
router := gin.Default()
router.Use(handleErrors)
router.Post("/test", handler)
router.Run(":8080")
}
func handler(c *gin.Context) {
c.Error(errors.New("testerr"))
// I wan't to skip handleErrors here.
c.Abort()
return
}
func handleErrors(c gin.Context) {
c.Next()
// This shouldn't be called because I use c.Abort in handler above.
errorToPrint := c.Errors.ByType(gin.ErrorTypePublic).Last()
if errorToPrint != nil {
c.JSON(500, gin.H {
"status": 500,
"message": errorToPrint.Error(),
})
}
} |
@joeyave I believe you have to call |
I'm in the process of writing a REST API which returns JSON. I've found my route handlers can get quite cluttered with error handling. I've been looking at ways I can reduce and simply error handling and would appreciate anyone's opinions or advice on what I've come up with.
I'm following the JSON API standard for error returning, http://jsonapi.org/format/#errors, they contain a bit more than the standard Error in Go, so when I initiate an error it can get quite big.
I have created an APIError struct, some helper functions and some default errors:
All of my other funcs in my app return their err but when it get backs to my handler I need to handle how I return it to the client. As far as I'm aware I can't return to middleware so I've added my own Recovery middleware which handles APIError that I panic from my handlers, i.e.
Then in my handler I can do something like this:
which, in my opinion, keeps things really tidy and reduces repetition. I've seen that people recommend you try to use panic as little as possible, but I'm not sure if it works well in this situation? I wonder what people think of this, whether it's a good way to go about it and, if my method isn't great, whether there's any recommended alternatives?
The text was updated successfully, but these errors were encountered: