Skip to content

gRPC-web: Access-Control-Expose-Headers is missing, so a browser can never read an error status #164

Description

@Mustdzyl

transport.miden.io returns the right gRPC error, with a genuinely useful message, and a browser client can never see it.

The same failing call, two ways. With curl (no CORS in the way):

HTTP/2 200
access-control-allow-origin: *
content-type: application/grpc-web+proto
grpc-message: Too%20many%20tags%20in%20fetch_notes%20request:%20142%20(max%20128)
grpc-status: 3
content-length: 0

From a page on http://localhost:5180, same request, same headers:

const r = await fetch(url, { method: "POST", headers: {
  "content-type": "application/grpc-web+proto", "x-grpc-web": "1" }, body: frame });
r.status                          // 200
(await r.arrayBuffer()).byteLength // 0
r.headers.get("grpc-status")      // null
r.headers.get("grpc-message")     // null

The response carries no Access-Control-Expose-Headers, so the browser hides grpc-status and grpc-message from JS. rpc.testnet.miden.io sets it, and its errors read fine from a page:

access-control-expose-headers: grpc-status,grpc-message,grpc-status-details-bin

What a client can do with what is left: an HTTP 200 with an empty body and no status is not a valid gRPC-web response, so miden-client reports

Fetch notes failed: Status { code: Unknown, message: "malformed response",
  metadata: MetadataMap { headers: {"content-length": "0", "content-type": "application/grpc-web+proto"} },
  source: Some(MalformedResponse) }

Measured today against transport.miden.io, service version 0.5.0-rc.2 (from Stats):

request HTTP body grpc-status via curl grpc-status in a browser
FetchNotes, 128 tags 200 25 B 0 0, in the trailer
FetchNotes, 129 tags 200 0 B 3 + message not visible
FetchNotes, invalid protobuf 200 0 B 3 not visible
unknown method 200 0 B 12 not visible
Stats 200 43 B 0 0

Why this is worse than a poor error message: MidenClient.sync() fetches from the NTL first and fails fast, and createTestnet() syncs on creation. So the client cannot be constructed at all, and a browser app does not degrade — it fails to open, with an error that points nowhere. That is what happened to my app this morning. On-chain state was healthy the whole time (syncChain() returned block 260019 in 444 ms); I spent the morning bisecting request shapes to find the boundary, while the service had been sending me the reason in a header the browser was throwing away.

The status itself is produced correctly — I read crates/node/src/node/grpc/mod.rs, where MAX_TAGS_PER_FETCH_REQUEST is 128 and the rejection is a clean Status::invalid_argument. So this looks like one line wherever CORS is configured for the deployment: expose grpc-status, grpc-message and grpc-status-details-bin, the way the RPC endpoint already does. I could not find that configuration in this repo; if you point me at it I am happy to open the PR.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions