recovery.go 1.3 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364
  1. package middleware
  2. import (
  3. "errors"
  4. "net"
  5. "net/http"
  6. "os"
  7. "strings"
  8. infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
  9. "github.com/Wei-Shaw/sub2api/internal/pkg/response"
  10. "github.com/gin-gonic/gin"
  11. )
  12. // Recovery converts panics into the project's standard JSON error envelope.
  13. //
  14. // It preserves Gin's broken-pipe handling by not attempting to write a response
  15. // when the client connection is already gone.
  16. func Recovery() gin.HandlerFunc {
  17. return gin.CustomRecoveryWithWriter(gin.DefaultErrorWriter, func(c *gin.Context, recovered any) {
  18. recoveredErr, _ := recovered.(error)
  19. if isBrokenPipe(recoveredErr) {
  20. if recoveredErr != nil {
  21. _ = c.Error(recoveredErr)
  22. }
  23. c.Abort()
  24. return
  25. }
  26. if c.Writer.Written() {
  27. c.Abort()
  28. return
  29. }
  30. response.ErrorWithDetails(
  31. c,
  32. http.StatusInternalServerError,
  33. infraerrors.UnknownMessage,
  34. infraerrors.UnknownReason,
  35. nil,
  36. )
  37. c.Abort()
  38. })
  39. }
  40. func isBrokenPipe(err error) bool {
  41. if err == nil {
  42. return false
  43. }
  44. var opErr *net.OpError
  45. if !errors.As(err, &opErr) {
  46. return false
  47. }
  48. var syscallErr *os.SyscallError
  49. if !errors.As(opErr.Err, &syscallErr) {
  50. return false
  51. }
  52. msg := strings.ToLower(syscallErr.Error())
  53. return strings.Contains(msg, "broken pipe") || strings.Contains(msg, "connection reset by peer")
  54. }