Summary
The gRPC API can't be called from a browser today, so any web frontend has to run a gRPC-Web proxy (Envoy, grpcwebproxy, ...) in front of the node. Accepting gRPC-Web directly would remove that extra hop and keep ldk-server a single binary for that use case.
Why a browser can't call the API today
Three code-level blockers, plus one deployment concern:
-
The status lives in HTTP/2 trailers. GrpcBody delivers grpc-status / grpc-message as http_body::Frame::trailers (ldk-server-grpc/src/grpc.rs). The browser fetch API cannot read trailers, so a page has no way to tell success from failure on any response that carries a body. (The Empty / trailers-only path puts the status in the response headers, which a browser can read — so errors raised before the handler runs are visible, but normal responses are not.) gRPC-Web exists to solve exactly this: the trailers go out as a length-prefixed body frame whose flag byte has the MSB set (0x80).
-
The content type is rejected. validate_grpc_request accepts only application/grpc and application/grpc+proto; a browser client sends application/grpc-web+proto.
-
No CORS. There is no OPTIONS handling and no Access-Control-* headers anywhere, so a page on any other origin is blocked before the request leaves. The trailers-only case above would additionally need Access-Control-Expose-Headers: grpc-status, grpc-message.
-
Self-signed TLS (deployment concern, not a code change). The auto-generated cert at <storage_dir>/tls.crt is meant to be pinned by clients, and page JavaScript cannot pin a certificate — an operator exposing the API to a browser would front it with a normally-trusted cert.
HTTP/2 itself is not a blocker: ALPN already advertises h2 (ldk-server/src/util/tls.rs) and browsers speak HTTP/2.
What would not have to change
The HMAC scheme is unaffected. x-auth signs timestamp_be_bytes || grpc_request_body_bytes including the 5-byte frame header, and a gRPC-Web request body is byte-identical to a gRPC one — only the response framing and the headers differ. A browser can compute the same signature with WebCrypto, and the signature also survives a translating proxy unchanged.
There is already precedent in the service for a non-gRPC-shaped request: GET /metrics returns GrpcBody::Plain from the same NodeService.
Possible shape of the change
Roughly:
- accept
application/grpc-web+proto and application/grpc-web in validate_grpc_request, and carry which dialect the request used through to the response
- for gRPC-Web requests, end the body with a
0x80-flagged length-prefixed trailers frame instead of HTTP trailers — GrpcBody::Unary and GrpcBody::Stream already own that ending
- answer
OPTIONS preflight, and set Access-Control-Allow-Origin, Access-Control-Allow-Headers: content-type, x-grpc-web, x-auth, and Access-Control-Expose-Headers: grpc-status, grpc-message
- gate the CORS part behind config (e.g.
[api] cors_allowed_origins = [], empty by default) so existing deployments are untouched and the browser surface is opt-in
SubscribeEvents is fine under gRPC-Web — server streaming is supported by the protocol; only client-streaming and bidirectional streaming are not, and the API has neither.
Context
I'm building a browser-based management UI against this API and currently ship an Envoy config alongside it to do the translation. It works, and the HMAC auth passes through it untouched, but it's an extra process to deploy and configure for what is otherwise a single-binary node.
Happy to put a patch together if this direction sounds reasonable — wanted to check the appetite and preferred shape first.
Summary
The gRPC API can't be called from a browser today, so any web frontend has to run a gRPC-Web proxy (Envoy,
grpcwebproxy, ...) in front of the node. Accepting gRPC-Web directly would remove that extra hop and keepldk-servera single binary for that use case.Why a browser can't call the API today
Three code-level blockers, plus one deployment concern:
The status lives in HTTP/2 trailers.
GrpcBodydeliversgrpc-status/grpc-messageashttp_body::Frame::trailers(ldk-server-grpc/src/grpc.rs). The browserfetchAPI cannot read trailers, so a page has no way to tell success from failure on any response that carries a body. (TheEmpty/ trailers-only path puts the status in the response headers, which a browser can read — so errors raised before the handler runs are visible, but normal responses are not.) gRPC-Web exists to solve exactly this: the trailers go out as a length-prefixed body frame whose flag byte has the MSB set (0x80).The content type is rejected.
validate_grpc_requestaccepts onlyapplication/grpcandapplication/grpc+proto; a browser client sendsapplication/grpc-web+proto.No CORS. There is no
OPTIONShandling and noAccess-Control-*headers anywhere, so a page on any other origin is blocked before the request leaves. The trailers-only case above would additionally needAccess-Control-Expose-Headers: grpc-status, grpc-message.Self-signed TLS (deployment concern, not a code change). The auto-generated cert at
<storage_dir>/tls.crtis meant to be pinned by clients, and page JavaScript cannot pin a certificate — an operator exposing the API to a browser would front it with a normally-trusted cert.HTTP/2 itself is not a blocker: ALPN already advertises
h2(ldk-server/src/util/tls.rs) and browsers speak HTTP/2.What would not have to change
The HMAC scheme is unaffected.
x-authsignstimestamp_be_bytes || grpc_request_body_bytesincluding the 5-byte frame header, and a gRPC-Web request body is byte-identical to a gRPC one — only the response framing and the headers differ. A browser can compute the same signature with WebCrypto, and the signature also survives a translating proxy unchanged.There is already precedent in the service for a non-gRPC-shaped request:
GET /metricsreturnsGrpcBody::Plainfrom the sameNodeService.Possible shape of the change
Roughly:
application/grpc-web+protoandapplication/grpc-webinvalidate_grpc_request, and carry which dialect the request used through to the response0x80-flagged length-prefixed trailers frame instead of HTTP trailers —GrpcBody::UnaryandGrpcBody::Streamalready own that endingOPTIONSpreflight, and setAccess-Control-Allow-Origin,Access-Control-Allow-Headers: content-type, x-grpc-web, x-auth, andAccess-Control-Expose-Headers: grpc-status, grpc-message[api] cors_allowed_origins = [], empty by default) so existing deployments are untouched and the browser surface is opt-inSubscribeEventsis fine under gRPC-Web — server streaming is supported by the protocol; only client-streaming and bidirectional streaming are not, and the API has neither.Context
I'm building a browser-based management UI against this API and currently ship an Envoy config alongside it to do the translation. It works, and the HMAC auth passes through it untouched, but it's an extra process to deploy and configure for what is otherwise a single-binary node.
Happy to put a patch together if this direction sounds reasonable — wanted to check the appetite and preferred shape first.