Commit ad33253
authored
fix(client): fix content type selection in the runtime client. (#435)
* fixes #386
* fixes #387
* fixes #32
* test(client): behavioral harness for content-type selection
* fix(client): honor stream payload's declared Content-Type
* feat(client): name ContentTyper as an exported interface that Producers may implement
* feat(client): payload-aware two-stage Content-Type selection
* feat(client): honor SetHeader Content-Type as escape hatch for streams
* feat(client): producer-capability filter in pickConsumesMediaType
* feat: ContentTyper as a new exported interface
* docs: describe two-stage client Content-Type selection
* docs: summarise v0.30 client-negotiation deltas vs v0.29
Additional tests for client-side content negotiation
====================================================
Captures the current behavior of (*request).buildHTTP and
Runtime.Submit across (consumes, payload, fields) combinations.
FIXME-#387 so subsequent fixes produce visible deltas in the harness.
Coverage on negotiation paths: buildHTTP 76.3 → 78.6%,
createHttpRequest 81.0 → 85.7%.
Fix: client-side content type selection
====================================================
Stream payloads (io.Reader, io.ReadCloser) bypass the producer in
buildHTTP — their bytes flow through verbatim. The picker's chosen
mediaType therefore only describes the operation's intent, not the
body.
* "infer Content-Type from payload" (#387)
Payloads that implement runtime.ContentTyper may override this pick.
with a non-empty result now overrides the picked mediaType for the
header. This addresses the "infer Content-Type from payload" half of
issue #387.
* pick the right consumes entry (#386)
The picker-side counterpart (choosing the right consumes entry up front
when the payload is a raw stream and no ContentType() is declared)
requires payload-aware two-stage selection.
Non-stream (struct, []byte) payloads are unchanged: producer runs and
the header reflects the picker.
The picker (pickConsumesMediaType) runs in createHttpRequest before
the writer populates the payload. It cannot see whether the payload
is a stream, so its choice may misrepresent the body — e.g. picking
application/json for a request whose payload is a raw byte stream.
This change introduces a Stage-2 fallback inside buildHTTP that runs
*after* the writer has populated r.payload. For stream payloads
(io.Reader / io.ReadCloser) that do not declare their own type via
`ContentType() string`:
- if application/octet-stream is also offered in the operation's
consumes list AND a producer is registered for it, the wire
Content-Type advertises octet-stream — a safer claim than the
picker's structural mime;
- otherwise the picker's choice is preserved (we cannot infer
something better without more information from the caller).
This addresses the picker-side counterpart of issue #386. Combined
with the previous step (#387 — payload-declared ContentType()
override), the wire Content-Type now matches the body in every case
where the runtime has enough information to know.
* honor SetHeader Content-Type as escape hatch for streams
1. SetHeaderParam("Content-Type", X) — user's explicit assertion;
2. runtime.ContentTyper on the payload — value declares its type;
3. Stage-2 octet-stream upgrade — stream with neither of the above;
4. Stage-1 picker mediaType — final fallback.
Non-stream paths (struct/[]byte payloads, form fields, multipart) are
deliberately not honored.
* producer-capability filter in pickConsumesMediaType
The selection rules become:
1. multipart/form-data preference (issue #286, unchanged);
2. first non-empty entry that is structural OR has a registered
producer in r.Producers — closes the gap;
3. first non-empty entry overall (preserves the historical
"none of producers" diagnostic when nothing is registered);
4. DefaultMediaType when consumes is empty.
Signed-off-by: Frederic BIDON <fredbi@yahoo.com>1 parent 2b5a6dd commit ad33253
7 files changed
Lines changed: 1164 additions & 202 deletions
File tree
- .claude/plans
- client
- docs
This file was deleted.
0 commit comments