Summary
No test case pairs a non-200 HTTP status with a grpc-status header. That combination is exactly where connect-go and grpc-go disagree today, so the suite currently permits two mutually incompatible client behaviours — including one where the same wire response is a success in one implementation and an error in the other.
What is already covered
testsuites/client_http_to_rpc_code.yaml covers bare non-200 responses (400, 401, 403, 404, 409, 412, 413, 415, 429, 431, 502, 503, 504, 505) and pins the synthesized RPC code for each. It carries no relevantProtocols, so it runs for all three protocols.
What is missing is the case where the non-200 also carries the gRPC status in its headers — the shape a proxy emits when it synthesizes a trailers-only error. Scanning every testCases entry in testsuites/*.yaml for a rawResponse with a non-200 statusCode whose request also mentions grpc-status returns zero matches.
The two reference implementations disagree
Measured by driving both against a purpose-built h2c server emitting each shape exactly. Versions: connect-go v1.19.1, grpc-go v1.81.1.
| Response |
connect-go |
grpc-go |
503, content-type: application/grpc, grpc-status: 14, grpc-message: upstream connect error, no body, no trailers |
unavailable — "HTTP status 503 Service Unavailable", no metadata |
Unavailable — "upstream connect error" |
500, content-type: application/grpc, grpc-status: 3, grpc-message: bad argument, no body, no trailers |
unknown — "HTTP status 500 Internal Server Error" |
InvalidArgument — "bad argument" |
503, valid data envelope, HTTP/2 trailer grpc-status: 0 |
unavailable — "HTTP status 503" |
OK, message returned |
The mechanism in each:
- connect-go —
grpcValidateResponse (protocol_grpc.go) returns errorf(httpToCode(status), "HTTP status %v", ...) before it looks at the content-type or any gRPC status. A non-200 never has its grpc-status read, and the resulting error carries no metadata.
- grpc-go — in
http2_client.go's operateHeaders, a valid content-type: application/grpc* sets isGRPC = true, and the HTTP-status branch is guarded by if !isGRPC. The HTTP status is discarded entirely.
Neither is obviously wrong. grpc-go's reading follows from "the content-type identifies the protocol, and gRPC carries its own status"; connect-go's follows from "a non-200 means the request did not reach a gRPC handler". But the third row is the uncomfortable one: a complete, valid, successful gRPC response delivered under HTTP 503 is OK to one client and a failure to the other.
Why it matters in practice
The shape comes from intermediaries, not from conformant servers, which is presumably why it has not surfaced: an Envoy local reply pairs a non-200 with grpc-status and grpc-message, and so do a number of ingress controllers and service meshes. For a client author the practical consequence is whether an upstream's error message and metadata reach the caller at all, or are replaced by "HTTP status 503".
We hit this implementing the gRPC client in connectrpc/connect-rust and had no way to resolve it from the suite, since the suite is silent and the two references point opposite ways.
Suggested cases
All of these are expressible with the existing harness — RawHTTPResponse already carries status_code, headers, trailers and a stream body — so this is test data, not machinery.
- Non-200 +
grpc-status/grpc-message in headers, no body, no trailers (the Envoy local-reply shape), for PROTOCOL_GRPC and PROTOCOL_GRPC_WEB. Whether the expected code is the gRPC one or the HTTP-derived one is the decision this issue is really asking for.
- The same, but where the two disagree on the code — e.g. HTTP
500 with grpc-status: 3 — so the choice is observable rather than coincidentally identical.
- Non-200 carrying a complete and successful response (data envelope +
grpc-status: 0 trailer). Even if the answer is "unspecified", it seems worth an explicit decision.
- Non-200 whose
grpc-status is malformed, to pin whether that reads as the synthesized HTTP mapping or as unknown.
Happy to send a PR with these once the expected behaviour is settled. We do not have a stake in which way it goes — our interest is that it is pinned, because right now a client can pass the full suite with either behaviour.
Summary
No test case pairs a non-200 HTTP status with a
grpc-statusheader. That combination is exactly where connect-go and grpc-go disagree today, so the suite currently permits two mutually incompatible client behaviours — including one where the same wire response is a success in one implementation and an error in the other.What is already covered
testsuites/client_http_to_rpc_code.yamlcovers bare non-200 responses (400, 401, 403, 404, 409, 412, 413, 415, 429, 431, 502, 503, 504, 505) and pins the synthesized RPC code for each. It carries norelevantProtocols, so it runs for all three protocols.What is missing is the case where the non-200 also carries the gRPC status in its headers — the shape a proxy emits when it synthesizes a trailers-only error. Scanning every
testCasesentry intestsuites/*.yamlfor arawResponsewith a non-200statusCodewhose request also mentionsgrpc-statusreturns zero matches.The two reference implementations disagree
Measured by driving both against a purpose-built h2c server emitting each shape exactly. Versions: connect-go v1.19.1, grpc-go v1.81.1.
503,content-type: application/grpc,grpc-status: 14,grpc-message: upstream connect error, no body, no trailersunavailable— "HTTP status 503 Service Unavailable", no metadataUnavailable— "upstream connect error"500,content-type: application/grpc,grpc-status: 3,grpc-message: bad argument, no body, no trailersunknown— "HTTP status 500 Internal Server Error"InvalidArgument— "bad argument"503, valid data envelope, HTTP/2 trailergrpc-status: 0unavailable— "HTTP status 503"OK, message returnedThe mechanism in each:
grpcValidateResponse(protocol_grpc.go) returnserrorf(httpToCode(status), "HTTP status %v", ...)before it looks at the content-type or any gRPC status. A non-200 never has itsgrpc-statusread, and the resulting error carries no metadata.http2_client.go'soperateHeaders, a validcontent-type: application/grpc*setsisGRPC = true, and the HTTP-status branch is guarded byif !isGRPC. The HTTP status is discarded entirely.Neither is obviously wrong. grpc-go's reading follows from "the content-type identifies the protocol, and gRPC carries its own status"; connect-go's follows from "a non-200 means the request did not reach a gRPC handler". But the third row is the uncomfortable one: a complete, valid, successful gRPC response delivered under HTTP 503 is
OKto one client and a failure to the other.Why it matters in practice
The shape comes from intermediaries, not from conformant servers, which is presumably why it has not surfaced: an Envoy local reply pairs a non-200 with
grpc-statusandgrpc-message, and so do a number of ingress controllers and service meshes. For a client author the practical consequence is whether an upstream's error message and metadata reach the caller at all, or are replaced by "HTTP status 503".We hit this implementing the gRPC client in connectrpc/connect-rust and had no way to resolve it from the suite, since the suite is silent and the two references point opposite ways.
Suggested cases
All of these are expressible with the existing harness —
RawHTTPResponsealready carriesstatus_code,headers,trailersand astreambody — so this is test data, not machinery.grpc-status/grpc-messagein headers, no body, no trailers (the Envoy local-reply shape), forPROTOCOL_GRPCandPROTOCOL_GRPC_WEB. Whether the expected code is the gRPC one or the HTTP-derived one is the decision this issue is really asking for.500withgrpc-status: 3— so the choice is observable rather than coincidentally identical.grpc-status: 0trailer). Even if the answer is "unspecified", it seems worth an explicit decision.grpc-statusis malformed, to pin whether that reads as the synthesized HTTP mapping or asunknown.Happy to send a PR with these once the expected behaviour is settled. We do not have a stake in which way it goes — our interest is that it is pinned, because right now a client can pass the full suite with either behaviour.