Skip to content

Commit e9a4588

Browse files
authored
Mark panics in log with -500 code; also return 500 to caller if response hasn't been started yet (#70)
* Mark panics in log with -500 code; also return 500 to caller if response hasn't been started yet * Fix the test: needed a smarter NullWriter and make the panic in the handler and not the writer * Switch type for level to int8; silence gosec
1 parent 2fef4e4 commit e9a4588

File tree

3 files changed

+47
-16
lines changed

3 files changed

+47
-16
lines changed

http_logging.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,14 +184,18 @@ func (rr *ResponseRecorder) Flush() {
184184
func LogAndCall(msg string, handlerFunc http.HandlerFunc, extraAttributes ...KeyVal) http.HandlerFunc {
185185
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
186186
// This is really 2 functions but we want to be able to change config without rewiring the middleware
187-
if Config.CombineRequestAndResponse {
187+
if Config.CombineRequestAndResponse { //nolint:nestif // see above comment.
188188
respRec := &ResponseRecorder{w: w, startTime: time.Now()}
189189
defer func() {
190190
if err := recover(); err != nil {
191191
s(Critical, false, Config.JSON, "panic in handler", Any("error", err))
192192
if Log(Verbose) {
193193
s(Verbose, false, Config.JSON, "stack trace", Str("stack", string(debug.Stack())))
194194
}
195+
respRec.StatusCode = -500 // Marking as a panic for the log.
196+
if respRec.ContentLength == 0 { // Nothing was written yet so we can write an error
197+
http.Error(w, fmt.Sprintf("Internal Server Error\n%s", err), http.StatusInternalServerError)
198+
}
195199
}
196200
attr := []KeyVal{
197201
Int("status", respRec.StatusCode),

http_logging_test.go

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -46,26 +46,32 @@ func TestLogRequest(t *testing.T) {
4646
}
4747

4848
func testHandler(w http.ResponseWriter, r *http.Request) {
49+
if r.URL != nil && r.URL.Path == "/panicbefore" {
50+
panic("some test handler panic before response")
51+
}
4952
if r.URL != nil && r.URL.Path == "/tea" {
5053
w.WriteHeader(http.StatusTeapot)
5154
}
5255
w.Write([]byte("hello"))
5356
time.Sleep(100 * time.Millisecond)
57+
if r.URL != nil && r.URL.Path == "/panicafter" {
58+
panic("some test handler panic after response")
59+
}
5460
}
5561

5662
type NullHTTPWriter struct {
5763
doErr bool
58-
doPanic bool
64+
headers http.Header
5965
}
6066

6167
func (n *NullHTTPWriter) Header() http.Header {
62-
return nil
68+
if n.headers == nil {
69+
n.headers = make(http.Header)
70+
}
71+
return n.headers
6372
}
6473

6574
func (n *NullHTTPWriter) Write(b []byte) (int, error) {
66-
if n.doPanic {
67-
panic("some fake http write panic")
68-
}
6975
if n.doErr {
7076
return 0, errors.New("some fake http write error")
7177
}
@@ -100,7 +106,7 @@ func TestLogAndCall(t *testing.T) {
100106
if !strings.HasPrefix(actual, expectedPrefix) {
101107
t.Errorf("unexpected:\n%s\nvs should start with:\n%s\n", actual, expectedPrefix)
102108
}
103-
if hw.Header() != nil {
109+
if len(hw.Header()) != 0 {
104110
t.Errorf("unexpected non nil header: %v", hw.Header())
105111
}
106112
hr.URL = &url.URL{Path: "/tea"}
@@ -123,20 +129,33 @@ func TestLogAndCall(t *testing.T) {
123129
if !strings.Contains(actual, expectedFragment) {
124130
t.Errorf("unexpected:\n%s\nvs should contain error:\n%s\n", actual, expectedFragment)
125131
}
126-
n.doPanic = true
132+
hr.URL = &url.URL{Path: "/panicafter"}
127133
n.doErr = false
128134
SetLogLevelQuiet(Verbose)
129135
b.Reset()
130136
LogAndCall("test-log-and-call4", testHandler).ServeHTTP(hw, hr)
131137
w.Flush()
132138
actual = b.String()
133-
expectedFragment = `,"size":0,`
139+
expectedFragment = `"status":-500,`
140+
Config.GoroutineID = false
141+
if !strings.Contains(actual, expectedFragment) {
142+
t.Errorf("unexpected:\n%s\nvs should contain error:\n%s\n", actual, expectedFragment)
143+
}
144+
if !strings.Contains(actual, `{"level":"crit","msg":"panic in handler","error":"some test handler panic after response"}`) {
145+
t.Errorf("unexpected:\n%s\nvs should contain error:\n%s\n", actual, "some test handler panic after response")
146+
}
147+
hr.URL = &url.URL{Path: "/panicbefore"}
148+
b.Reset()
149+
LogAndCall("test-log-and-call5", testHandler).ServeHTTP(hw, hr)
150+
w.Flush()
151+
actual = b.String()
152+
expectedFragment = `"status":-500,`
134153
Config.GoroutineID = false
135154
if !strings.Contains(actual, expectedFragment) {
136155
t.Errorf("unexpected:\n%s\nvs should contain error:\n%s\n", actual, expectedFragment)
137156
}
138-
if !strings.Contains(actual, `{"level":"crit","msg":"panic in handler","error":"some fake http write panic"`) {
139-
t.Errorf("unexpected:\n%s\nvs should contain error:\n%s\n", actual, "some fake http write panic")
157+
if !strings.Contains(actual, `{"level":"crit","msg":"panic in handler","error":"some test handler panic before response"}`) {
158+
t.Errorf("unexpected:\n%s\nvs should contain error:\n%s\n", actual, "some test handler panic before response")
140159
}
141160
// restore for other tests
142161
Config.GoroutineID = true

logger.go

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ import (
4343
)
4444

4545
// Level is the level of logging (0 Debug -> 6 Fatal).
46-
type Level int32
46+
type Level int8
4747

4848
// Log levels. Go can't have variable and function of the same name so we keep
4949
// medium length (Dbg,Info,Warn,Err,Crit,Fatal) names for the functions.
@@ -180,6 +180,13 @@ func (l *JSONEntry) Time() time.Time {
180180
)
181181
}
182182

183+
func intToLevel(i int) Level {
184+
if i < 0 || i >= len(LevelToStrA) {
185+
return -1
186+
}
187+
return Level(i) //nolint:gosec // we just checked above.
188+
}
189+
183190
//nolint:gochecknoinits // needed
184191
func init() {
185192
if !isValid(os.Stderr) { // wasm in browser case for instance
@@ -190,12 +197,13 @@ func init() {
190197
JSONStringLevelToLevel = make(map[string]Level, len(LevelToJSON)-1) // -1 to not reverse info to NoLevel
191198
for l, name := range LevelToStrA {
192199
// Allow both -loglevel Verbose and -loglevel verbose ...
193-
levelToStrM[name] = Level(l)
194-
levelToStrM[strings.ToLower(name)] = Level(l)
200+
lvl := intToLevel(l)
201+
levelToStrM[name] = lvl
202+
levelToStrM[strings.ToLower(name)] = lvl
195203
}
196204
for l, name := range LevelToJSON[0 : Fatal+1] { // Skip NoLevel
197205
// strip the quotes around
198-
JSONStringLevelToLevel[name[1:len(name)-1]] = Level(l)
206+
JSONStringLevelToLevel[name[1:len(name)-1]] = intToLevel(l)
199207
}
200208
log.SetFlags(log.Ltime)
201209
configFromEnv()
@@ -339,7 +347,7 @@ func EnvHelp(w io.Writer) {
339347

340348
// GetLogLevel returns the currently configured LogLevel.
341349
func GetLogLevel() Level {
342-
return Level(atomic.LoadInt32(&levelInternal))
350+
return intToLevel(int(atomic.LoadInt32(&levelInternal)))
343351
}
344352

345353
// Log returns true if a given level is currently logged.

0 commit comments

Comments
 (0)