Stop the payload content type from clobbering a modeled Accept header - #220
Merged
Conversation
Found in use against a generated client. An operation with a response @httpPayload emits that payload's content type as the request's Accept header, but did so with an unconditional Set — after the @HTTPHeader bindings had already written the caller's own modeled Accept. The modeled member was dead on arrival: set, then overwritten, on every call. The workaround left to a consumer is an interceptor re-setting the header in ModifyBeforeTransmit, which is the client's own job. The payload content type is a default, not an override, so it is now applied only when nothing has already set the header. That is the same guard the generated Send helper has always applied to its document-response default ("Operations with a non-document response payload set their own accept"), and the one writePayloadWrite already applied to the request payload's Content-Type — Accept was the one default in this file that missed it. Covered at both levels, each written red first and mutation-checked against the unguarded emitter: a codegen pin on the emitted line, and a wire-level acceptance test that injects a recording transport into a generated client and reads back the header it actually sent — with the unset case asserted too, so guarding the default is not mistaken for deleting it. Goldens move for every operation with a response payload; behavior changes only for a model that binds Accept, or an @httpPrefixHeaders map carrying it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jj5X2fKdgrYurHmbwLzUiQ
|
Reviewed f11b292. The guarded default preserves modeled and prefix-provided |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Reported from use against a generated client. An operation with a response
@httpPayloademits that payload's content type as the request'sAcceptheader — but with an unconditionalSet, after the@httpHeaderbindings have already written the caller's own modeledAccept:So a modeled
@httpHeader("Accept")member was dead on arrival, and the workaround left to a consumer is an interceptor re-setting the header inModifyBeforeTransmit— which is the client's own job.The payload content type is a default, not an override, so it is now applied only when nothing has already set the header:
That is the same guard two neighbours already had, which is what makes this a miss rather than a design choice:
Sendhelper, on its document-response default — "Operations with a non-document response payload set their own accept";HttpBindingCodeGen.writePayloadWrite, on the request payload'sContent-Type.Acceptwas the one default in this file without it.I checked the other unguarded
Set("content-type", ...)sites and left them alone deliberately: those write the content type of a document body the generator just encoded, so it is a fact about bytes it wrote rather than a caller preference, and the error-body helper builds a fresh response with no modeled member to lose.Testing
Both levels, each written red first and mutation-checked by reverting the emitter to the unguarded form:
GeneratedCodeShapeTest.aModeledAcceptHeaderSurvivesThePayloadContentType— pins the emitted guard, and asserts per line that the bare unguardedSetis gone (a substring search would always find it inside the guarded statement).examples/bazel-consumer/response_sink_acceptance_test.cc— a string pin is not proof the header reaches the wire, soModeledAcceptHeaderTestinjects a recording transport into a generated client and reads back what it actually sent: a modeledAcceptsurvives (and appears exactly once), and an unset one still gets the payload content type — so guarding the default is not mistaken for deleting it.DownloadInputgains the modeled member; no handler changes.Goldens move for every operation with a response payload — the diff is the guard and nothing else. Behavior changes only for a model that binds
Accept, or an@httpPrefixHeadersmap carrying it.bazel test --config=werror //...— 130/130. Consumer module, same flags (minus the Beast targets a download-blocking proxy cannot run) — 14/14.gradle test spotlessCheckgreen. clang-format and buildifier clean.Checklist
bazel test //...and(cd codegen && gradle build spotlessCheck)pass locally🤖 Generated with Claude Code
https://claude.ai/code/session_01Jj5X2fKdgrYurHmbwLzUiQ
Generated by Claude Code