Skip to content

Comments

fix(rpc): move CORS layer above AcceptHeaderLayer#1707

Open
WiktorStarczewski wants to merge 3 commits intomainfrom
fix/cors-layer-ordering
Open

fix(rpc): move CORS layer above AcceptHeaderLayer#1707
WiktorStarczewski wants to merge 3 commits intomainfrom
fix/cors-layer-ordering

Conversation

@WiktorStarczewski
Copy link

Summary

Reorders the tower middleware stack so that cors_for_grpc_web_layer() is applied before AcceptHeaderLayer.

Problem

When a browser-based gRPC-Web client (e.g. the Miden web SDK) connects to the RPC server with an SDK version the server doesn't recognize, AcceptHeaderLayer rejects the request by returning a tonic::Status::invalid_argument response directly via futures::future::ready(). This short-circuits all downstream middleware — including the CORS layer.

Because the CORS layer sat below AcceptHeaderLayer in the middleware stack, rejection responses were missing Access-Control-Allow-Origin and other CORS headers entirely. The browser then blocked the response at the network level, giving the user an opaque net::ERR_FAILED / "CORS policy" error instead of the actual gRPC error message ("server does not support any of the specified application/vnd.miden content types").

This made it impossible to diagnose version mismatches from a browser environment — the real error was silently swallowed by the browser's CORS enforcement.

Root Cause

In tower's .layer() builder, layers listed earlier are outer layers — they see responses last on the way out. The previous ordering was:

.layer(HealthCheckLayer)
.layer(AcceptHeaderLayer)         // rejects here → returns response directly
.layer(cors_for_grpc_web_layer()) // never sees rejection responses
.layer(GrpcWebLayer)

When AcceptHeaderLayer rejected a request, the response traveled back out through HealthCheckLayer and TraceLayer but never passed through CorsLayer, since it was an inner layer that the request never reached.

Fix

.layer(HealthCheckLayer)
.layer(cors_for_grpc_web_layer()) // now wraps AcceptHeaderLayer
.layer(AcceptHeaderLayer)         // rejections still get CORS headers
.layer(GrpcWebLayer)

By moving the CORS layer above the accept header check, all responses — including version rejections — are wrapped with proper CORS headers. The browser can then read and surface the actual gRPC error to the application.

Verification

Tested with curl against the two scenarios:

Before (no CORS headers on rejection):

$ curl -X POST "https://rpc.testnet.miden.io/rpc.Api/GetBlockHeaderByNumber" \
  -H "Origin: http://localhost:3000" \
  -H "Content-Type: application/grpc-web+proto" \
  -H "accept: application/vnd.miden; version=0.14.0"

HTTP/2 200
content-type: application/grpc
grpc-message: server does not support any of the specified application/vnd.miden content types
grpc-status: 3
# ← no Access-Control-Allow-Origin

After (CORS headers present on rejection):
The same request would include Access-Control-Allow-Origin, Access-Control-Allow-Credentials, and Access-Control-Expose-Headers, allowing the browser to read the gRPC error.

When AcceptHeaderLayer rejects a request due to an unsupported SDK
version, it short-circuits the response via futures::future::ready()
which bypasses all downstream middleware. Since the CORS layer was
below AcceptHeaderLayer, rejection responses had no CORS headers,
causing browsers to block the error entirely.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant