Skip to content

Client: no coverage for a non-200 response carrying a gRPC status, where connect-go and grpc-go disagree #1119

Description

@iainmcgin

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-gogrpcValidateResponse (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.

  1. 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.
  2. 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.
  3. 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.
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions