<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/"><channel><title>Grpc-Web on kmcd.dev</title><link>https://kmcd.dev/tags/grpc-web/</link><description>Recent content in Grpc-Web on kmcd.dev</description><generator>Hugo -- gohugo.io</generator><language>en</language><copyright>All Rights Reserved</copyright><lastBuildDate>Tue, 14 Jul 2026 10:00:00 +0000</lastBuildDate><atom:link href="https://kmcd.dev/tags/grpc-web/index.xml" rel="self" type="application/rss+xml"/><item><title>gRPC-Web Should Have Fixed gRPC</title><link>https://kmcd.dev/posts/grpc-web-should-have-fixed-grpc/</link><pubDate>Tue, 14 Jul 2026 10:00:00 +0000</pubDate><guid>https://kmcd.dev/posts/grpc-web-should-have-fixed-grpc/</guid><description> 
                
                gRPC-Web should have been the pressure that made gRPC simpler, more inspectable, and better suited for the web.
                </description><content:encoded><![CDATA[<p>gRPC did a lot right. It turned Protocol Buffers into an API design tool, delivering typed messages, generated clients, and strict contracts. For backend networks where you control every hop, it is a fantastic tool.</p>
<p>But gRPC tied itself too tightly to its HTTP/2 transport model. Specifically, it built core application behavior, like delivering final application status codes via response trailers, around protocol features that browser JavaScript could not fully expose.</p>
<p>Browsers support HTTP/2 at the network layer, but frontend JavaScript gets a much narrower API. Standard browser tools like <code>fetch</code> and XHR do not expose HTTP trailers to application code in the way native gRPC depends on. This left gRPC in an awkward position: the browser was using HTTP/2 underneath, but JavaScript could not make a native gRPC call.</p>
<h2 id="the-compromise-grpc-web">The Compromise: gRPC-Web</h2>
<p>gRPC-Web was the official answer to this problem. It adjusted the wire format by encoding trailer-like data directly into the response body, allowing browser clients to function.</p>
<p>It worked, but the deployment story usually required a proxy, which added friction and complexity. Because ordinary gRPC servers did not speak this variant natively, you usually had to run Envoy or another gRPC-Web translation layer just to bridge browser requests into your backend.</p>
<p>We accepted a specialized protocol for backend-to-backend efficiency, but bringing gRPC to the web involved extra parts. gRPC-Web was treated as a weird browser variant on the side instead of a mandate to simplify gRPC itself.</p>
<h2 id="unary-calls-should-be-boring">Unary Calls Should Be Boring</h2>
<p>The real mistake was letting the hardest engineering cases define the common case. Most RPCs are not bidirectional streams. They are ordinary request-response operations: create a thing, fetch a thing, update a thing.</p>
<p>For these unary calls, the protocol should have used standard HTTP semantics.</p>
<p>That means using HTTP’s existing machinery instead of recreating it inside a custom message envelope. <code>Content-Type</code> should say whether the body is JSON or binary protobuf. For successful unary calls, the response can use the same format as the request. Developers should be able to use <code>application/json</code> in browsers or local <code>curl</code> calls for easy debugging, while production workloads can use a binary protobuf content type for better performance. <code>Accept-Encoding</code> should advertise supported compression formats, and <code>Content-Encoding</code> should describe the compression actually used. When the body size is known, <code>Content-Length</code> can say how large it is. When it is not known ahead of time, the underlying HTTP version already has ways to determine where the body ends.</p>
<p>For unary RPCs, gRPC’s extra length prefix and compression flag were not elegant protocol design. They were streaming machinery leaking into the boring case.</p>
<p>You should still define your schema in protobuf, generate your clients, and get type safety. But the network layer should look like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>curl <span style="color:#ebcb8b">\
</span></span></span><span style="display:flex;"><span><span style="color:#ebcb8b"></span>  -H <span style="color:#a3be8c">&#39;Content-Type: application/json&#39;</span> <span style="color:#ebcb8b">\
</span></span></span><span style="display:flex;"><span><span style="color:#ebcb8b"></span>  -d <span style="color:#a3be8c">&#39;{&#34;userId&#34;:&#34;123&#34;}&#39;</span> <span style="color:#ebcb8b">\
</span></span></span><span style="display:flex;"><span><span style="color:#ebcb8b"></span>  https://api.example.com/UserService/GetUser
</span></span></code></pre></div><p>If the operation naturally maps to an HTTP status, use it. If a user exists, return <code>200</code>. If they do not, return <code>404</code>. If the server crashes, return <code>500</code>.</p>
<p>The standard counterargument from RPC purists is that HTTP status codes are too coarse. A <code>404</code> could mean the URL path is missing, or it could mean the requested database resource is missing. To avoid that ambiguity, gRPC often returns <code>200 OK</code> for a successfully handled HTTP request and puts the RPC status in trailers.</p>
<p>But treating transport errors and application errors as completely separate worlds asks too much of the web. Load balancers, API gateways, CDNs, browser tools, and monitoring dashboards already understand <code>4xx</code> and <code>5xx</code> rates. Hiding application failures behind <code>200 OK</code> makes that infrastructure less useful unless every layer becomes protocol-aware.</p>
<p>A web-native design should use standard HTTP status codes for coarse outcomes, then put richer domain-specific error details inside the JSON or binary response body.</p>
<p>Boring HTTP works. Browser dev tools understand it, standard middleboxes log it correctly, and tired engineers can debug it at 2 AM without specialized tools.</p>
<p>A practical protocol split would have been straightforward:</p>
<ul>
<li><strong>Unary calls:</strong> Standard HTTP request-response semantics.</li>
<li><strong>Streaming calls:</strong> Framed messages over a stream-friendly transport.</li>
</ul>
<h2 id="the-web-native-evolution">The Web-Native Evolution</h2>
<p>What I want from “the next version of gRPC” is not exotic. In fact, it already exists; it is exactly how <a href="https://connectrpc.com/" rel="external">ConnectRPC</a> works. Connect preserves the protobuf service model and client generation, but treats unary calls as standard HTTP requests without the complexities of binary framing on top of HTTP. It proves that you can have type-safe contracts without forcing simple calls to cosplay as complex streams.</p>
<p>gRPC-Web should have been the moment gRPC admitted that the web was not a weird edge case. It should have been gRPC v2: protobuf contracts and generated clients on top of boring, inspectable, web-native HTTP, with special framing saved for real streaming instead of forced onto every unary call.</p>
<p>Instead, it became another compatibility layer. Useful, yes. But far less ambitious than it should have been.</p>
]]></content:encoded></item></channel></rss>