Skip to content

Commit

Permalink
Don't panic when encoding a Stringer field (uber-go#675)
Browse files Browse the repository at this point in the history
This commit ensures any panic raised while encoding zap.Stringer
is caught and reported as an error as field kError
for a field k. The reported panic is prefixed with "PANIC=" to
distinguish these errors from other encoding errors.

Closes uber-go#495
  • Loading branch information
joa authored and prashantv committed Mar 27, 2019
1 parent e0838f2 commit 1607c65
Show file tree
Hide file tree
Showing 2 changed files with 52 additions and 10 deletions.
13 changes: 12 additions & 1 deletion zapcore/field.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ func (f Field) AddTo(enc ObjectEncoder) {
case NamespaceType:
enc.OpenNamespace(f.Key)
case StringerType:
enc.AddString(f.Key, f.Interface.(fmt.Stringer).String())
err = encodeStringer(f.Key, f.Interface, enc)
case ErrorType:
encodeError(f.Key, f.Interface.(error), enc)
case SkipType:
Expand Down Expand Up @@ -199,3 +199,14 @@ func addFields(enc ObjectEncoder, fields []Field) {
fields[i].AddTo(enc)
}
}

func encodeStringer(key string, stringer interface{}, enc ObjectEncoder) (err error) {
defer func() {
if v := recover(); v != nil {
err = fmt.Errorf("PANIC=%v", v)
}
}()

enc.AddString(key, stringer.(fmt.Stringer).String())
return
}
49 changes: 40 additions & 9 deletions zapcore/field_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,12 @@ import (
"errors"
"fmt"
"math"
"net/url"
"testing"
"time"

"go.uber.org/zap"

"github.com/stretchr/testify/assert"

"go.uber.org/zap"
. "go.uber.org/zap/zapcore"
)

Expand Down Expand Up @@ -58,6 +57,28 @@ func (u users) MarshalLogArray(enc ArrayEncoder) error {
return nil
}

type obj struct {
kind int
}

func (o *obj) String() string {
if o == nil {
return "nil obj"
}

if o.kind == 1 {
panic("panic with string")
} else if o.kind == 2 {
panic(errors.New("panic with error"))
} else if o.kind == 3 {
// panic with an arbitrary object that causes a panic itself
// when being converted to a string
panic((*url.URL)(nil))
}

return "obj"
}

func TestUnknownFieldType(t *testing.T) {
unknown := Field{Key: "k", String: "foo"}
assert.Equal(t, UnknownType, unknown.Type, "Expected zero value of FieldType to be UnknownType.")
Expand All @@ -67,19 +88,27 @@ func TestUnknownFieldType(t *testing.T) {
}

func TestFieldAddingError(t *testing.T) {
var empty interface{}
tests := []struct {
t FieldType
want interface{}
t FieldType
iface interface{}
want interface{}
err string
}{
{ArrayMarshalerType, []interface{}{}},
{ObjectMarshalerType, map[string]interface{}{}},
{t: ArrayMarshalerType, iface: users(-1), want: []interface{}{}, err: "too few users"},
{t: ObjectMarshalerType, iface: users(-1), want: map[string]interface{}{}, err: "too few users"},
{t: StringerType, iface: obj{}, want: empty, err: "PANIC=interface conversion: zapcore_test.obj is not fmt.Stringer: missing method String"},
{t: StringerType, iface: &obj{1}, want: empty, err: "PANIC=panic with string"},
{t: StringerType, iface: &obj{2}, want: empty, err: "PANIC=panic with error"},
{t: StringerType, iface: &obj{3}, want: empty, err: "PANIC=<nil>"},
{t: StringerType, iface: (*url.URL)(nil), want: empty, err: "PANIC=runtime error: invalid memory address or nil pointer dereference"},
}
for _, tt := range tests {
f := Field{Key: "k", Interface: users(-1), Type: tt.t}
f := Field{Key: "k", Interface: tt.iface, Type: tt.t}
enc := NewMapObjectEncoder()
assert.NotPanics(t, func() { f.AddTo(enc) }, "Unexpected panic when adding fields returns an error.")
assert.Equal(t, tt.want, enc.Fields["k"], "On error, expected zero value in field.Key.")
assert.Equal(t, "too few users", enc.Fields["kError"], "Expected error message in log context.")
assert.Equal(t, tt.err, enc.Fields["kError"], "Expected error message in log context.")
}
}

Expand Down Expand Up @@ -116,6 +145,8 @@ func TestFields(t *testing.T) {
{t: ReflectType, iface: users(2), want: users(2)},
{t: NamespaceType, want: map[string]interface{}{}},
{t: StringerType, iface: users(2), want: "2 users"},
{t: StringerType, iface: &obj{}, want: "obj"},
{t: StringerType, iface: (*obj)(nil), want: "nil obj"},
{t: SkipType, want: interface{}(nil)},
}

Expand Down

0 comments on commit 1607c65

Please sign in to comment.