From 189de38a9508a64dd2864a76e1f822dd41e1f08f Mon Sep 17 00:00:00 2001 From: HughChaw <146055770+Hughhhhcoder@users.noreply.github.com> Date: Mon, 24 Aug 2026 12:07:52 +0800 Subject: [PATCH] fix(apiform): encode typed nil readers as empty fields instead of panicking --- internal/apiform/encoder.go | 21 +++++++++++----- internal/apiform/form_test.go | 46 +++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 6 deletions(-) diff --git a/internal/apiform/encoder.go b/internal/apiform/encoder.go index fc963b2d..52af9523 100644 --- a/internal/apiform/encoder.go +++ b/internal/apiform/encoder.go @@ -51,15 +51,27 @@ func (e *encoder) encodeValue(key string, val reflect.Value, writer *multipart.W t := val.Type() + // Encode nil pointers and nil interfaces as empty fields before any other + // type detection. A typed nil pointer whose type implements io.Reader must + // not be routed into encodeReader, which would invoke Read on a nil + // receiver via io.Copy and panic. + switch t.Kind() { + case reflect.Pointer: + if val.IsNil() { + return writer.WriteField(key, "") + } + case reflect.Interface: + if val.IsNil() { + return writer.WriteField(key, "") + } + } + if t.Implements(reflect.TypeOf((*io.Reader)(nil)).Elem()) { return e.encodeReader(key, val, writer) } switch t.Kind() { case reflect.Pointer: - if val.IsNil() || !val.IsValid() { - return writer.WriteField(key, "") - } return e.encodeValue(key, val.Elem(), writer) case reflect.Slice, reflect.Array: @@ -69,9 +81,6 @@ func (e *encoder) encodeValue(key string, val reflect.Value, writer *multipart.W return e.encodeMap(key, val, writer) case reflect.Interface: - if val.IsNil() { - return writer.WriteField(key, "") - } return e.encodeValue(key, val.Elem(), writer) case reflect.String: diff --git a/internal/apiform/form_test.go b/internal/apiform/form_test.go index 70a8923c..01e4eeea 100644 --- a/internal/apiform/form_test.go +++ b/internal/apiform/form_test.go @@ -2,6 +2,7 @@ package apiform import ( "bytes" + "io" "mime/multipart" "testing" ) @@ -116,3 +117,48 @@ func TestEncode(t *testing.T) { }) } } + +// panicReader panics if its Read method is invoked. It is used to assert that +// a typed nil pointer implementing io.Reader is never dereferenced during +// multipart encoding. +type panicReader struct{} + +func (*panicReader) Read([]byte) (int, error) { + panic("Read called on nil receiver") +} + +func TestEncodeTypedNilReader(t *testing.T) { + t.Parallel() + + var reader *panicReader + + buf := bytes.NewBuffer(nil) + writer := multipart.NewWriter(buf) + writer.SetBoundary("xxx") + + // A typed nil pointer that implements io.Reader must follow the same + // empty-field semantics as any other nil value, without invoking Read. + form := map[string]any{"foo": reader} + if err := MarshalWithSettings(form, writer, FormatRepeat); err != nil { + t.Fatalf("serialization of typed nil reader failed with error %v", err) + } + if err := writer.Close(); err != nil { + t.Fatalf("closing writer failed with error %v", err) + } + + parts := multipart.NewReader(bytes.NewReader(buf.Bytes()), writer.Boundary()) + part, err := parts.NextPart() + if err != nil { + t.Fatalf("reading encoded field failed with error %v", err) + } + if part.FormName() != "foo" { + t.Errorf("encoded field name = %q, want %q", part.FormName(), "foo") + } + contents, err := io.ReadAll(part) + if err != nil { + t.Fatalf("reading encoded field contents failed with error %v", err) + } + if len(contents) != 0 { + t.Errorf("encoded typed nil reader contents = %q, want empty", contents) + } +}