gRPC-Web and gRPC Transcoding Through an API Gateway

API7.ai

September 17, 2026

API Gateway Guide

An API gateway can expose one gRPC service through three different contracts: native gRPC proxying, gRPC-Web translation for browsers, or HTTP/JSON-to-gRPC transcoding. These are not interchangeable modes. They differ in clients, wire formats, streaming support, schemas, error handling, and operational risk.

Choose the narrowest contract that the client actually needs, then test it end to end. Protocol translation does not make every HTTP plugin understand Protocol Buffers, preserve every streaming pattern, or turn an internal gRPC service into a well-designed public REST API.

Key Takeaways

  • Use native gRPC when the client can speak gRPC over HTTP/2.
  • Use gRPC-Web for browser clients that use the gRPC-Web protocol and generated stubs.
  • Use transcoding when HTTP/JSON clients need a deliberately designed mapping to a specific gRPC method.
  • Set deadlines and message limits explicitly, and test gRPC status, trailers, cancellation, and streaming behavior.
  • Treat authentication metadata, CORS, and public JSON schemas as independent security and compatibility contracts.

Choose the Protocol Contract First

Client needGateway modeUpstreamMain boundary
Service-to-service gRPCNative proxygRPC or gRPCSPreserve HTTP/2 gRPC semantics
Browser with generated gRPC-Web clientgRPC-Web pluginNative gRPCTranslate browser-compatible framing and CORS
REST-like HTTP/JSON clientgRPC transcodingNative gRPCMap one public HTTP contract to one proto service method

Native proxying is the least transformative. The client and server share the protobuf service definition and gRPC status model. The gateway still needs HTTP/2, TLS, routing, authentication, observability, and plugin-compatibility tests.

gRPC-Web exists because browser APIs do not expose native gRPC transport directly. The official gRPC-Web 2.1.1 project README documents unary RPCs in binary or text mode and server-side streaming in grpcwebtext mode; client-streaming and bidirectional streaming are not supported by that pinned client model. Confirm the exact client library and version rather than generalizing native gRPC capabilities to browsers.

Transcoding creates a distinct public HTTP API. The path, HTTP method, query and body mapping, JSON number representation, error mapping, deadline, and authorization policy become part of that API's compatibility contract.

Keep the Translation Boundaries Visible

flowchart LR
    N[Native gRPC client] -->|gRPC over HTTP/2| G[API gateway]
    B[Browser client] -->|gRPC-Web| G
    H[HTTP and JSON client] -->|HTTP and JSON| G
    G -->|Native gRPC| S[gRPC service]
    G -. gRPC-Web framing and CORS .-> B
    G -. Proto-based HTTP and JSON mapping .-> H

The solid arrows are request paths; the dotted arrows label gateway-owned translation contracts. Authentication and authorization must be applied to each public contract. A browser Origin accepted by CORS is not an authenticated user, and a JSON field mapped into protobuf is not trusted merely because it decoded successfully.

Proxy gRPC-Web for Browser Clients

Apache APISIX 3.19.0's grpc-web plugin accepts documented gRPC-Web content types, decodes binary or base64 text messages, and forwards native gRPC to the upstream. This excerpt uses a prefix route because the request path includes the package, service, and method. It is not a complete production route: add authentication, authorization, upstream TLS, health checks, and an origin policy appropriate to the application.

{ "uri": "/grpc/web/*", "methods": ["POST", "OPTIONS"], "plugins": { "grpc-web": { "max_req_body_size": 1048576, "cors_allow_headers": "content-type,x-grpc-web,x-user-agent,authorization" } }, "upstream": { "scheme": "grpc", "type": "roundrobin", "nodes": { "greeter.internal:50051": 1 } } }

The plugin's max_req_body_size defaults to 67,108,864 bytes. This example deliberately lowers it to 1 MiB; select a limit from the protobuf contract and workload, not from the default alone. An oversized or unreadable body is rejected before the service can apply field-level validation.

The plugin handles gRPC-Web CORS preflight and allows all origins by default. cors_allow_headers controls the allowed request-header list; it does not authenticate an Origin or user. Review whether a separate browser edge or application control must restrict origins, and keep bearer tokens out of URLs. CORS protects browser behavior, not the service from non-browser clients.

Test all content types used by the generated client. Binary application/grpc-web+proto and base64 text application/grpc-web-text+proto have different framing and streaming support in the standard client. Also test cancellation, large responses, trailer delivery, expired tokens, and a preflight that requests the exact authorization headers.

Transcode One Deliberate HTTP Contract

Apache APISIX's grpc-transcode plugin requires a Proto resource plus an exact service and method. First register the service definition. This example is a minimal unary method; imported proto files should be compiled into a descriptor set with their dependencies as documented by APISIX.

{ "content": "syntax = \"proto3\"; package orders; service OrderService { rpc GetOrder (GetOrderRequest) returns (GetOrderReply); } message GetOrderRequest { string order_id = 1; } message GetOrderReply { string order_id = 1; int64 created_at_unix_ms = 2; }" }

Then bind one public route to that proto, service, and method:

