Gin: [question] How to handle 401 error and 500 error I couldn't expected?

Created on 1 May 2016  路  8Comments  路  Source: gin-gonic/gin

I've started to use it.
But I wander how to handle unexpected 500 error and 401 error?
I want to show specific error page when something error occured.

Is there something handler?

Most helpful comment

One thing to add to what @im7mortal has already said is that you should _never_ have an _unexpected_ 500. Meaning you should never need to use local defer-recovery.
The reason being is that it creates too many behaviors and will become a nightmare to manage once your app grows.

You should use the c.AbortWithError method in all your handlers. It lets you to set public or private error types

c.AbortWithError(http.StatusUnauthorized, errors.Unauthorised).SetType(gin.ErrorTypePublic)

Which then can be captured in a "generic error handling middleware" that renders the public errors and logs the private ones. This way you have a complete consistency and never have to worry about handlers behaving differently.

And only then, on top of all that, you have a global recovery middleware that catches all _really unexpected_ panics. And these should really really be things that you have no control over, like mysql crashing half way through a request or server running out of memory.


Authentication errors are a perfect example of things that you have a complete control over. Your _generic error handling middleware_ can detect that you have set the response status to unauthorised, and then output the appropriate html or json. This is not something your login handler should be doing.

:)

All 8 comments

There is c.AbortWithError, #342 will be helpful.
You can find AbortWithError(code int, err error) in context.go. (But AbortWithError doesn't print custom error pages.)

And I made custom error pages like this.

func index(c *gin.Context) {
    _, err := SomethingMakesError500()
    if err != nil {
        AbortMsg(500, err, c) // Instead of c.AbortWithError(500, err)
        return
    }
}

func AbortMsg(code int, err error, c *gin.Context) {
    c.String(code, "Oops! Please retry.")
    // A custom error page with HTML templates can be shown by c.HTML()
    c.Error(err)
    c.Abort()
}

Thanks for your nice idea.

That case is OK because something error can be handled in your defined func (index()).

I tried to use default basic auth handler.
But when authorization is failed, execution stop by BasicAuthForRealm() func .
As a result, my defined func indexAction() can't be run even though I set handling code in indexAction().

authorized := r.Group("/admin", gin.BasicAuth(gin.Accounts{
    "web": "test",
}))
authorized.GET("/", admins.IndexAction)

func BasicAuthForRealm(accounts Accounts, realm string) gin.HandlerFunc {
    if realm == "" {
        realm = "Authorization Required"
    }
    realm = "Basic realm=" + strconv.Quote(realm)
    pairs := processAccounts(accounts)
    return func(c *gin.Context) {
        // Search user in the slice of allowed credentials
        user, found := pairs.searchCredential(c.Request.Header.Get("Authorization"))
        if !found {
            // Credentials doesn't match, we return 401 and abort handlers chain.
            c.Header("WWW-Authenticate", realm)
            c.AbortWithStatus(401)
            //TODO:return specific error html
            c.HTML(http.StatusUnauthorized, "errors/error.tmpl", gin.H{
                "message": "401 errors",
            })

        } else {
            // The user credentials was found, set user's id to key AuthUserKey in this context, the userId can be read later using
            // c.MustGet(gin.AuthUserKey)
            c.Set(AuthUserKey, user)
        }
    }
}

I added code to show I wish.
After c.AbortWithStatus(401), I want to return html or handle error by func I defined.

//TODO:return specific error html
c.HTML(http.StatusUnauthorized, "errors/error.tmpl", gin.H{
    "message": "401 errors",
})

@hiromaily

500

I assume that a _unexpected_ error mean that you can't catch it via returned _err_.

There are two ways for handling it: locally or globally.
Both approach are based on defer-recovery statement. Check go documentation

Globally

Check a difference between _gin.Default()_ and _gin.New()_
_gin.New()_ return a router without default recover function and a logger.

func main() {
    router := gin.New()
    router.Use(gin.Logger) // if you want to use standard logger anyway
    router.Use(globalRecover)
    // ...
}

func globalRecover(c *gin.Context) {
    defer func(c *gin.Context) {
        if rec := recover(); rec != nil {
            // that recovery also handle XHR's
            // you need handle it
            if XHR(c) {
                c.JSON(http.StatusInternalServerError, gin.H{
                    "error": rec,
                })
            } else {
                c.HTML(http.StatusOK, "500", gin.H{})
            }
        }
    }(c)
    c.Next()
}

func XHR(c *gin.Context) bool {
    return strings.ToLower(c.Request.Header.Get("X-Requested-With")) == "xmlhttprequest"
}

Locally

Just use defer-recovery statement. Don't forget use different response for XHR's.

func someHandler(c *gin.Context) {
    defer func(c *gin.Context) {
        if rec := recover(); rec != nil {
            c.HTML(http.StatusOK, "500", gin.H{})
        }
    }(c)
    // ...
}

One thing to add to what @im7mortal has already said is that you should _never_ have an _unexpected_ 500. Meaning you should never need to use local defer-recovery.
The reason being is that it creates too many behaviors and will become a nightmare to manage once your app grows.

You should use the c.AbortWithError method in all your handlers. It lets you to set public or private error types

c.AbortWithError(http.StatusUnauthorized, errors.Unauthorised).SetType(gin.ErrorTypePublic)

Which then can be captured in a "generic error handling middleware" that renders the public errors and logs the private ones. This way you have a complete consistency and never have to worry about handlers behaving differently.

And only then, on top of all that, you have a global recovery middleware that catches all _really unexpected_ panics. And these should really really be things that you have no control over, like mysql crashing half way through a request or server running out of memory.


Authentication errors are a perfect example of things that you have a complete control over. Your _generic error handling middleware_ can detect that you have set the response status to unauthorised, and then output the appropriate html or json. This is not something your login handler should be doing.

:)

