Follow-up to #1433, closed by #1435 with a service-local workaround rather than the modeled contract.
Where it stands
HEAD /iili/v1/r/{slug} answers 302 with Location today, via WithHeadAsGet in domains/iili/apis/iili/smithy_handler.cc: it re-dispatches HEAD as the GET it mirrors and clears the body on the way out. main.cc and both test harnesses share that composition, and wire_test.cc pins the 302 and the 404.
It is deliberately not modeled. Routing was never the obstacle — smithy-cpp's router buckets by method and only checks conflicts within a bucket, so @http(method: "HEAD", uri: "/iili/v1/r/{slug}") would register cleanly beside the GET. The obstacle is framing: ToWireResponse copies the body and calls prepare_payload() without ever seeing the request method, so a modeled HEAD operation would put the generated {} body on a HEAD response — forbidden by RFC 9110 §9.3.2, and on a keep-alive connection the client reads those two bytes as the start of the next response. The handler cannot suppress it: the body comes from the generated serializer and the transport owns framing.
Filed upstream as muchq/smithy-cpp#192.
What to do here, once the pin picks that up
- Add the HEAD binding for
Redirect to domains/iili/apis/iili/model/iili.smithy.
- Delete
WithHeadAsGet and its declaration, and unwrap the three call sites (main.cc, wire_test.cc, production_chain_test.cc).
- Keep both HEAD wire tests exactly as they are — they assert behavior, not mechanism, so they should stay green across the swap. That is the check that the modeled version is a true replacement.
Why it is worth doing
The contract is invisible where a reader looks for it. Nothing in iili.smithy says HEAD is answered, so the model understates what the service serves, and a generated client has no HEAD operation. The workaround also lives only in this service; the modeled form would come from the same place every other route does.
Not urgent — behavior is correct on the wire today. This is about where the truth is written down.
Follow-up to #1433, closed by #1435 with a service-local workaround rather than the modeled contract.
Where it stands
HEAD /iili/v1/r/{slug}answers 302 withLocationtoday, viaWithHeadAsGetindomains/iili/apis/iili/smithy_handler.cc: it re-dispatches HEAD as the GET it mirrors and clears the body on the way out.main.ccand both test harnesses share that composition, andwire_test.ccpins the 302 and the 404.It is deliberately not modeled. Routing was never the obstacle — smithy-cpp's router buckets by method and only checks conflicts within a bucket, so
@http(method: "HEAD", uri: "/iili/v1/r/{slug}")would register cleanly beside the GET. The obstacle is framing:ToWireResponsecopies the body and callsprepare_payload()without ever seeing the request method, so a modeled HEAD operation would put the generated{}body on a HEAD response — forbidden by RFC 9110 §9.3.2, and on a keep-alive connection the client reads those two bytes as the start of the next response. The handler cannot suppress it: the body comes from the generated serializer and the transport owns framing.Filed upstream as muchq/smithy-cpp#192.
What to do here, once the pin picks that up
Redirecttodomains/iili/apis/iili/model/iili.smithy.WithHeadAsGetand its declaration, and unwrap the three call sites (main.cc,wire_test.cc,production_chain_test.cc).Why it is worth doing
The contract is invisible where a reader looks for it. Nothing in
iili.smithysays HEAD is answered, so the model understates what the service serves, and a generated client has no HEAD operation. The workaround also lives only in this service; the modeled form would come from the same place every other route does.Not urgent — behavior is correct on the wire today. This is about where the truth is written down.