{ "uri": "/v1/orders", "methods": ["GET"], "plugins": { "grpc-transcode": { "proto_id": "orders-v1", "service": "orders.OrderService", "method": "GetOrder", "deadline": 1000, "max_resp_body_size": 1048576, "pb_option": [ "enum_as_name", "int64_as_string", "auto_default_values", "disable_hooks" ] } }, "upstream": { "scheme": "grpc", "type": "roundrobin", "nodes": { "orders.internal:50051": 1 } } }

The route is a configuration excerpt. The request would supply order_id according to the plugin's documented field mapping. The deadline is 1,000 ms, and int64_as_string prevents a 64-bit integer from being forced into a JSON number that a JavaScript client may not represent exactly. The public OpenAPI or other HTTP contract must describe the resulting string type, not the internal protobuf integer type.

max_resp_body_size defaults to 67,108,864 bytes. This route lowers the in-memory transcoding buffer to 1 MiB. Use this setting only when the upstream contract or another enforced upstream limit keeps the complete encoded gRPC response, including framing, strictly below 1 MiB; otherwise paginate, bound the upstream result, or choose a larger validated buffer. At the cap, APISIX's body buffer can pass a truncated byte prefix to protobuf decoding. Decoding may fail, but a prefix ending at a field boundary can also produce valid, incomplete JSON under the original HTTP success status. The plugin does not reliably signal truncation or generate an alert. Test responses below, at, and above the limit, and monitor the upstream response-size bound independently.

show_status_in_body is a behavior-changing Boolean in grpc-transcode and defaults to false. With false, APISIX does not add decoded grpc-status-details-bin content to the response body for an error status. With true, it decodes and exposes those details in the JSON body. Enable the true path only after reviewing information disclosure and client compatibility; neither setting replaces an explicit public error schema.

Design Deadlines, Cancellation, and Retries Together

The gRPC deadline guide notes that clients do not set a deadline by default. Without a deadline, a call can wait indefinitely. Define a total client budget, subtract gateway and network time, propagate the remaining deadline where supported, and make the upstream stop work after cancellation.

Do not layer independent full-length timeouts at the browser, gateway, transcoder, and service. Their combined behavior can leave expensive work running after the caller has gone. Observe client cancellation, gateway termination, upstream cancellation, and service completion separately.

Retry only methods whose semantics permit it. A unary read may be retryable within one deadline; a state-changing method needs application idempotency. Streaming reconnection needs a resume token or application offset if messages must not be lost or duplicated. A gateway cannot infer those contracts from rpc syntax alone.

Preserve gRPC Status and Public Error Meaning

Native gRPC reports status in trailers. gRPC-Web carries protocol status through its browser-compatible framing. Transcoding may produce an HTTP status and JSON body. Monitoring only HTTP 200 misses application-level gRPC failures; blindly rewriting every gRPC code into one HTTP code destroys useful semantics.

Create and test an explicit mapping for the public contract:

  • unauthenticated and permission-denied results;
  • invalid arguments and failed preconditions;
  • not found and already exists;
  • resource exhaustion and overload;
  • deadline exceeded and unavailable;
  • internal errors with redacted details.

Keep correlation metadata separate from trusted identity. If the caller can supply a request ID, validate its size and format and do not treat it as proof of the authenticated principal.

Test Plugin and Observability Compatibility

An HTTP-header plugin may work across all three modes, while a plugin that expects a JSON body may not understand binary gRPC or gRPC-Web frames. Test authentication, rate limiting, logging, body limits, transformations, and tracing against the exact mode and message size.

For native gRPC and gRPC-Web, capture service and method, gRPC status, duration, request and response bytes, deadline outcome, and stream lifetime without logging sensitive protobuf payloads. For transcoding, add the public route and HTTP outcome while retaining the upstream gRPC status for diagnosis.

gRPC Gateway Verification Checklist

  • Is each route explicitly native gRPC, gRPC-Web, or transcoded HTTP/JSON?
  • Does the chosen client mode support the required streaming pattern?
  • Is the complete encoded response guaranteed to stay below the transcoding buffer limit?
  • Are CORS and authentication treated as different controls?
  • Is every transcoded field and JSON number representation documented?
  • Are deadline and cancellation behavior tested across all hops?
  • Are retries limited to methods with proven safe semantics?
  • Are gRPC status, trailers, and public HTTP errors observable?
  • Has each configured plugin been tested with the exact protocol mode?

FAQ

Can a browser call a native gRPC service directly?

Ordinary browser APIs use gRPC-Web rather than native gRPC transport. A gateway or compatible proxy translates gRPC-Web to native gRPC for the service.

Does transcoding automatically create a REST API?

It creates a protocol mapping, not a complete API design. You still own resource naming, HTTP semantics, schemas, errors, authentication, compatibility, and documentation.

Should every gRPC error become HTTP 500?

No. Define an intentional mapping for the public contract and retain the original gRPC status for diagnostics. Avoid exposing internal status details that reveal sensitive implementation information.

Next Steps

Add timeout and retry budgets, fine-grained authorization, and API access-log evidence to the chosen protocol contract.

Share article link