Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions encoding.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"log/slog"
"path/filepath"
"reflect"
"runtime"
"time"
)
Expand Down Expand Up @@ -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 <nil> 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, "<nil>", attrValue)
return
}
e.writeColoredString(buf, v.String(), attrValue)
return
}
Expand Down
24 changes: 24 additions & 0 deletions handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 "<nil>" 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=<nil>\n"
AssertEqual(t, expected, buf.String())
}