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.
transport.miden.ioreturns 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):
From a page on
http://localhost:5180, same request, same headers:The response carries no
Access-Control-Expose-Headers, so the browser hidesgrpc-statusandgrpc-messagefrom JS.rpc.testnet.miden.iosets it, and its errors read fine from a page: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-clientreportsMeasured today against
transport.miden.io, service version0.5.0-rc.2(fromStats):FetchNotes, 128 tagsFetchNotes, 129 tagsFetchNotes, invalid protobufStatsWhy this is worse than a poor error message:
MidenClient.sync()fetches from the NTL first and fails fast, andcreateTestnet()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, whereMAX_TAGS_PER_FETCH_REQUESTis 128 and the rejection is a cleanStatus::invalid_argument. So this looks like one line wherever CORS is configured for the deployment: exposegrpc-status,grpc-messageandgrpc-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.