fix(multipart): handle typed nil readers consistently - #90
fix(multipart): handle typed nil readers consistently#90sylvesterkaczmarek wants to merge 9 commits into
Conversation
|
I followed up on the typed-nil review and pushed e6eeec6 via maintainer edits. The original shallow nil check still allowed a typed-nil pointer wrapped in an |
HAYDEN-OAI
left a comment
There was a problem hiding this comment.
Reviewed typed-nil reader handling across interface/pointer unwrapping, multipart field encoding, reader dispatch, header validation, and streamed request ownership/error propagation. No substantive correctness, security, or compatibility issues found.
jbeckwith-oai
left a comment
There was a problem hiding this comment.
Requesting changes because typed-nil reader normalization is inconsistent between multipart serialization and request transport selection. The encoder now writes a typed-nil reader as an empty scalar field, but inspectMultipartBody still classifies it as an upload and multipartRequestOptions therefore disables retries and selects a one-shot streamed body.
I reproduced this with a synthetic httptest server: a multipart request containing map[string]any{"file": (*reader)(nil)} received HTTP 429 and made only one attempt instead of retrying as a scalar-only form. Please normalize typed-nil readers consistently in the upload classifier and add an end-to-end request regression covering retry behavior. Redirect/replayability and Content-Length behavior should remain consistent with an empty scalar field as well.
The current-head CI, CodeQL, and Castiron custom-code workflows are also all stopped with action_required, so those checks still need to run successfully before merge.
| } | ||
| val = val.Elem() | ||
| } | ||
| if val.Kind() == reflect.Pointer && val.IsNil() { |
There was a problem hiding this comment.
[P2] Apply the same typed-nil semantics before choosing multipart transport. This branch correctly serializes a typed-nil reader as an empty scalar field, but pkg/cmd/multipartbody.go:inspectMultipartBody still sees the non-nil io.Reader interface and marks it as hasUpload. As a result, multipartRequestOptions unnecessarily selects one-shot streaming, applies option.WithMaxRetries(0), and rejects otherwise replayable 307/308 redirects. I verified the mismatch end to end with a synthetic HTTP 429 response: the empty-field request made one attempt and failed instead of retrying. Please update the upload classifier to recognize typed-nil readers as scalars and add a request-level retry regression.
There was a problem hiding this comment.
Fixed. The transport classifier now applies the same typed-nil reader semantics as the encoder, so typed-nil readers stay on the buffered scalar path instead of being treated as uploads. I also updated the known-length framing path so a typed-nil optional reader can coexist with a real file upload.
Added request-level regressions covering the reported 429 retry case, replay across both 307 and 308 redirects, buffered Content-Length, the empty scalar-field representation, and a known-length file upload alongside a typed-nil reader.
Fresh CI, CodeQL and Castiron runs are currently action_required pending maintainer approval.
Summary
Normalize typed-nil reader semantics across multipart encoding and request transport selection so typed-nil readers are treated consistently as empty scalar fields.
Fixes #89.
Root cause
internal/apiform.encodeValuedetectedio.Readercapability before applying nil-pointer semantics. A typed-nil pointer therefore retained a concrete reader type and could reachio.Copy.The request layer had the same semantic mismatch in a different place:
inspectMultipartBodyclassified the same typed-nil reader as an upload even after the encoder treated it as an empty field. That unnecessarily selected one-shot streaming, disabled retries, and made an otherwise replayable scalar form reject 307/308 redirects.Fix
Content-Lengthcalculation;Regression coverage
The tests cover:
map[string]any;map[string]io.Reader;io.Readerinterface;Content-Length;Validation
The earlier encoder-focused revision passed focused tests, race tests, repository compile-only tests,
go mod verify, and lint locally. The transport follow-up is covered by the new request-level regressions above.Fresh upstream CI, CodeQL, and Castiron runs are currently gated with
action_requiredpending external-contributor approval.Risk
Low. The behavior change is limited to typed-nil reader values. Non-nil readers and real uploads retain the existing streamed path with retries disabled; scalar-only forms remain buffered and replayable.