Skip to content

Commit 37854ee

Browse files
justinfxthinkerou
authored andcommitted
Fix panic stack trace being printed during recovery of broken pipe (#1089) (#1259)
1 parent 66b47a8 commit 37854ee

File tree

2 files changed

+69
-5
lines changed

2 files changed

+69
-5
lines changed

recovery.go

+29-5
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,12 @@ import (
1010
"io"
1111
"io/ioutil"
1212
"log"
13+
"net"
1314
"net/http"
1415
"net/http/httputil"
16+
"os"
1517
"runtime"
18+
"syscall"
1619
"time"
1720
)
1821

@@ -37,16 +40,37 @@ func RecoveryWithWriter(out io.Writer) HandlerFunc {
3740
return func(c *Context) {
3841
defer func() {
3942
if err := recover(); err != nil {
43+
// Check for a broken connection, as it is not really a
44+
// condition that warrants a panic stack trace.
45+
var brokenPipe bool
46+
if ne, ok := err.(*net.OpError); ok {
47+
if se, ok := ne.Err.(*os.SyscallError); ok {
48+
if se.Err == syscall.EPIPE || se.Err == syscall.ECONNRESET {
49+
brokenPipe = true
50+
}
51+
}
52+
}
4053
if logger != nil {
4154
stack := stack(3)
42-
if IsDebugging() {
43-
httprequest, _ := httputil.DumpRequest(c.Request, false)
44-
logger.Printf("[Recovery] %s panic recovered:\n%s\n%s\n%s%s", timeFormat(time.Now()), string(httprequest), err, stack, reset)
55+
httprequest, _ := httputil.DumpRequest(c.Request, false)
56+
if brokenPipe {
57+
logger.Printf("%s\n%s%s", err, string(httprequest), reset)
58+
} else if IsDebugging() {
59+
logger.Printf("[Recovery] %s panic recovered:\n%s\n%s\n%s%s",
60+
timeFormat(time.Now()), string(httprequest), err, stack, reset)
4561
} else {
46-
logger.Printf("[Recovery] %s panic recovered:\n%s\n%s%s", timeFormat(time.Now()), err, stack, reset)
62+
logger.Printf("[Recovery] %s panic recovered:\n%s\n%s%s",
63+
timeFormat(time.Now()), err, stack, reset)
4764
}
4865
}
49-
c.AbortWithStatus(http.StatusInternalServerError)
66+
67+
// If the connection is dead, we can't write a status to it.
68+
if brokenPipe {
69+
c.Error(err.(error))
70+
c.Abort()
71+
} else {
72+
c.AbortWithStatus(http.StatusInternalServerError)
73+
}
5074
}
5175
}()
5276
c.Next()

recovery_test.go

+40
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,16 @@
22
// Use of this source code is governed by a MIT style
33
// license that can be found in the LICENSE file.
44

5+
// +build go1.7
6+
57
package gin
68

79
import (
810
"bytes"
11+
"net"
912
"net/http"
13+
"os"
14+
"syscall"
1015
"testing"
1116

1217
"github.com/stretchr/testify/assert"
@@ -72,3 +77,38 @@ func TestFunction(t *testing.T) {
7277
bs := function(1)
7378
assert.Equal(t, []byte("???"), bs)
7479
}
80+
81+
// TestPanicWithBrokenPipe asserts that recovery specifically handles
82+
// writing responses to broken pipes
83+
func TestPanicWithBrokenPipe(t *testing.T) {
84+
const expectCode = 204
85+
86+
expectMsgs := map[syscall.Errno]string{
87+
syscall.EPIPE: "broken pipe",
88+
syscall.ECONNRESET: "connection reset",
89+
}
90+
91+
for errno, expectMsg := range expectMsgs {
92+
t.Run(expectMsg, func(t *testing.T) {
93+
94+
var buf bytes.Buffer
95+
96+
router := New()
97+
router.Use(RecoveryWithWriter(&buf))
98+
router.GET("/recovery", func(c *Context) {
99+
// Start writing response
100+
c.Header("X-Test", "Value")
101+
c.Status(expectCode)
102+
103+
// Oops. Client connection closed
104+
e := &net.OpError{Err: &os.SyscallError{Err: errno}}
105+
panic(e)
106+
})
107+
// RUN
108+
w := performRequest(router, "GET", "/recovery")
109+
// TEST
110+
assert.Equal(t, expectCode, w.Code)
111+
assert.Contains(t, buf.String(), expectMsg)
112+
})
113+
}
114+
}

0 commit comments

Comments
 (0)