@nazwa

500

You told about a best practice. In the reality we have something like

func someHandler(c *gin.Context) {
    // special handling
    defer func(c *gin.Context) {
        if rec := recover(); rec != nil {
            c.AbortWithError(http.StatusUnauthorized, errors.Unauthorised).SetType(gin.ErrorTypePublic)
            // ...
        }
    }(c)
    // just assume we don't have alternatives and has error here in some cases
    value := blackBox.FuncWithoutErrorHandling()

    // ...
}

When panic occurs, it stops the ordinary flow.

Panic is a built-in function that stops the ordinary flow of control and begins panicking. When the function F calls panic, execution of F stops, any deferred functions in F are executed normally, and then F returns to its caller. To the caller, F then behaves like a call to panic. The process continues up the stack until all functions in the current goroutine have returned, at which point the program crashes. Panics can be initiated by invoking panic directly. They can also be caused by runtime errors, such as out-of-bounds array accesses.

Check go documentation

Of course you can use panics for that - that's the beauty of Go. I'm just saying it gets very messy very quickly and I suggest against it. Imagine Having 50 handlers, each with its own recovery, error handling and error page rendering... Your choice :)

500

@nazwa
@hiromaily asked about unexpected 500 error (exception).
In my example I wanted show that if we assumed that error is unexpected
_For example_: we have some hardcoded library with C bindings maybe with unsafe pkg which was written by a messy codder - blackBox. And we don't have alternatives.
Often packages have uncovered exceptions.
We must use a exception handling in this case if we want a reliable code. If you will use only _c.AbortWithError_ :bangbang:goorutine will be crashed:zap::bangbang:.
Like alternative we can wrap unsafe function

func someHandler(c *gin.Context) {

    value, err := wrapUnsafe()
    if err != nil {
        c.AbortWithError(http.StatusUnauthorized, errors.Unauthorised).SetType(gin.ErrorTypePublic)
        // ...
        return // obligatory
    }
    // ...
}

func wrapUnsafe() (value Type, err error) {
    defer func(c *gin.Context) {
        if rec := recover(); rec != nil {
            err = errors.New("some error")
            // ...
        }
    }(c)
    // just assume we don't have alternatives and has error here in some cases
    value := blackBox.FuncWithoutErrorHandling()
}

Check simple examples: without handling and with

Please check go documentation about defer, panic, and recover

I like any best practice. And I am fully agree with you. But the question wasn't about it. :smiley:

@im7mortal ,@nazwa
Thank you for your help. I could understand!!

Was this page helpful?
0 / 5 - 0 ratings