<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>Quic on kmcd.dev</title><link>https://kmcd.dev/tags/quic/</link><description>Recent content in Quic on kmcd.dev</description><generator>Hugo -- gohugo.io</generator><language>en</language><copyright>All Rights Reserved</copyright><lastBuildDate>Thu, 12 Nov 2026 10:00:00 +0000</lastBuildDate><atom:link href="https://kmcd.dev/tags/quic/index.xml" rel="self" type="application/rss+xml"/><item><title>HTTP/3 From Scratch: Part 1</title><link>https://kmcd.dev/posts/http3-from-scratch-part-1/</link><pubDate>Thu, 12 Nov 2026 10:00:00 +0000</pubDate><guid>https://kmcd.dev/posts/http3-from-scratch-part-1/</guid><description> 
                &lt;p> &lt;img hspace="5" src="https://kmcd.dev/posts/http3-from-scratch-part-1/cover.svg" /> &lt;/p>
                
                Building HTTP/3 from scratch: HTTP/3 framing and QPACK over QUIC streams in Go.
                </description><content:encoded><![CDATA[<p>In my previous posts in this series (<a href="https://kmcd.dev/posts/http0.9-from-scratch/">HTTP/0.9</a>, <a href="https://kmcd.dev/posts/http1.0-from-scratch/">HTTP/1.0</a>, <a href="https://kmcd.dev/posts/http1.1-from-scratch/">HTTP/1.1</a>, and <a href="https://kmcd.dev/posts/http2-from-scratch-part-1/">HTTP/2</a>), I always started with a raw TCP socket. We would dial a port, wrap the connection in TLS, and then build the HTTP parser on top of it.</p>
<p>For HTTP/3, I&rsquo;m doing the opposite and building from the sky, down.</p>
<p>HTTP/3 doesn&rsquo;t use TCP. It runs over QUIC, which is built on UDP and implements its own encryption (TLS 1.3) and streams. If we started with a raw UDP socket today, we would have to write a lot of QUIC packet and crypto code before we could send a single HTTP request. I didn&rsquo;t want to spend two or three posts on UDP plumbing before even touching HTTP/3.</p>
<p>Instead, we&rsquo;re going to start at the top using <a href="https://github.com/quic-go/quic-go" rel="external"><code>quic-go</code></a> for the QUIC streams in Part 1 and Part 2 while we build our HTTP/3 framing, QPACK encoder/decoder, server, and client. Once our HTTP/3 implementation works against <code>curl</code> and browsers, we&rsquo;ll come back in Part 3, remove <code>quic-go</code>, and write a basic QUIC layer over UDP sockets.</p>
<p>All of the code for this post is in Go and is linked on GitHub at the bottom of the page.</p>
<h3 id="sending-bytes-over-quic">Sending Bytes Over QUIC</h3>
<p>Okay, let&rsquo;s get some bytes moving. Before we build HTTP/3, we&rsquo;ll give QUIC a quick test drive with an echo server. It is going to be the foundation of everything we build here, after all. Accept a connection, accept a stream, and send back whatever comes in:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">conn</span><span class="p">,</span><span class="w"> </span><span class="nx">_</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">listener</span><span class="p">.</span><span class="nf">Accept</span><span class="p">(</span><span class="nx">ctx</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nx">stream</span><span class="p">,</span><span class="w"> </span><span class="nx">_</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">conn</span><span class="p">.</span><span class="nf">AcceptStream</span><span class="p">(</span><span class="nx">ctx</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nx">data</span><span class="p">,</span><span class="w"> </span><span class="nx">_</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">io</span><span class="p">.</span><span class="nf">ReadAll</span><span class="p">(</span><span class="nx">stream</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nx">stream</span><span class="p">.</span><span class="nf">Write</span><span class="p">(</span><span class="nb">append</span><span class="p">([]</span><span class="nb">byte</span><span class="p">(</span><span class="s">&#34;echo: &#34;</span><span class="p">),</span><span class="w"> </span><span class="nx">data</span><span class="o">...</span><span class="p">))</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nx">stream</span><span class="p">.</span><span class="nf">Close</span><span class="p">()</span><span class="w">
</span></span></span></code></pre></div><p>Dialing from the client works the same way. As I wrote about in <a href="https://kmcd.dev/posts/grpc-over-http3/">gRPC Over HTTP/3</a>, QUIC always uses TLS 1.3 and checks the ALPN string (<code>tls.Config.NextProtos</code>) during the handshake, so we pass a <code>tls.Config</code> into <code>quic.DialAddr</code>, open a stream, write our bytes, and read back the response:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">conn</span><span class="p">,</span><span class="w"> </span><span class="nx">_</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">quic</span><span class="p">.</span><span class="nf">DialAddr</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span><span class="w"> </span><span class="nx">addr</span><span class="p">,</span><span class="w"> </span><span class="nx">tlsConf</span><span class="p">,</span><span class="w"> </span><span class="kc">nil</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">defer</span><span class="w"> </span><span class="nx">conn</span><span class="p">.</span><span class="nf">CloseWithError</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="s">&#34;done&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nx">stream</span><span class="p">,</span><span class="w"> </span><span class="nx">_</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">conn</span><span class="p">.</span><span class="nf">OpenStreamSync</span><span class="p">(</span><span class="nx">ctx</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nx">stream</span><span class="p">.</span><span class="nf">Write</span><span class="p">([]</span><span class="nb">byte</span><span class="p">(</span><span class="s">&#34;Hello over QUIC!&#34;</span><span class="p">))</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nx">stream</span><span class="p">.</span><span class="nf">Close</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nx">reply</span><span class="p">,</span><span class="w"> </span><span class="nx">_</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">io</span><span class="p">.</span><span class="nf">ReadAll</span><span class="p">(</span><span class="nx">stream</span><span class="p">)</span><span class="w">
</span></span></span></code></pre></div><p>Notice that the client calls <code>stream.Close()</code> <em>before</em> calling <code>io.ReadAll(stream)</code>. In <code>quic-go</code>, closing a stream only closes our write side (sending a FIN bit on that stream), so we can still read the server&rsquo;s response afterward. Because QUIC streams already have their own FIN bit, HTTP/3 frames don&rsquo;t need an <code>END_STREAM</code> flag like HTTP/2 had.</p>
<p>The server also needs to leave the connection open long enough for the client to read the reply. Calling <code>conn.CloseWithError</code> right after writing can cut that short. For this echo server, we close the stream and wait on <code>&lt;-conn.Context().Done()</code> until the client disconnects.</p>
<h3 id="reusing-http2-framing">Reusing HTTP/2 Framing</h3>
<p>When you open <a href="https://www.rfc-editor.org/rfc/rfc9114.html" rel="external">RFC 9114 (HTTP/3)</a>, you get a strong sense of déjà vu if you&rsquo;ve read the HTTP/2 spec.</p>

    
    
        
        
            
        
        
        
        
        
        
            
        
        
        
            <img src="https://kmcd.dev/posts/http3-from-scratch-part-1/h3-unmasking_hu_7e1b44832ad03596.webp"
                 class="img-fade-in center"
                 onload="this.classList.add('is-loaded')"
                 alt="" width="420px" height="960"/>
        
    


<p>In <a href="https://kmcd.dev/posts/http2-from-scratch-part-2/">HTTP/2 From Scratch: Part 2</a>, we built a parser for HTTP/2&rsquo;s 9-byte frame header (Length, Type, Flags, and Stream ID). All of those frames shared a single TCP connection, so HTTP/2 had to keep track of the streams itself. In HTTP/3, each request gets its own QUIC stream. With QUIC handling more of the bookkeeping, our frame header gets a lot smaller: just <strong>Type</strong> and <strong>Length</strong>:</p>

    
    
        
        
        
        
        
        
        
        
        <div class="svg-diagram-inline center"
                 style="display: block; max-width: 100%; width: 800px; margin-left: auto; margin-right: auto;">
                <svg class="h3-diagram" viewBox="0 0 880 260" xmlns="http://www.w3.org/2000/svg" font-family="Inter, ui-sans-serif, system-ui, sans-serif">
  <style>
    .h3-diagram {
      --bg-neutral: #1E1E2E;
      --c-border: #585B70;
      --text-heading: #CDD6F4;
      --text-main: #CDD6F4;
      --text-sub: #A6ADC8;
      --c-blue: #89B4FA;
      --bg-blue: #1E1E2E;
      --c-green: #A6E3A1;
      --bg-green: #1E1E2E;
      --c-purple: #CBA6F7;
      --bg-purple: #1E1E2E;
      --c-yellow: #F9E2AF;
      --bg-yellow: #1E1E2E;
      --c-red: #F38BA8;
      --bg-red: #1E1E2E;
    }
    @media (prefers-color-scheme: light) {
      :root:not([data-theme="dark"]) .h3-diagram {
        --bg-neutral: #F6EFE0;
        --c-border: #475569;
        --text-heading: #141A2E;
        --text-main: #141A2E;
        --text-sub: #434E70;
        --c-blue: #0369A1;
        --bg-blue: #EDF6FF;
        --c-green: #0F766E;
        --bg-green: #E6F7F4;
        --c-purple: #6D28D9;
        --bg-purple: #F3E8FF;
        --c-yellow: #B45309;
        --bg-yellow: #FFF6D6;
        --c-red: #B8251B;
        --bg-red: #FFE5E0;
      }
    }
    [data-theme="light"] .h3-diagram {
      --bg-neutral: #F6EFE0;
      --c-border: #475569;
      --text-heading: #141A2E;
      --text-main: #141A2E;
      --text-sub: #434E70;
      --c-blue: #0369A1;
      --bg-blue: #EDF6FF;
      --c-green: #0F766E;
      --bg-green: #E6F7F4;
      --c-purple: #6D28D9;
      --bg-purple: #F3E8FF;
      --c-yellow: #B45309;
      --bg-yellow: #FFF6D6;
      --c-red: #B8251B;
      --bg-red: #FFE5E0;
    }
  </style>

  <!-- HTTP/2 Row -->
  <text x="20" y="32" fill="var(--text-heading)" font-size="14" font-weight="700">HTTP/2 Frame</text>
  <g transform="translate(20 44)" font-family="ui-monospace, SFMono-Regular, Menlo, monospace">
    <rect x="0" y="0" width="140" height="64" rx="8" fill="var(--bg-blue)" stroke="var(--c-blue)" stroke-width="2" />
    <text x="70" y="30" fill="var(--text-main)" font-size="16" font-weight="700" text-anchor="middle">Length</text>
    <text x="70" y="50" fill="var(--text-sub)" font-size="12" text-anchor="middle">24 bits</text>

    <rect x="150" y="0" width="110" height="64" rx="8" fill="var(--bg-green)" stroke="var(--c-green)" stroke-width="2" />
    <text x="205" y="30" fill="var(--text-main)" font-size="16" font-weight="700" text-anchor="middle">Type</text>
    <text x="205" y="50" fill="var(--text-sub)" font-size="12" text-anchor="middle">8 bits</text>

    <rect x="270" y="0" width="120" height="64" rx="8" fill="var(--bg-red)" stroke="var(--c-red)" stroke-width="2" stroke-dasharray="5,4" opacity="0.85" />
    <text x="330" y="30" fill="var(--c-red)" font-size="16" font-weight="700" text-anchor="middle" text-decoration="line-through">Flags</text>
    <text x="330" y="50" fill="var(--c-red)" font-size="12" text-anchor="middle">8 bits</text>

    <rect x="400" y="0" width="180" height="64" rx="8" fill="var(--bg-red)" stroke="var(--c-red)" stroke-width="2" stroke-dasharray="5,4" opacity="0.85" />
    <text x="490" y="30" fill="var(--c-red)" font-size="16" font-weight="700" text-anchor="middle" text-decoration="line-through">Stream ID</text>
    <text x="490" y="50" fill="var(--c-red)" font-size="12" text-anchor="middle">1 + 31 bits</text>

    <rect x="590" y="0" width="250" height="64" rx="8" fill="var(--bg-yellow)" stroke="var(--c-yellow)" stroke-width="2" />
    <text x="715" y="30" fill="var(--text-main)" font-size="16" font-weight="700" text-anchor="middle">Payload</text>
    <text x="715" y="50" fill="var(--text-sub)" font-size="12" text-anchor="middle">Length bytes</text>
  </g>

  <!-- HTTP/3 Row -->
  <text x="20" y="162" fill="var(--text-heading)" font-size="14" font-weight="700">HTTP/3 Frame</text>
  <g transform="translate(20 174)" font-family="ui-monospace, SFMono-Regular, Menlo, monospace">
    <rect x="0" y="0" width="220" height="64" rx="8" fill="var(--bg-green)" stroke="var(--c-green)" stroke-width="2" />
    <text x="110" y="30" fill="var(--text-main)" font-size="16" font-weight="700" text-anchor="middle">Type</text>
    <text x="110" y="50" fill="var(--text-sub)" font-size="12" text-anchor="middle">Varint (1–8B)</text>

    <rect x="232" y="0" width="220" height="64" rx="8" fill="var(--bg-blue)" stroke="var(--c-blue)" stroke-width="2" />
    <text x="342" y="30" fill="var(--text-main)" font-size="16" font-weight="700" text-anchor="middle">Length</text>
    <text x="342" y="50" fill="var(--text-sub)" font-size="12" text-anchor="middle">Varint (1–8B)</text>

    <rect x="464" y="0" width="376" height="64" rx="8" fill="var(--bg-yellow)" stroke="var(--c-yellow)" stroke-width="2" />
    <text x="652" y="30" fill="var(--text-main)" font-size="16" font-weight="700" text-anchor="middle">Payload</text>
    <text x="652" y="50" fill="var(--text-sub)" font-size="12" text-anchor="middle">Length bytes</text>
  </g>
</svg>

            </div>
        
    


<p>The remaining frame types keep the same numeric values we used in HTTP/2:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">const</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">FrameData</span><span class="w">     </span><span class="kt">uint64</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="mh">0x00</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">FrameHeaders</span><span class="w">  </span><span class="kt">uint64</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="mh">0x01</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">FrameSettings</span><span class="w"> </span><span class="kt">uint64</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="mh">0x04</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">FrameGoAway</span><span class="w">   </span><span class="kt">uint64</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="mh">0x07</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="kd">type</span><span class="w"> </span><span class="nx">Frame</span><span class="w"> </span><span class="kd">struct</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">Type</span><span class="w">    </span><span class="kt">uint64</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">Payload</span><span class="w"> </span><span class="p">[]</span><span class="kt">byte</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>Several frame types from HTTP/2 (<code>PRIORITY</code>, <code>RST_STREAM</code>, <code>PUSH_PROMISE</code>, and <code>PING</code>) are gone here. Stream resets and pings moved into QUIC itself, and priority frames and Server Push were removed.</p>
<p>The main difference in the wire format is that Type and Length use <a href="https://www.rfc-editor.org/rfc/rfc9000.html#name-variable-length-integer-enc" rel="external">QUIC variable-length integers</a> instead of fixed-width integers. In <a href="https://kmcd.dev/posts/grpc-from-scratch-part-3/#big-numbers">gRPC From Scratch: Part 3</a>, we covered Protobuf varints, which use the most significant bit of each byte as a continuation flag. Instead of checking a continuation bit on every byte, a QUIC varint uses the top two bits of the first byte to encode whether the integer is 1, 2, 4, or 8 bytes long. This approach ends up being faster to process than the protobuf version.</p>

    
    
        
        
        
        
        
        
        
        
        <div class="svg-diagram-inline center"
                 style="display: block; max-width: 100%; width: 800px; margin-left: auto; margin-right: auto;">
                <svg class="h3-diagram" viewBox="0 0 880 185" xmlns="http://www.w3.org/2000/svg" font-family="Inter, ui-sans-serif, system-ui, sans-serif">
  <style>
    .h3-diagram {
      --bg-neutral: #1E1E2E;
      --c-border: #585B70;
      --text-heading: #CDD6F4;
      --text-main: #CDD6F4;
      --text-sub: #A6ADC8;
      --c-blue: #89B4FA;
      --bg-blue: #1E1E2E;
      --c-purple: #CBA6F7;
      --bg-purple: #313244;
    }
    @media (prefers-color-scheme: light) {
      :root:not([data-theme="dark"]) .h3-diagram {
        --bg-neutral: #F6EFE0;
        --c-border: #475569;
        --text-heading: #141A2E;
        --text-main: #141A2E;
        --text-sub: #434E70;
        --c-blue: #0369A1;
        --bg-blue: #EDF6FF;
        --c-purple: #6D28D9;
        --bg-purple: #F3E8FF;
      }
    }
    [data-theme="light"] .h3-diagram {
      --bg-neutral: #F6EFE0;
      --c-border: #475569;
      --text-heading: #141A2E;
      --text-main: #141A2E;
      --text-sub: #434E70;
      --c-blue: #0369A1;
      --bg-blue: #EDF6FF;
      --c-purple: #6D28D9;
      --bg-purple: #F3E8FF;
    }
  </style>

  <!-- First Byte Bits -->
  <text x="20" y="28" fill="var(--text-heading)" font-size="14" font-weight="700">First Byte</text>
  <g transform="translate(20 40)" font-family="ui-monospace, SFMono-Regular, Menlo, monospace">
    <!-- 2-bit prefix -->
    <rect x="0" y="0" width="46" height="52" rx="6" fill="var(--bg-purple)" stroke="var(--c-purple)" stroke-width="2" />
    <text x="23" y="32" fill="var(--c-purple)" font-size="16" font-weight="700" text-anchor="middle">b7</text>
    <rect x="52" y="0" width="46" height="52" rx="6" fill="var(--bg-purple)" stroke="var(--c-purple)" stroke-width="2" />
    <text x="75" y="32" fill="var(--c-purple)" font-size="16" font-weight="700" text-anchor="middle">b6</text>

    <!-- 6-bit value -->
    <rect x="110" y="0" width="46" height="52" rx="6" fill="var(--bg-blue)" stroke="var(--c-blue)" stroke-width="2" />
    <text x="133" y="32" fill="var(--c-blue)" font-size="15" font-weight="700" text-anchor="middle">b5</text>
    <rect x="162" y="0" width="46" height="52" rx="6" fill="var(--bg-blue)" stroke="var(--c-blue)" stroke-width="2" />
    <text x="185" y="32" fill="var(--c-blue)" font-size="15" font-weight="700" text-anchor="middle">b4</text>
    <rect x="214" y="0" width="46" height="52" rx="6" fill="var(--bg-blue)" stroke="var(--c-blue)" stroke-width="2" />
    <text x="237" y="32" fill="var(--c-blue)" font-size="15" font-weight="700" text-anchor="middle">b3</text>
    <rect x="266" y="0" width="46" height="52" rx="6" fill="var(--bg-blue)" stroke="var(--c-blue)" stroke-width="2" />
    <text x="289" y="32" fill="var(--c-blue)" font-size="15" font-weight="700" text-anchor="middle">b2</text>
    <rect x="318" y="0" width="46" height="52" rx="6" fill="var(--bg-blue)" stroke="var(--c-blue)" stroke-width="2" />
    <text x="341" y="32" fill="var(--c-blue)" font-size="15" font-weight="700" text-anchor="middle">b1</text>
    <rect x="370" y="0" width="46" height="52" rx="6" fill="var(--bg-blue)" stroke="var(--c-blue)" stroke-width="2" />
    <text x="393" y="32" fill="var(--c-blue)" font-size="15" font-weight="700" text-anchor="middle">b0</text>

    <path d="M 4 62 L 4 70 L 94 70 L 94 62" fill="none" stroke="var(--c-purple)" stroke-width="2" />
    <text x="49" y="88" fill="var(--c-purple)" font-size="13" font-weight="700" text-anchor="middle">Length Prefix</text>
    <text x="49" y="106" fill="var(--text-sub)" font-size="12" font-weight="600" text-anchor="middle">b &gt;&gt; 6</text>

    <path d="M 114 62 L 114 70 L 412 70 L 412 62" fill="none" stroke="var(--c-blue)" stroke-width="2" />
    <text x="263" y="88" fill="var(--c-blue)" font-size="13" font-weight="700" text-anchor="middle">Value Bits</text>
    <text x="263" y="106" fill="var(--text-sub)" font-size="12" font-weight="600" text-anchor="middle">b &amp; 0x3f</text>
  </g>

  <!-- Prefix Legend on Right -->
  <text x="485" y="28" fill="var(--text-heading)" font-size="14" font-weight="700">Prefix Values</text>
  <g transform="translate(485 40)" font-family="ui-monospace, SFMono-Regular, Menlo, monospace" font-size="14">
    <rect x="0" y="0" width="180" height="52" rx="6" fill="var(--bg-neutral)" stroke="var(--c-border)" stroke-width="1.5" />
    <text x="90" y="24" fill="var(--c-purple)" font-weight="700" text-anchor="middle">00 → 1 Byte</text>
    <text x="90" y="42" fill="var(--text-sub)" font-size="12" text-anchor="middle">6-bit (max 63)</text>

    <rect x="194" y="0" width="180" height="52" rx="6" fill="var(--bg-neutral)" stroke="var(--c-border)" stroke-width="1.5" />
    <text x="284" y="24" fill="var(--c-purple)" font-weight="700" text-anchor="middle">01 → 2 Bytes</text>
    <text x="284" y="42" fill="var(--text-sub)" font-size="12" text-anchor="middle">14-bit (max 16,383)</text>

    <rect x="0" y="64" width="180" height="52" rx="6" fill="var(--bg-neutral)" stroke="var(--c-border)" stroke-width="1.5" />
    <text x="90" y="88" fill="var(--c-purple)" font-weight="700" text-anchor="middle">10 → 4 Bytes</text>
    <text x="90" y="106" fill="var(--text-sub)" font-size="12" text-anchor="middle">30-bit</text>

    <rect x="194" y="64" width="180" height="52" rx="6" fill="var(--bg-neutral)" stroke="var(--c-border)" stroke-width="1.5" />
    <text x="284" y="88" fill="var(--c-purple)" font-weight="700" text-anchor="middle">11 → 8 Bytes</text>
    <text x="284" y="106" fill="var(--text-sub)" font-size="12" text-anchor="middle">62-bit</text>
  </g>
</svg>

            </div>
        
    


<p>In <code>ReadVarint</code>, we read that first byte, shift its top two bits down with <code>first[0] &gt;&gt; 6</code>, and use <code>1 &lt;&lt; prefix</code> to figure out how many bytes to read:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="nf">ReadVarint</span><span class="p">(</span><span class="nx">r</span><span class="w"> </span><span class="nx">io</span><span class="p">.</span><span class="nx">Reader</span><span class="p">)</span><span class="w"> </span><span class="p">(</span><span class="kt">uint64</span><span class="p">,</span><span class="w"> </span><span class="kt">error</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="kd">var</span><span class="w"> </span><span class="nx">first</span><span class="w"> </span><span class="p">[</span><span class="mi">1</span><span class="p">]</span><span class="kt">byte</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">if</span><span class="w"> </span><span class="nx">_</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">io</span><span class="p">.</span><span class="nf">ReadFull</span><span class="p">(</span><span class="nx">r</span><span class="p">,</span><span class="w"> </span><span class="nx">first</span><span class="p">[:]);</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">return</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">prefix</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">first</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="w"> </span><span class="o">&gt;&gt;</span><span class="w"> </span><span class="mi">6</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">length</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="o">&lt;&lt;</span><span class="w"> </span><span class="nx">prefix</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">val</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nb">uint64</span><span class="p">(</span><span class="nx">first</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="w"> </span><span class="o">&amp;</span><span class="w"> </span><span class="mh">0x3f</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">if</span><span class="w"> </span><span class="nx">length</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">return</span><span class="w"> </span><span class="nx">val</span><span class="p">,</span><span class="w"> </span><span class="kc">nil</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">rest</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nb">make</span><span class="p">([]</span><span class="kt">byte</span><span class="p">,</span><span class="w"> </span><span class="nx">length</span><span class="o">-</span><span class="mi">1</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">_</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">io</span><span class="p">.</span><span class="nf">ReadFull</span><span class="p">(</span><span class="nx">r</span><span class="p">,</span><span class="w"> </span><span class="nx">rest</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">for</span><span class="w"> </span><span class="nx">_</span><span class="p">,</span><span class="w"> </span><span class="nx">b</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="k">range</span><span class="w"> </span><span class="nx">rest</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">val</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="p">(</span><span class="nx">val</span><span class="w"> </span><span class="o">&lt;&lt;</span><span class="w"> </span><span class="mi">8</span><span class="p">)</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nb">uint64</span><span class="p">(</span><span class="nx">b</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">return</span><span class="w"> </span><span class="nx">val</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>Once we can read and write varints, <code>ReadFrame</code> is just two varint reads followed by <code>io.ReadFull</code> for the payload (in <code>frame.go</code> I also cap <code>length</code> at 16 MB so a bogus 62-bit varint doesn&rsquo;t try to allocate terabytes of memory):</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="nf">ReadFrame</span><span class="p">(</span><span class="nx">r</span><span class="w"> </span><span class="nx">io</span><span class="p">.</span><span class="nx">Reader</span><span class="p">)</span><span class="w"> </span><span class="p">(</span><span class="nx">Frame</span><span class="p">,</span><span class="w"> </span><span class="kt">error</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">fType</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nf">ReadVarint</span><span class="p">(</span><span class="nx">r</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">if</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">return</span><span class="w"> </span><span class="nx">Frame</span><span class="p">{},</span><span class="w"> </span><span class="nx">err</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">length</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nf">ReadVarint</span><span class="p">(</span><span class="nx">r</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">if</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">return</span><span class="w"> </span><span class="nx">Frame</span><span class="p">{},</span><span class="w"> </span><span class="nx">err</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">payload</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nb">make</span><span class="p">([]</span><span class="kt">byte</span><span class="p">,</span><span class="w"> </span><span class="nx">length</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">_</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">io</span><span class="p">.</span><span class="nf">ReadFull</span><span class="p">(</span><span class="nx">r</span><span class="p">,</span><span class="w"> </span><span class="nx">payload</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">return</span><span class="w"> </span><span class="nx">Frame</span><span class="p">{</span><span class="nx">Type</span><span class="p">:</span><span class="w"> </span><span class="nx">fType</span><span class="p">,</span><span class="w"> </span><span class="nx">Payload</span><span class="p">:</span><span class="w"> </span><span class="nx">payload</span><span class="p">},</span><span class="w"> </span><span class="nx">err</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><h3 id="from-hpack-to-qpack">From HPACK to QPACK</h3>
<p>In <a href="https://kmcd.dev/posts/http2-from-scratch-part-3/">HTTP/2 From Scratch: Part 3</a> and <a href="https://kmcd.dev/posts/http2-from-scratch-part-4/">Part 4</a>, we wrote an <code>HPACKDecoder</code> and <code>HPACKEncoder</code> (<a href="https://www.rfc-editor.org/rfc/rfc7541.html" rel="external">RFC 7541</a>). Why couldn&rsquo;t the HTTP/3 authors just reuse HPACK as-is and let us all go home early?</p>
<p>The issue is HPACK&rsquo;s dynamic table. In HTTP/2, every <code>HEADERS</code> frame that inserts a new header into the dynamic table is sent over the same underlying TCP connection. Because TCP delivers all bytes strictly in order, the decoder is guaranteed to see table insertions in the exact order the encoder sent them, even across different streams.</p>
<p>With QUIC, packets on different streams can arrive out of order. If a packet on Stream 0 gets dropped on the network while a packet on Stream 4 arrives, QUIC can deliver Stream 4 right away. But if Stream 4&rsquo;s headers referenced a dynamic table entry that was supposed to be created by Stream 0&rsquo;s lost packet, the decoder wouldn&rsquo;t be able to read Stream 4 until Stream 0 was retransmitted. We&rsquo;d be right back to head-of-line blocking.</p>
<p>To get around this, HTTP/3 uses QPACK (<a href="https://www.rfc-editor.org/rfc/rfc9204.html" rel="external">RFC 9204</a>). In QPACK, any dynamic table updates happen on separate unidirectional streams instead of inside the request streams, and every HEADERS frame starts with a 2-byte prefix (Required Insert Count and Delta Base). The static table is also bigger than HPACK&rsquo;s (99 entries starting at index 0, compared to 61 entries starting at index 1 in HPACK) and adds full name/value pairs like <code>content-type: application/json</code>.</p>

    
    
        
        
        
        
        
        
        
        
        <div class="svg-diagram-inline center"
                 style="display: block; max-width: 100%; width: 800px; margin-left: auto; margin-right: auto;">
                <svg class="h3-diagram" viewBox="0 0 880 124" xmlns="http://www.w3.org/2000/svg" font-family="ui-monospace, SFMono-Regular, Menlo, monospace">
  <style>
    .h3-diagram {
      --text-main: #CDD6F4;
      --text-sub: #A6ADC8;
      --c-blue: #89B4FA;
      --bg-blue: #1E1E2E;
      --c-green: #A6E3A1;
      --bg-green: #1E1E2E;
      --c-purple: #CBA6F7;
      --bg-purple: #1E1E2E;
    }
    @media (prefers-color-scheme: light) {
      :root:not([data-theme="dark"]) .h3-diagram {
        --text-main: #141A2E;
        --text-sub: #434E70;
        --c-blue: #0369A1;
        --bg-blue: #EDF6FF;
        --c-green: #0F766E;
        --bg-green: #E6F7F4;
        --c-purple: #6D28D9;
        --bg-purple: #F3E8FF;
      }
    }
    [data-theme="light"] .h3-diagram {
      --text-main: #141A2E;
      --text-sub: #434E70;
      --c-blue: #0369A1;
      --bg-blue: #EDF6FF;
      --c-green: #0F766E;
      --bg-green: #E6F7F4;
      --c-purple: #6D28D9;
      --bg-purple: #F3E8FF;
    }
  </style>

  <!-- Prefix -->
  <rect x="20" y="20" width="210" height="84" rx="8" fill="var(--bg-purple)" stroke="var(--c-purple)" stroke-width="2" />
  <text x="125" y="48" fill="var(--c-purple)" font-size="16" font-weight="700" text-anchor="middle">0x00   0x00</text>
  <text x="125" y="72" fill="var(--text-main)" font-size="13" font-weight="600" text-anchor="middle">2-Byte Prefix</text>
  <text x="125" y="91" fill="var(--text-sub)" font-size="11" text-anchor="middle">Static Mode</text>

  <!-- Indexed -->
  <rect x="244" y="20" width="190" height="84" rx="8" fill="var(--bg-green)" stroke="var(--c-green)" stroke-width="2" />
  <text x="339" y="48" fill="var(--c-green)" font-size="16" font-weight="700" text-anchor="middle">0xd1</text>
  <text x="339" y="72" fill="var(--text-main)" font-size="13" font-weight="600" text-anchor="middle">Indexed (#17)</text>
  <text x="339" y="91" fill="var(--text-sub)" font-size="11" text-anchor="middle">:method: GET</text>

  <!-- Indexed 2 -->
  <rect x="448" y="20" width="170" height="84" rx="8" fill="var(--bg-green)" stroke="var(--c-green)" stroke-width="2" />
  <text x="533" y="48" fill="var(--c-green)" font-size="16" font-weight="700" text-anchor="middle">0xc1</text>
  <text x="533" y="72" fill="var(--text-main)" font-size="13" font-weight="600" text-anchor="middle">Indexed (#1)</text>
  <text x="533" y="91" fill="var(--text-sub)" font-size="11" text-anchor="middle">:path: /</text>

  <!-- Literal -->
  <rect x="632" y="20" width="228" height="84" rx="8" fill="var(--bg-blue)" stroke="var(--c-blue)" stroke-width="2" />
  <text x="746" y="48" fill="var(--c-blue)" font-size="16" font-weight="700" text-anchor="middle">0x50 0x08 "kmcd.dev"</text>
  <text x="746" y="72" fill="var(--text-main)" font-size="13" font-weight="600" text-anchor="middle">Name Ref (#0) + Literal</text>
  <text x="746" y="91" fill="var(--text-sub)" font-size="11" text-anchor="middle">:authority</text>
</svg>

            </div>
        
    


<p>We&rsquo;ll keep our implementation simple and encode headers using only the static table and string literals. With no dynamic table to synchronize, we can skip the encoder/decoder streams and set the header block prefix to two zero bytes.</p>
<p>We also get to reuse some code! QPACK keeps HPACK&rsquo;s integer encoding and Huffman table (<a href="https://www.rfc-editor.org/rfc/rfc9204.html#section-4.1" rel="external">RFC 9204 Section 4.1</a>), so we can bring over those helpers from our HTTP/2 package, including Huffman decoding from <code>golang.org/x/net/http2/hpack</code>. We just need to adjust the bitmasks that identify each kind of field line:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">const</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">maskQPACKIndexed</span><span class="w">     </span><span class="p">=</span><span class="w"> </span><span class="mh">0x80</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">flagQPACKIndexedStat</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="mh">0x40</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">maskQPACKLiteralNameRef</span><span class="w">     </span><span class="p">=</span><span class="w"> </span><span class="mh">0xc0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">patternQPACKLiteralNameRef</span><span class="w">  </span><span class="p">=</span><span class="w"> </span><span class="mh">0x40</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">flagQPACKLiteralNameRefStat</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="mh">0x10</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">maskQPACKLiteral</span><span class="w">    </span><span class="p">=</span><span class="w"> </span><span class="mh">0xe0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">patternQPACKLiteral</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="mh">0x20</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="kd">func</span><span class="w"> </span><span class="p">(</span><span class="nx">d</span><span class="w"> </span><span class="o">*</span><span class="nx">QPACKDecoder</span><span class="p">)</span><span class="w"> </span><span class="nf">Decode</span><span class="p">(</span><span class="nx">payload</span><span class="w"> </span><span class="p">[]</span><span class="kt">byte</span><span class="p">)</span><span class="w"> </span><span class="p">([]</span><span class="nx">HeaderField</span><span class="p">,</span><span class="w"> </span><span class="kt">error</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">r</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">bytes</span><span class="p">.</span><span class="nf">NewReader</span><span class="p">(</span><span class="nx">payload</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">_</span><span class="p">,</span><span class="w"> </span><span class="nx">_</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nf">decodeInt</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="nx">r</span><span class="p">,</span><span class="w"> </span><span class="mi">8</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">_</span><span class="p">,</span><span class="w"> </span><span class="nx">_</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nf">decodeInt</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="nx">r</span><span class="p">,</span><span class="w"> </span><span class="mi">7</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="kd">var</span><span class="w"> </span><span class="nx">headers</span><span class="w"> </span><span class="p">[]</span><span class="nx">HeaderField</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">for</span><span class="w"> </span><span class="nx">r</span><span class="p">.</span><span class="nf">Len</span><span class="p">()</span><span class="w"> </span><span class="p">&gt;</span><span class="w"> </span><span class="mi">0</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">b</span><span class="p">,</span><span class="w"> </span><span class="nx">_</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">r</span><span class="p">.</span><span class="nf">ReadByte</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">switch</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">case</span><span class="w"> </span><span class="nx">b</span><span class="o">&amp;</span><span class="p">(</span><span class="nx">maskQPACKIndexed</span><span class="p">|</span><span class="nx">flagQPACKIndexedStat</span><span class="p">)</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="nx">maskQPACKIndexed</span><span class="p">|</span><span class="nx">flagQPACKIndexedStat</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="nx">index</span><span class="p">,</span><span class="w"> </span><span class="nx">_</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nf">decodeInt</span><span class="p">(</span><span class="nx">b</span><span class="p">,</span><span class="w"> </span><span class="nx">r</span><span class="p">,</span><span class="w"> </span><span class="mi">6</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="nx">headers</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nb">append</span><span class="p">(</span><span class="nx">headers</span><span class="p">,</span><span class="w"> </span><span class="nx">QPACKStaticTable</span><span class="p">[</span><span class="nx">index</span><span class="p">])</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">case</span><span class="w"> </span><span class="nx">b</span><span class="o">&amp;</span><span class="p">(</span><span class="nx">maskQPACKLiteralNameRef</span><span class="p">|</span><span class="nx">flagQPACKLiteralNameRefStat</span><span class="p">)</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="nx">patternQPACKLiteralNameRef</span><span class="p">|</span><span class="nx">flagQPACKLiteralNameRefStat</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="nx">index</span><span class="p">,</span><span class="w"> </span><span class="nx">_</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nf">decodeInt</span><span class="p">(</span><span class="nx">b</span><span class="p">,</span><span class="w"> </span><span class="nx">r</span><span class="p">,</span><span class="w"> </span><span class="mi">4</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="nx">val</span><span class="p">,</span><span class="w"> </span><span class="nx">_</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nf">decodeString</span><span class="p">(</span><span class="nx">r</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="nx">headers</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nb">append</span><span class="p">(</span><span class="nx">headers</span><span class="p">,</span><span class="w"> </span><span class="nx">HeaderField</span><span class="p">{</span><span class="nx">Name</span><span class="p">:</span><span class="w"> </span><span class="nx">QPACKStaticTable</span><span class="p">[</span><span class="nx">index</span><span class="p">].</span><span class="nx">Name</span><span class="p">,</span><span class="w"> </span><span class="nx">Value</span><span class="p">:</span><span class="w"> </span><span class="nx">val</span><span class="p">})</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">case</span><span class="w"> </span><span class="nx">b</span><span class="o">&amp;</span><span class="nx">maskQPACKLiteral</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="nx">patternQPACKLiteral</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="nx">name</span><span class="p">,</span><span class="w"> </span><span class="nx">_</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nf">decodeNameString</span><span class="p">(</span><span class="nx">b</span><span class="p">,</span><span class="w"> </span><span class="nx">r</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="nx">val</span><span class="p">,</span><span class="w"> </span><span class="nx">_</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nf">decodeString</span><span class="p">(</span><span class="nx">r</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="nx">headers</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nb">append</span><span class="p">(</span><span class="nx">headers</span><span class="p">,</span><span class="w"> </span><span class="nx">HeaderField</span><span class="p">{</span><span class="nx">Name</span><span class="p">:</span><span class="w"> </span><span class="nx">name</span><span class="p">,</span><span class="w"> </span><span class="nx">Value</span><span class="p">:</span><span class="w"> </span><span class="nx">val</span><span class="p">})</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">return</span><span class="w"> </span><span class="nx">headers</span><span class="p">,</span><span class="w"> </span><span class="kc">nil</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><h3 id="modeling-requests-and-responses">Modeling Requests and Responses</h3>
<p>Now that we can encode QPACK headers and read/write HTTP/3 frames, sending an HTTP request or response over a QUIC stream looks a lot like HTTP/2. We still have the same lowercase header names and pseudo-headers (<code>:method</code>, <code>:scheme</code>, <code>:authority</code>, <code>:path</code> on requests, and <code>:status</code> on responses). In <code>WriteRequest</code>, we QPACK-encode the headers into a <code>HEADERS</code> frame (<code>0x01</code>) and then write the request body into a <code>DATA</code> frame (<code>0x00</code>):</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="nf">WriteRequest</span><span class="p">(</span><span class="nx">w</span><span class="w"> </span><span class="nx">io</span><span class="p">.</span><span class="nx">Writer</span><span class="p">,</span><span class="w"> </span><span class="nx">req</span><span class="w"> </span><span class="o">*</span><span class="nx">http</span><span class="p">.</span><span class="nx">Request</span><span class="p">)</span><span class="w"> </span><span class="kt">error</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">headers</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="p">[]</span><span class="nx">HeaderField</span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="p">{</span><span class="nx">Name</span><span class="p">:</span><span class="w"> </span><span class="s">&#34;:method&#34;</span><span class="p">,</span><span class="w"> </span><span class="nx">Value</span><span class="p">:</span><span class="w"> </span><span class="nx">req</span><span class="p">.</span><span class="nx">Method</span><span class="p">},</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="p">{</span><span class="nx">Name</span><span class="p">:</span><span class="w"> </span><span class="s">&#34;:scheme&#34;</span><span class="p">,</span><span class="w"> </span><span class="nx">Value</span><span class="p">:</span><span class="w"> </span><span class="nx">req</span><span class="p">.</span><span class="nx">URL</span><span class="p">.</span><span class="nx">Scheme</span><span class="p">},</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="p">{</span><span class="nx">Name</span><span class="p">:</span><span class="w"> </span><span class="s">&#34;:authority&#34;</span><span class="p">,</span><span class="w"> </span><span class="nx">Value</span><span class="p">:</span><span class="w"> </span><span class="nx">req</span><span class="p">.</span><span class="nx">URL</span><span class="p">.</span><span class="nx">Host</span><span class="p">},</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="p">{</span><span class="nx">Name</span><span class="p">:</span><span class="w"> </span><span class="s">&#34;:path&#34;</span><span class="p">,</span><span class="w"> </span><span class="nx">Value</span><span class="p">:</span><span class="w"> </span><span class="nx">req</span><span class="p">.</span><span class="nx">URL</span><span class="p">.</span><span class="nf">RequestURI</span><span class="p">()},</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">enc</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nf">NewQPACKEncoder</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nf">WriteFrame</span><span class="p">(</span><span class="nx">w</span><span class="p">,</span><span class="w"> </span><span class="nx">FrameHeaders</span><span class="p">,</span><span class="w"> </span><span class="nx">enc</span><span class="p">.</span><span class="nf">Encode</span><span class="p">(</span><span class="nx">headers</span><span class="p">))</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">if</span><span class="w"> </span><span class="nx">req</span><span class="p">.</span><span class="nx">Body</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">bodyBytes</span><span class="p">,</span><span class="w"> </span><span class="nx">_</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">io</span><span class="p">.</span><span class="nf">ReadAll</span><span class="p">(</span><span class="nx">req</span><span class="p">.</span><span class="nx">Body</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">if</span><span class="w"> </span><span class="nb">len</span><span class="p">(</span><span class="nx">bodyBytes</span><span class="p">)</span><span class="w"> </span><span class="p">&gt;</span><span class="w"> </span><span class="mi">0</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="nf">WriteFrame</span><span class="p">(</span><span class="nx">w</span><span class="p">,</span><span class="w"> </span><span class="nx">FrameData</span><span class="p">,</span><span class="w"> </span><span class="nx">bodyBytes</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">return</span><span class="w"> </span><span class="kc">nil</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>To read the message on the other side, we call <code>ReadFrame</code> in a loop until <code>io.EOF</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="k">for</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">frame</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nf">ReadFrame</span><span class="p">(</span><span class="nx">r</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">if</span><span class="w"> </span><span class="nx">errors</span><span class="p">.</span><span class="nf">Is</span><span class="p">(</span><span class="nx">err</span><span class="p">,</span><span class="w"> </span><span class="nx">io</span><span class="p">.</span><span class="nx">EOF</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">break</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">switch</span><span class="w"> </span><span class="nx">frame</span><span class="p">.</span><span class="nx">Type</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">case</span><span class="w"> </span><span class="nx">FrameHeaders</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">fields</span><span class="p">,</span><span class="w"> </span><span class="nx">_</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">dec</span><span class="p">.</span><span class="nf">Decode</span><span class="p">(</span><span class="nx">frame</span><span class="p">.</span><span class="nx">Payload</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nf">applyHeaders</span><span class="p">(</span><span class="nx">req</span><span class="p">,</span><span class="w"> </span><span class="nx">fields</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">case</span><span class="w"> </span><span class="nx">FrameData</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">bodyBuf</span><span class="p">.</span><span class="nf">Write</span><span class="p">(</span><span class="nx">frame</span><span class="p">.</span><span class="nx">Payload</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><h3 id="what-about-http-trailers">What About HTTP Trailers?</h3>
<p>In HTTP/3, how do you send HTTP trailers (like <code>grpc-status</code> at the end of a gRPC call)? Because there are no flags on HTTP/3 frames, trailers are simply sent as a <strong>second <code>HEADERS</code> frame after the <code>DATA</code> frames</strong>, right before closing the stream.</p>
<p>Are we going to implement HTTP trailers in our from-scratch server and client?</p>

    
    
        
        
            
        
        
        
        
        
        
            
        
        
        
            <img src="https://kmcd.dev/posts/http3-from-scratch-part-1/trailers_hu_41da2c4f990cf317.webp"
                 class="img-fade-in center"
                 onload="this.classList.add('is-loaded')"
                 alt="" width="420px" height="507"/>
        
    


<p><strong>Nah. I&rsquo;m not implementing trailers in this walkthrough.</strong></p>
<p>If you really want to see how trailers work in HTTP/3, you can reference my PRs to <code>quic-go</code> (<a href="https://github.com/quic-go/quic-go/pull/4581" rel="external">#4581</a>, <a href="https://github.com/quic-go/quic-go/pull/4630" rel="external">#4630</a>, <a href="https://github.com/quic-go/quic-go/pull/4639" rel="external">#4639</a>, <a href="https://github.com/quic-go/quic-go/pull/4656" rel="external">#4656</a>) where I added trailer support to the <code>quic-go</code> HTTP/3 implementation, or read my <a href="https://kmcd.dev/posts/grpc-over-http3/">gRPC Over HTTP/3</a> and <a href="https://kmcd.dev/posts/grpc-over-http3-followup/">gRPC Over HTTP/3: Followup</a> articles. If a second <code>HEADERS</code> frame shows up after <code>DATA</code>, we&rsquo;re just going to ignore it.</p>
<h3 id="running-the-demo">Running the Demo</h3>
<p>In <code>main.go</code>, I wired up the raw QUIC echo test followed by an HTTP/3 request and response over a QUIC stream:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">$ go run .
</span></span><span class="line"><span class="cl"><span class="o">===</span> Step 1: Raw QUIC Echo <span class="nv">Demo</span> <span class="o">===</span>
</span></span><span class="line"><span class="cl"><span class="o">[</span>Echo Client<span class="o">]</span> Sending on stream 0: <span class="s2">&#34;Hello over QUIC!&#34;</span>
</span></span><span class="line"><span class="cl"><span class="o">[</span>Echo Server<span class="o">]</span> Received on stream 0: <span class="s2">&#34;Hello over QUIC!&#34;</span>
</span></span><span class="line"><span class="cl"><span class="o">[</span>Echo Client<span class="o">]</span> Got reply: <span class="s2">&#34;echo: Hello over QUIC!&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="o">===</span> Step 2: HTTP/3 Framing <span class="p">&amp;</span> QPACK Over a QUIC <span class="nv">Stream</span> <span class="o">===</span>
</span></span><span class="line"><span class="cl"><span class="o">[</span>H3 Server<span class="o">]</span> Decoded POST /hello <span class="o">(</span>Host: kmcd.dev, User-Agent: kmcd-h3/0.1, Body: <span class="s2">&#34;ping from h3&#34;</span><span class="o">)</span>
</span></span><span class="line"><span class="cl"><span class="o">[</span>H3 Client<span class="o">]</span> Status: <span class="m">200</span> OK
</span></span><span class="line"><span class="cl"><span class="o">[</span>H3 Client<span class="o">]</span> Content-Type: text/plain<span class="p">;</span> <span class="nv">charset</span><span class="o">=</span>utf-8, Server: kmcd-h3-scratch
</span></span><span class="line"><span class="cl"><span class="o">[</span>H3 Client<span class="o">]</span> Body: Hello over HTTP/3! You sent <span class="m">12</span> bytes.
</span></span></code></pre></div><p>That&rsquo;s our first real <code>*http.Request</code> encoded into QPACK headers and HTTP/3 <code>HEADERS</code> + <code>DATA</code> frames, sent across a QUIC stream, and answered with a QPACK-encoded <code>*http.Response</code>.</p>
<h3 id="whats-next">What&rsquo;s Next?</h3>
<p>If you pointed <code>curl --http3</code> or Chrome at our listener right now, they would hang up on us immediately. Real HTTP/3 peers expect both sides to open a one-way <strong>Control Stream</strong> (<code>0x00</code>) as soon as the connection starts and swap <code>SETTINGS</code> frames (<code>0x04</code>). Plus, browsers need a way to find out that your server speaks HTTP/3 over UDP in the first place.</p>
<p>In <strong>Part 2</strong>, we&rsquo;ll look at how browsers discover HTTP/3 (<code>Alt-Svc</code> and DNS <code>HTTPS</code> records), how to convince Chrome and <code>curl</code> to talk HTTP/3 to <code>localhost</code>, and wrap our framing code into a proper <code>http.Handler</code> server and <code>Client</code>.</p>
<p>See all of the code mentioned in this article in <a href="https://github.com/sudorandom/kmcd.dev/tree/main/content/posts/2026/http3-from-scratch-part-1/go" rel="external">http3-from-scratch-part-1/go</a>.</p>
]]></content:encoded></item></channel></rss>