diff --git a/encoding.go b/encoding.go index 6088e65..51b6ae7 100644 --- a/encoding.go +++ b/encoding.go @@ -4,6 +4,7 @@ import ( "fmt" "log/slog" "path/filepath" + "reflect" "runtime" "time" ) @@ -147,6 +148,19 @@ func (e encoder) writeValue(buf *buffer, value slog.Value) { e.writeColoredString(buf, v.Error(), e.opts.Theme.AttrValueError()) return case fmt.Stringer: + // A typed-nil pointer still satisfies the Stringer + // interface (e.g. (*time.Time)(nil) picks up the + // auto-generated pointer-receiver wrapper for + // time.Time.String), so calling v.String() would + // dispatch the method on a nil receiver and panic + // with 'value method ... called using nil * pointer' + // (#22). Render the literal instead so a + // missing optional field is just a missing attribute, + // not a crashed logger. + if rv := reflect.ValueOf(v); rv.Kind() == reflect.Pointer && rv.IsNil() { + e.writeColoredString(buf, "", attrValue) + return + } e.writeColoredString(buf, v.String(), attrValue) return } diff --git a/handler_test.go b/handler_test.go index eb09629..101c4c7 100644 --- a/handler_test.go +++ b/handler_test.go @@ -434,3 +434,27 @@ func TestThemes(t *testing.T) { }) } } + +// TestHandler_NilStringerPointer is a regression for #22. Logging a +// typed-nil pointer whose underlying value type implements fmt.Stringer +// (e.g. (*time.Time)(nil)) used to dispatch the String method on the +// nil receiver and panic with 'value method time.Time.String called +// using nil *Time pointer'. The handler must render "" instead. +func TestHandler_NilStringerPointer(t *testing.T) { + buf := bytes.Buffer{} + h := NewHandler(&buf, &HandlerOptions{NoColor: true}) + rec := slog.NewRecord(time.Time{}, slog.LevelInfo, "foobar", 0) + + var expiration *time.Time + rec.AddAttrs(slog.Any("expiration", expiration)) + + defer func() { + if r := recover(); r != nil { + t.Fatalf("handler panicked on nil *time.Time: %v", r) + } + }() + AssertNoError(t, h.Handle(context.Background(), rec)) + + expected := "INF foobar expiration=\n" + AssertEqual(t, expected, buf.String()) +}