preface

When I wrote the service, I used a uniform format, and all the HTTP status codes were 200. However, during panic, Gin’s Recovery() middleware returned 500, which made my return results inconsistent, so I decided to write a Recovery() myself. How is Gin Recovery() implemented

A piece of code that can panic

First to a panic code, here are just inside the handler to write a line only panic (” I panic! “), after the operation to http://localhost:8080/panic will find back to 500, And the console output indicates that Gin Recovery() middleware caught the Panic() and responded.

package main

import "github.com/gin-gonic/gin"

func main(a) {
   r := gin.New()
   r.Use(gin.Logger(), gin.Recovery())
   r.GET("panic".func(c *gin.Context) {
      panic("I panic.")
   })
   r.Run()
}
Copy the code

A preliminary Gin Recovery ()

Recover () the source code

Gin Recovery() calls RecoveryWithWriter(out IO.Writer, RecoveryWithWriter… RecoveryFunc) HandlerFunc, RecoveryWithWriter(out IO.Writer, recovery… RecoveryFunc) HandlerFunc, RecoveryWithWriter(out IO. RecoveryFunc) HandlerFunc in turn calls CustomRecoveryWithWriter(out IO.Writer, handle RecoveryFunc) HandlerFunc

You can see the core code for CustomRecoveryWithWriter() as follows:

func CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) HandlerFunc {
   // Log
   return func(c *Context) {
      defer func(a) { 
         if err := recover(a); err ! =nil { // Used to catch panic
            / / panic
         }
      }()
      c.Next() // Invoke the next processing}}Copy the code

The above code strips out the logging and panic handling, leaving only how Recovery() captures panic’s code.

The captured process is simple, just defer before calling the next service, and then c.ext (), so that as long as the corresponding handler panic of C.ext () is caught by recover() of defer.

The custom Recovery ()

Knowing how Gin caught panic, we can also write Recovery() :

func Recovery(a) func(c *gin.Context) {
   return func(c *gin.Context) {
      defer func(a) { 
         if err := recover(a); err ! =nil { // Used to catch panic
            c.String(http.StatusOK, "I caught Panic:" + err.(string) <-- custompanic}}() c.next ()// Invoke the next processing}}func main(a) {
   r := gin.New()
   r.Use(gin.Logger(), Recovery()) // <-- use custom Recovery() instead of gin.Recovery()
   r.GET("panic".func(c *gin.Context) {
      panic("I panic.")
   })
   r.Run()
}
Copy the code

Visit http://localhost:8080/panic can see again now returned to the us custom response.

Customize Recovery using Gin CustomRecovery

RecoveryFunc HandlerFunc CustomRecovery(Handle RecoveryFunc) HandlerFunc The only difference between it and Recovery() is that it can customize how panic’s handle function is handled. Therefore, Gin can reuse other Recovery logic using it.

func Recovery(a) func(c *gin.Context) {
   return gin.CustomRecovery(func(c *gin.Context, err interface{}) {
      c.String(http.StatusOK, "I caught Panic:" + err.(string) <-- custompanic})}Copy the code

As you can see, we no longer need to catch panic ourselves, but just define how to handle it.

Studied the Gin Recovery ()

We have overlooked all the details of Gin Recovery(), now let’s take a closer look.

DefaultErrorWriter

RecoveryWithWriter(out IO.Writer, recovery… RecoveryFunc) The first argument to HandlerFunc is an output stream. Recovery() defaults to terrorWriter, which is the standard error output stream os.stderr. Those red logs you saw on the screen during Panic.

defaultHandleRecovery

When using CustomRecovery, we will customize handle panic, and Recovery() uses defaultHandleRecovery by default. The code for defaultHandleRecovery is very simple as follows:

func defaultHandleRecovery(c *Context, err interface{}) {
   c.AbortWithStatus(http.StatusInternalServerError)
}
Copy the code

It also sets the status code of 500, and then give up continue to process, also is our first see when they visit http://localhost:8080/panic 500 response

Defer processing logic

The following is the processing code after defer received panic. You can see that CustomRecoveryWithWriter(out IO.Writer, handle RecoveryFunc) HandlerFunc does not call handle() immediately. Instead, you define a brokenPipe variable that is used to determine whether the network connection is broken

If the brokenPipe condition is false, that is, the connection is normal, then handle() is called directly to handle the response

If the brokenPipe condition is true, the connection is broken, then only a call to c.error (err.(error)) logs the error, and then a call to c.abort () aborts processing of the request

// func CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) HandlerFunc

defer func(a) {
   if err := recover(a); err ! =nil {
      var brokenPipe bool <-- Determines whether the network connection is disconnected
      if ne, ok := err.(*net.OpError); ok { // <-- Set this to brokenPipe
         if se, ok := ne.Err.(*os.SyscallError); ok {
            if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {
               brokenPipe = true}}}// Log code ignored
     
      if brokenPipe { // <-- if the connection has been broken, we simply log and interrupt processing
         c.Error(err.(error))
         c.Abort()
      } else {
         handle(c, err)
      }
   }
}()
Copy the code