
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/">
    <channel>
        <title><![CDATA[ The Cloudflare Blog ]]></title>
        <description><![CDATA[ Get the latest news on how products at Cloudflare are built, technologies used, and join the teams helping to build a better Internet. ]]></description>
        <link>https://blog.cloudflare.com</link>
        <atom:link href="https://blog.cloudflare.com/" rel="self" type="application/rss+xml"/>
        <language>en-us</language>
        <image>
            <url>https://blog.cloudflare.com/favicon.png</url>
            <title>The Cloudflare Blog</title>
            <link>https://blog.cloudflare.com</link>
        </image>
        <lastBuildDate>Mon, 06 Apr 2026 16:48:57 GMT</lastBuildDate>
        <item>
            <title><![CDATA[Open sourcing h3i: a command line tool and library for low-level HTTP/3 testing and debugging]]></title>
            <link>https://blog.cloudflare.com/h3i/</link>
            <pubDate>Mon, 30 Dec 2024 14:00:00 GMT</pubDate>
            <description><![CDATA[ h3i is a command line tool and Rust library designed for low-level testing and debugging of HTTP/3, which runs over QUIC. ]]></description>
            <content:encoded><![CDATA[ <p>Have you ever built a piece of IKEA furniture, or put together a LEGO set, by following the instructions closely and only at the end realized at some point you didn't <i>quite</i> follow them correctly? The final result might be close to what was intended, but there's a nagging thought that maybe, just maybe, it's not as rock steady or functional as it could have been.</p><p>Internet protocol specifications are instructions designed for engineers to build things. Protocol designers take great care to ensure the documents they produce are clear. The standardization process gathers consensus and review from experts in the field, to further ensure document quality. Any reasonably skilled engineer should be able to take a specification and produce a performant, reliable, and secure implementation. The Internet is central to everyone's lives, and we depend on these implementations. Any deviations from the specification can put us at risk. For example, mishandling of malformed requests can allow attacks such as <a href="https://en.wikipedia.org/wiki/HTTP_request_smuggling"><u>request smuggling</u></a>.</p><p>h3i is a binary command line tool and Rust library designed for low-level testing and debugging of HTTP/3, which runs over QUIC. <a href="https://crates.io/crates/h3i"><u>h3i</u></a> is free and open source as part of Cloudflare's <a href="https://github.com/cloudflare/quiche"><u>quiche</u></a> project. In this post we'll explain the motivation behind developing h3i, how we use it to help develop robust and safe standards-compliant software and production systems, and how you can similarly use it to test your own software or services. If you just want to jump into how to use h3i, go to the <a href="#the-h3i-command-line-tool"><u>h3i command line tool</u></a> section.</p>
    <div>
      <h2>A recap of QUIC and HTTP/3</h2>
      <a href="#a-recap-of-quic-and-http-3">
        
      </a>
    </div>
    <p><a href="https://blog.cloudflare.com/http3-the-past-present-and-future/"><u>QUIC</u></a> is a secure-by-default transport protocol that provides performance advantages compared to TCP and TLS via a more efficient handshake, along with stream multiplexing that provides <a href="https://en.wikipedia.org/wiki/Head-of-line_blocking"><u>head-of-line blocking</u></a> avoidance. <a href="https://www.cloudflare.com/en-gb/learning/performance/what-is-http3/"><u>HTTP/3</u></a> is an application protocol that maps HTTP semantics to QUIC, such as defining how HTTP requests and responses are assigned to individual QUIC streams.</p><p>Cloudflare has supported QUIC on our global network in some shape or form <a href="https://blog.cloudflare.com/http3-the-past-present-and-future/"><u>since 2018</u></a>. We started while the <a href="https://ietf.org/"><u>Internet Engineering Task Force (IETF)</u></a> was earnestly standardizing the protocol, working through early iterations and using interoperability testing and experience to help provide feedback for the standards process. We <a href="https://blog.cloudflare.com/quic-version-1-is-live-on-cloudflare/"><u>launched support</u></a> for QUIC version 1 and HTTP/3 as soon as <a href="https://datatracker.ietf.org/doc/html/rfc9000"><u>RFC 9000</u></a> (and its accompanying specifications) were published in 2021.</p><p>We work on the Protocols team, who own the ingress proxy into the Cloudflare network. This is essentially Cloudflare’s “front door” — HTTP requests that come to Cloudflare from the Internet pass through us first. The majority of requests are passed onwards to things like rulesets, workers, caches, or a customer origin. However, you might be surprised that many requests don't ever make it that far because they are, in some way, invalid or malformed. Servers listening on the Internet have to be robust to traffic that is not RFC compliant, whether caused by accident or malicious intent.</p><p>The Protocols team actively participates in IETF standardization work and has also helped build and maintain other Cloudflare services that leverage quiche for QUIC and HTTP/3, from the proxies that help <a href="https://blog.cloudflare.com/icloud-private-relay/"><u>iCloud Private Relay</u></a> via <a href="https://blog.cloudflare.com/unlocking-quic-proxying-potential/"><u>MASQUE proxying</u></a>, to replacing <a href="https://blog.cloudflare.com/zero-trust-warp-with-a-masque/"><u>WARP's use of Wireguard with MASQUE</u></a>, and beyond.</p><p>Throughout all of these different use cases, it is important for us to extensively test all aspects of the protocols. A deep dive into protocol details is a blog post (or three) in its own right. So let's take a thin slice across HTTP to help illustrate the concepts.</p><p><a href="https://www.rfc-editor.org/rfc/rfc9110.html"><u>HTTP Semantics</u></a> are common to all versions of HTTP — the overall architecture, terminology, and protocol aspects such as request and response messages, methods, status codes, header and trailer fields, message content, and much more. Each individual HTTP version defines how semantics are transformed into a "wire format" for exchange over the Internet. You can read more about HTTP/1.1 and HTTP/2 in some of our previous <a href="https://blog.cloudflare.com/a-primer-on-proxies/"><u>blog</u></a> <a href="https://blog.cloudflare.com/technical-breakdown-http2-rapid-reset-ddos-attack/"><u>posts</u></a>.</p><p>With HTTP/3, HTTP request and response messages are split into a series of binary frames. <a href="https://datatracker.ietf.org/doc/html/rfc9114#section-7.2.2"><u>HEADERS</u></a> frames carry a representation of HTTP metadata (method, path, status code, field lines). The payload of the frame is the encoded <a href="https://datatracker.ietf.org/doc/html/rfc9204"><u>QPACK</u></a> compression output. <a href="https://datatracker.ietf.org/doc/html/rfc9114#section-7.2.1"><u>DATA</u></a> frames carry <a href="https://datatracker.ietf.org/doc/html/rfc9110#section-6.4.1"><u>HTTP content</u></a> (aka "message body"). In order to exchange these frames, HTTP/3 relies on QUIC <a href="https://datatracker.ietf.org/doc/html/rfc9000#section-2"><u>streams</u></a>. These provide an ordered and reliable byte stream and each have an identifier (ID) that is unique within the scope of a connection. There are <a href="https://datatracker.ietf.org/doc/html/rfc9000#section-2.1"><u>four different stream types</u></a>, denominated by the two least significant bits of the ID.</p><p>As a simple example, assuming a QUIC connection has already been established, a client can make a GET request and receive a 200 OK response with an HTML body using the follow sequence:</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/7vVfQ5CYaaVPVmGloRUnkI/88bd727c3526e540bd493bc15fbe904a/unnamed.png" />
          </figure><ol><li><p>Client allocates the first available client-initiated bidirectional QUIC stream. (The IDs start at 0, then 4, 8, 12 and so on)</p></li><li><p>Client sends the request HEADERS frame on the stream and sets the stream's <a href="https://datatracker.ietf.org/doc/html/rfc9000#section-19.8"><u>FIN bit</u></a> to mark the end of stream.</p></li><li><p>Server receives the request HEADERS frame and validates it against <a href="https://datatracker.ietf.org/doc/html/rfc9114#section-4.1.2"><u>RFC 9114 rules</u></a>. If accepted, it processes the request and prepares the response.</p></li><li><p>Server sends the response HEADERS frame on the same stream.</p></li><li><p>Server sends the response DATA frame on the same stream and sets the FIN bit.</p></li><li><p>Client receives the response frames and validates them. If accepted, the content is presented to the user.</p></li></ol><p>At the QUIC layer, stream data is split into STREAM frames, which are sent in QUIC packets over UDP. QUIC deals with any loss detection and recovery, helping to ensure stream data is reliable. The layer cake diagram below provides a handy comparison of how HTTP/1.1, HTTP/2 and HTTP/3 use TCP, UDP and IP.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/4049UpKGn4BJcYcEXSFgWz/32143a5ba3672786639908ad96851225/image2.png" />
          </figure>
    <div>
      <h2>Background on testing QUIC and HTTP/3 at Cloudflare</h2>
      <a href="#background-on-testing-quic-and-http-3-at-cloudflare">
        
      </a>
    </div>
    <p>The Protocols team has a diverse set of automated test tools that exercise our ingress proxy software in order to ensure it can stand up to the deluge that the Internet can throw at it. Just like a bouncer at a nightclub front door, we need to prevent as much bad traffic as possible before it gets inside and potentially causes damage.</p><p>HTTP/2 and HTTP/3 share several concepts. When we started developing early HTTP/3 support, we'd already learned a lot from production experience with HTTP/2. While HTTP/2 addressed many issues with HTTP/1.1 (especially problems like <a href="https://www.cgisecurity.com/lib/HTTP-Request-Smuggling.pdf"><u>request smuggling</u></a>, caused by its ASCII-based message delineation), HTTP/2 also added complexity and new avenues for attack. Security is an ongoing process, and the Protocols team continually hardens our software and systems to threats. For example, mitigating the range of <a href="https://blog.cloudflare.com/on-the-recent-http-2-dos-attacks/"><u>denial-of-service attacks</u></a> identified by Netflix in 2019, or the <a href="https://blog.cloudflare.com/technical-breakdown-http2-rapid-reset-ddos-attack/"><u>HTTP/2 Rapid Reset</u></a> attacks of 2023.</p><p>For testing HTTP/2, we rely on the Python <a href="https://pypi.org/project/requests/"><u>Requests</u></a> library for testing conventional HTTP exchanges. However, that mostly only exercises HEADERS and DATA frames. There are eight other frame types and a plethora of ways that they can interact (hence the new attack vectors mentioned above). In order to get full testing coverage, we have to break down into the lower layer <a href="https://pypi.org/project/h2/"><u>h2</u></a> library, which allows exact frame-by-frame control. However, even that is not always enough. Libraries tend to want to follow the RFC rules and prevent their users from doing "the wrong thing". This is entirely logical for most purposes. For our needs though, we need to take off the safety guards just like any potential attackers might do. We have a few cases where the best way to exercise certain traffic patterns is to handcraft HTTP/2 frames in a hex editor, store that as binary, and replay it with a tool such as <a href="https://docs.openssl.org/1.0.2/man1/s_client/"><u>OpenSSL s_client</u></a>.</p><p>We knew we'd need similar testing approaches for HTTP/3. However, when we started in 2018, there weren't many other suitable client implementations. The rate of iteration on the specifications also meant it was hard to always keep in sync. So we built tests on quiche, using a mix of our <a href="https://github.com/cloudflare/quiche/blob/master/apps/src/client.rs"><u>quiche-client</u></a> and <a href="https://github.com/cloudflare/quiche/tree/master/tools/http3_test"><u>http3_test</u></a>. Over time, the python library <a href="https://github.com/aiortc/aioquic"><u>aioquic</u></a> has matured, and we have used it to add a range of lower-layer tests that break or bend HTTP/3 rules, in order to prove our proxies are robust.</p><p>Finally, we would be remiss not to mention that all the tests in our ingress proxy are <b>in addition to </b>the suite of over 500 integration tests that run on the quiche project itself.</p>
    <div>
      <h2>Making HTTP/3 testing more accessible and maintainable with h3i</h2>
      <a href="#making-http-3-testing-more-accessible-and-maintainable-with-h3i">
        
      </a>
    </div>
    <p>While we are happy with the coverage of our current tests, the smorgasbord of test tools makes it hard to know what to reach for when adding new tests. For example, we've had cases where aioquic's safety guards prevent us from doing something, and it has needed a patch or workaround. This sort of thing requires a time investment just to debug/develop the tests.</p><p>We believe it shouldn't take a protocol or code expert to develop what are often very simple to describe tests. While it is important to provide guide rails for the majority of conventional use cases, it is also important to provide accessible methods for taking them off.</p><p>Let's consider a simple example. In HTTP/3 there is something called the control stream. It's used to exchange frames such as SETTINGS, which affect the HTTP/3 connection. RFC 9114 <a href="https://datatracker.ietf.org/doc/html/rfc9114#section-6.2.1"><u>Section 6.2.1</u></a> states:</p><blockquote><p><i>Each side MUST initiate a single control stream at the beginning of the connection and send its SETTINGS frame as the first frame on this stream. If the first frame of the control stream is any other frame type, this MUST be treated as a connection error of type H3_MISSING_SETTINGS. Only one control stream per peer is permitted; receipt of a second stream claiming to be a control stream MUST be treated as a connection error of type H3_STREAM_CREATION_ERROR. The sender MUST NOT close the control stream, and the receiver MUST NOT request that the sender close the control stream. If either control stream is closed at any point, this MUST be treated as a connection error of type H3_CLOSED_CRITICAL_STREAM. Connection errors are described in Section 8.</i></p></blockquote><p>There are many tests we can conjure up just from that paragraph:</p><ol><li><p>Send a non-SETTINGS frame as the first frame on the control stream.</p></li><li><p>Open two control streams.</p></li><li><p>Open a control stream and then close it with a FIN bit.</p></li><li><p>Open a control stream and then reset it with a RESET_STREAM QUIC frame.</p></li><li><p>Wait for the peer to open a control stream and then ask for it to be reset with a STOP_SENDING QUIC frame.</p></li></ol><p>All of the above actions should cause a remote peer that has implemented the RFC properly to close the connection. Therefore, it is not in the interest of the local client or server applications to ever do these actions.</p><p>Many QUIC and HTTP/3 implementations are developed as libraries that are integrated into client or server applications. There may be an extensive set of unit or integration tests of the library checking RFC rules. However, it is also important to run the same tests on the integrated assembly of library and application, since it's all too common that an unhandled/mishandled library error can cascade to cause issues in upper layers. For instance, the HTTP/2 Rapid Reset attacks affected Cloudflare due to their <a href="https://blog.cloudflare.com/technical-breakdown-http2-rapid-reset-ddos-attack/#impact-on-customers"><u>impact on how one service spoke to another</u></a>.</p><p>We've developed h3i, a command line tool and library, to make testing more accessible and maintainable for all. We started with a client that can exercise servers, since that's what our focus has been. Future developments could support the opposite, a server that behaves in unusual ways in order to exercise clients.</p><p><b>Note: </b>h3i is <i>not</i> intended to be a production client! Its flexibility may cause issues that are not observed in other production-oriented clients. It is also not intended to be used for any type of performance testing and measurement.</p>
    <div>
      <h2>The h3i command line tool</h2>
      <a href="#the-h3i-command-line-tool">
        
      </a>
    </div>
    <p>The primary purpose of the h3i command line tool is quick low-level debugging and exploratory testing. Rather than worrying about writing code or a test script, users can quickly run an ad-hoc client test against a target, guided by interactive prompts.</p><p>In the simplest case, you can think of h3i a bit like <a href="https://curl.se/"><u>curl</u></a> but with access to some extra HTTP/3 parameters. In the example below, we issue a request to <a href="https://cloudflare-quic.com"><u>https://cloudflare-quic.com</u></a>/ and receive a response.</p><div>
  
</div><p>Walking through a simple GET with h3i step-by-step:</p><ol><li><p>Grab a copy of the h3i binary either by running <code>cargo install h3i</code> or cloning the quiche source repo at <a href="https://github.com/cloudflare/quiche/"><u>https://github.com/cloudflare/quiche/</u></a>. Both methods assume you have some familiarity with Rust and Cargo. See the cargo <a href="https://doc.rust-lang.org/book/ch14-04-installing-binaries.html"><u>documentation</u></a> for more information.</p><ol><li><p><code>cargo install</code> will place the binary on your path, so you can then just run it by executing <code>h3i</code>.</p></li><li><p>If running from source, navigate to the quiche/h3i directory and then use <code>cargo run</code>.</p></li></ol></li><li><p>Run the binary and provide the name and port of the target server. If the port is omitted, the default value 443 is assumed. E.g, <code>cargo run cloudflare-quic.com</code></p></li><li><p>h3i then enters the action prompting phase. A series of one or more HTTP/3 actions can be queued up, such as sending frames, opening or terminating streams, or waiting on data from the server. The full set of options is documented in the <a href="https://github.com/cloudflare/quiche/blob/master/h3i/README.md#command-line-tool"><u>readme</u></a>.</p><ol><li><p>The prompting interface adapts to keyboard inputs and supports tab completion.</p></li><li><p>In the example above, the <code>headers</code> action is selected, which walks through populating the fields in a HEADERS frame. It includes <a href="https://datatracker.ietf.org/doc/html/rfc9114#section-4.3.1"><u>mandatory fields</u></a> from RFC 9114 for convenience. If a test requires omitting these, the <code>headers_no_pseudo</code> can be used instead.</p></li></ol></li><li><p>The <code>commit</code> prompt choice finalizes the action list and moves to the connection phase. h3i initiates a QUIC connection to the server identified in step 2. Once connected, actions are executed in order.</p></li><li><p>By default, h3i reports some limited information about the frames the server sent. To get more detailed information, the <code>RUST_LOG</code> environment can be set with either <code>debug</code> or <code>trace</code> levels.</p></li></ol>
    <div>
      <h2>Instant record and replay, powered by qlog</h2>
      <a href="#instant-record-and-replay-powered-by-qlog">
        
      </a>
    </div>
    <p>It can be fun to play around with the h3i command line tool to see how different servers respond to different combinations or sequences of actions. Occasionally, you'll find a certain set that you want to run over and over again, or share with a friend or colleague. Having to manually enter the prompts repeatedly, or share screenshots of the h3i input can turn tedious. Fortunately, h3i records all the actions in a log file by default — the file path is printed immediately after h3i starts. The format of this file is based on <a href="https://datatracker.ietf.org/doc/html/draft-ietf-quic-qlog-main-schema"><u>qlog</u></a>, an in-progress standard in development at the IETF for network protocol logging. It’s a perfect fit for our low-level needs.</p><p>Here's an example h3i qlog file:</p>
            <pre><code>{"qlog_version":"0.3","qlog_format":"JSON-SEQ","title":"h3i","description":"h3i","trace":{"vantage_point":{"type":"client"},"title":"h3i","description":"h3i","configuration":{"time_offset":0.0}}}
{
  "time": 0.172783,
  "name": "http:frame_created",
  "data": {
    "stream_id": 0,
    "frame": {
      "frame_type": "headers",
      "headers": [
        {
          "name": ":method",
          "value": "GET"
        },
        {
          "name": ":authority",
          "value": "cloudflare-quic.com"
        },
        {
          "name": ":path",
          "value": "/"
        },
        {
          "name": ":scheme",
          "value": "https"
        },
        {
          "name": "user-agent",
          "value": "h3i"
        }
      ]
    }
  },
  "fin_stream": true
}</code></pre>
            <p>h3i logs can be replayed using the <code>--qlog-input</code> option. You can change the target server host and port, and keep all the same actions. However, most servers will validate the :authority pseudo-header or Host header contained in a HEADERS frame. The --replay-host-override option allows changing these fields without needing to modify the file by hand.</p><p>And yes, qlog files are human-readable text in the JSON-SEQ format. So you can also just write these by hand in the first place if you like! However, if you're going to start writing things, maybe Rust is your preferred option…</p>
    <div>
      <h2>Using the h3i library to send a malformed request with Rust</h2>
      <a href="#using-the-h3i-library-to-send-a-malformed-request-with-rust">
        
      </a>
    </div>
    <p>In our previous example, we just sent a valid request so there wasn't anything interesting to observe. Where h3i really shines is in generating traffic that isn't RFC compliant, such as malformed HTTP messages, invalid frame sequences, or other actions on streams. This helps determine if a server is acting robustly and defensively.</p><p>Let's explore this more with an example of HTTP content-length mismatch. RFC 9114 <a href="https://datatracker.ietf.org/doc/html/rfc9114#section-4.1.2"><u>section 4.1.2</u></a> specifies:</p><blockquote><p><i>A request or response that is defined as having content when it contains a Content-Length header field (Section 8.6 of [HTTP]) is malformed if the value of the Content-Length header field does not equal the sum of the DATA frame lengths received. A response that is defined as never having content, even when a Content-Length is present, can have a non-zero Content-Length header field even though no content is included in DATA frames.</i></p><p><i>Intermediaries that process HTTP requests or responses (i.e., any intermediary not acting as a tunnel) MUST NOT forward a malformed request or response. Malformed requests or responses that are detected MUST be treated as a stream error of type H3_MESSAGE_ERROR.</i></p><p><i>For malformed requests, a server MAY send an HTTP response indicating the error prior to closing or resetting the stream.</i></p></blockquote><p>There are good reasons that the RFC is so strict about handling mismatched content lengths. They can be a vector for <a href="https://portswigger.net/research/http2"><u>desynchronization attacks</u></a> (similar to request smuggling), especially when a proxy is converting inbound HTTP/3 to outbound HTTP/1.1.</p><p>We've provided an <a href="https://github.com/cloudflare/quiche/blob/master/h3i/examples/content_length_mismatch.rs"><u>example</u></a> of how to use the h3i Rust library to write a tailor-made test client that sends a mismatched content length request. It sends a Content-Length header of 5, but its body payload is “test”, which is only 4 bytes. It then waits for the server to respond, after which it explicitly closes the connection by sending a QUIC CONNECTION_CLOSE frame.</p><p>When running low-level tests, it can be interesting to also take a packet capture (<a href="https://en.wikipedia.org/wiki/Pcap"><u>pcap</u></a>) and observe what is happening on the wire. Since QUIC is an encrypted transport, we'll need to use the SSLKEYLOG environment variable to capture the session keys so that tools like Wireshark can <a href="https://wiki.wireshark.org/TLS#using-the-pre-master-secret"><u>decrypt and dissect</u></a>.</p><p>To follow along at home, clone a copy of the quiche repository, start a packet capture on the appropriate network interface and then run:</p>
            <pre><code>cd quiche/h3i
SSLKEYLOGFILE="h3i-example.keys" cargo run --example content_length_mismatch</code></pre>
            <p>In our decrypted capture, we see the expected sequence of handshake, request, response, and then closure.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/6Qkdd3h0x826tH95S61u92/5de829e018b9d3ef409a2452362fa81e/image1.png" />
          </figure>
    <div>
      <h2>Surveying the example code</h2>
      <a href="#surveying-the-example-code">
        
      </a>
    </div>
    <p>The <a href="https://github.com/cloudflare/quiche/blob/master/h3i/examples/content_length_mismatch.rs"><u>example</u></a> is a simple binary app with a <code>main()</code> entry point. Let's survey the key elements.</p><p>First, we set up an h3i configuration to a target server:</p>
            <pre><code>let config = Config::new()
        .with_host_port("cloudflare-quic.com".to_string())
        .with_idle_timeout(2000)
        .build()
        .unwrap();</code></pre>
            <p>The idle timeout is a QUIC concept which tells each endpoint when it should close the connection if the connection has been idle. This prevents endpoints from spinning idly if the peer hasn’t closed the connection. h3i’s default is 30 seconds, which can be too long for tests, so we set ours to 2 seconds here.</p><p>Next, we define a set of request headers and encode them with QPACK compression, ready to put in a HEADERS frame. Note that h3i does provide a <a href="https://docs.rs/h3i/latest/h3i/actions/h3/fn.send_headers_frame.html"><u>send_headers_frame</u></a> helper method which does this for you, but the example does it manually for clarity:</p>
            <pre><code>let headers = vec![
        Header::new(b":method", b"POST"),
        Header::new(b":scheme", b"https"),
        Header::new(b":authority", b"cloudflare-quic.com"),
        Header::new(b":path", b"/"),
        // We say that we're going to send a body with 5 bytes...
        Header::new(b"content-length", b"5"),
    ];

    let header_block = encode_header_block(&amp;headers).unwrap();</code></pre>
            <p>Then, we define the set of h3i actions that we want to execute in order: send HEADERS, send a too-short DATA frame, wait for the server's HEADERS, then close the connection.</p>
            <pre><code>let actions = vec![
        Action::SendHeadersFrame {
            stream_id: STREAM_ID,
            fin_stream: false,
            headers,
            frame: Frame::Headers { header_block },
        },
        Action::SendFrame {
            stream_id: STREAM_ID,
            fin_stream: true,
            frame: Frame::Data {
                // ...but, in actuality, we only send 4 bytes. This should yield a
                // 400 Bad Request response from an RFC-compliant
                // server: https://datatracker.ietf.org/doc/html/rfc9114#section-4.1.2-3
                payload: b"test".to_vec(),
            },
        },
        Action::Wait {
            wait_type: WaitType::StreamEvent(StreamEvent {
                stream_id: STREAM_ID,
                event_type: StreamEventType::Headers,
            }),
        },
        Action::ConnectionClose {
            error: quiche::ConnectionError {
                is_app: true,
                error_code: quiche::h3::WireErrorCode::NoError as u64,
                reason: vec![],
            },
        },
    ];</code></pre>
            <p>Finally, we'll set things in motion with <code>connect()</code>, which sets up the QUIC connection, executes the actions list and collects the summary.</p>
            <pre><code>let summary =
        sync_client::connect(config, &amp;actions).expect("connection failed");

    println!(
        "=== received connection summary! ===\n\n{}",
        serde_json::to_string_pretty(&amp;summary).unwrap_or_else(|e| e.to_string())
    );</code></pre>
            <p><a href="https://docs.rs/h3i/latest/h3i/client/connection_summary/struct.ConnectionSummary.html"><u>ConnectionSummary</u></a>  provides data about the connection, including the frames h3i received, details about why the connection closed, and connection statistics. The example prints the summary out. However, you can programmatically check it. We do this to write our own internal automation tests.</p><p>If you're running the example, it should print something like the following:</p>
            <pre><code>=== received connection summary! ===

{
  "stream_map": {
    "0": [
      {
        "UNKNOWN": {
          "raw_type": 2471591231244749708,
          "payload": ""
        }
      },
      {
        "UNKNOWN": {
          "raw_type": 2031803309763646295,
          "payload": "4752454153452069732074686520776f7264"
        }
      },
      {
        "enriched_headers": {
          "header_block_len": 75,
          "headers": [
            {
              "name": ":status",
              "value": "400"
            },
            {
              "name": "server",
              "value": "cloudflare"
            },
            {
              "name": "date",
              "value": "Sat, 07 Dec 2024 00:34:12 GMT"
            },
            {
              "name": "content-type",
              "value": "text/html"
            },
            {
              "name": "content-length",
              "value": "155"
            },
            {
              "name": "cf-ray",
              "value": "8ee06dbe2923fa17-ORD"
            }
          ]
        }
      },
      {
        "DATA": {
          "payload_len": 104
        }
      },
      {
        "DATA": {
          "payload_len": 51
        }
      }
    ]
  },
  "stats": {
    "recv": 10,
    "sent": 5,
    "lost": 0,
    "retrans": 0,
    "sent_bytes": 1712,
    "recv_bytes": 4178,
    "lost_bytes": 0,
    "stream_retrans_bytes": 0,
    "paths_count": 1,
    "reset_stream_count_local": 0,
    "stopped_stream_count_local": 0,
    "reset_stream_count_remote": 0,
    "stopped_stream_count_remote": 0,
    "path_challenge_rx_count": 0
  },
  "path_stats": [
    {
      "local_addr": "0.0.0.0:64418",
      "peer_addr": "104.18.29.7:443",
      "active": true,
      "recv": 10,
      "sent": 5,
      "lost": 0,
      "retrans": 0,
      "rtt": 0.008140072,
      "min_rtt": 0.004645536,
      "rttvar": 0.004238173,
      "cwnd": 13500,
      "sent_bytes": 1712,
      "recv_bytes": 4178,
      "lost_bytes": 0,
      "stream_retrans_bytes": 0,
      "pmtu": 1350,
      "delivery_rate": 247720
    }
  ],
  "error": {
    "local_error": {
      "is_app": true,
      "error_code": 256,
      "reason": ""
    },
    "timed_out": false
  }
}
</code></pre>
            <p>Let’s walk through the output. Up first is the <a href="https://docs.rs/h3i/latest/h3i/client/connection_summary/struct.StreamMap.html"><u>StreamMap</u></a>, which is a record of all frames received on each stream. We can see that we received 5 frames on stream 0: 2 UNKNOWNs, one <a href="https://docs.rs/h3i/latest/h3i/frame/struct.EnrichedHeaders.html"><u>EnrichedHeaders</u></a> frame, and two DATA frames.</p><p>The UNKNOWN frames are extension frames that are unknown to h3i; the server under test is sending what are known as <a href="https://datatracker.ietf.org/doc/draft-edm-protocol-greasing/"><u>GREASE</u></a> frames to help exercise the protocol and ensure clients are not erroring when they receive something unexpected per <a href="https://datatracker.ietf.org/doc/html/rfc9114#extensions"><u>RFC 9114 requirements</u></a>.</p><p>The EnrichedHeaders frame is essentially an HTTP/3 HEADERS frame, but with some small helpers, like one to get the response status code. The server under test sent a 400 as expected.</p><p>The DATA frames carry response body bytes. In this case, the body is the HTML required to render the Cloudflare Bad Request page (you can peek at the HTML yourself in Wireshark). We chose to omit the raw bytes from the ConnectionSummary since they may not be representable safely as text. A future improvement could be to encode the bytes in base64 or hex, in order to support tests that need to check response content.</p>
    <div>
      <h2>h3i for test automation</h2>
      <a href="#h3i-for-test-automation">
        
      </a>
    </div>
    <p>We believe h3i is a great library for building automated tests on. You can take the above example and modify it to fit within various types of (continuous) integration tests.</p><p>We outlined earlier how the Protocols team HTTP/3 testing has organically grown to use three different frameworks. Even within those, we still didn't have much flexibility and ease of use. Over the last year we've been building h3i itself and reimplementing our suite of ingress proxy test cases using the Rust library. This has helped us improve test coverage with a range of new tests not previously possible. It also surprisingly identified some problems with the old tests, particularly for some edge cases where it wasn't clear how the old test code implementation was running under the hood.</p>
    <div>
      <h2>Bake offs, interop, and wider testing of HTTP</h2>
      <a href="#bake-offs-interop-and-wider-testing-of-http">
        
      </a>
    </div>
    <p><a href="https://datatracker.ietf.org/doc/html/rfc1025"><u>RFC 1025</u></a> was published in 1987. Authored by <a href="https://icannwiki.org/Jon_Postel"><u>Jon Postel</u></a>, it discusses bake offs:</p><blockquote><p><i>In the early days of the development of TCP and IP, when there were very few implementations and the specifications were still evolving, the only way to determine if an implementation was "correct" was to test it against other implementations and argue that the results showed your own implementation to have done the right thing.  These tests and discussions could, in those early days, as likely change the specification as change the implementation.</i></p><p><i>There were a few times when this testing was focused, bringing together all known implementations and running through a set of tests in hopes of demonstrating the N squared connectivity and correct implementation of the various tricky cases.  These events were called "Bake Offs".</i></p></blockquote><p>While nearly 4 decades old, the concept of exercising Internet protocol implementations and seeing how they compare to the specification still holds true. The QUIC WG made heavy use of interoperability testing through its standardization process. We started off sitting in a room and running tests manually by hand (or with some help from scripts). Then <a href="https://seemann.io/"><u>Marten Seemann</u></a> developed the <a href="https://interop.seemann.io/"><u>QUIC Interop Runner</u></a>, which runs regular automated testing and collects and renders all the results. This has proven to be incredibly useful.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/2OGnVUbatoX8Ya2IO5RdCl/754316e004a8e658ac089e10e70b72ca/image6.png" />
          </figure><p>The state of HTTP/3 interoperability testing is not quite as mature. Although there are tools such as <a href="https://kazu-yamamoto.hatenablog.jp/"><u>Kazu Yamamoto's</u></a> excellent <a href="https://github.com/kazu-yamamoto/h3spec"><u>h3spec</u></a> (in Haskell) for testing conformance, there isn't a similar continuous integration process of collection and rendering of results. While h3i shares similarities with h3spec, we felt it important to focus on the framework capabilities rather than creating a corpus of tests and assertions. Cloudflare is a big fan of Rust and as several teams move to Rust-based proxies, having a consistent ecosystem provides advantages (such as developer velocity).</p><p>We certainly feel there is a great opportunity for continued collaboration and cross-pollination between projects in the QUIC and HTTP space. For example, h3i might provide a suitable basis to build another tool (or set of scripts) to run bake offs or interop tests. Perhaps it even makes sense to have a common collection of test cases owned by the community, that can be specialized to the most appropriate or preferred tooling. This topic was recently presented at the <a href="https://github.com/HTTPWorkshop/workshop2024/blob/main/talks/5.%20Testing/testing.pdf"><u>HTTP Workshop 2024</u></a> by Mohammed Al-Sahaf, and it excites us to see <a href="https://www.caffeinatedwonders.com/2024/12/18/towards-validated-http-implementation/"><u>new potential directions</u></a> of testing improvements.</p><p>When using any tools or methods for protocol testing, we encourage responsible handling of security-related matters. If you believe you may have identified a vulnerability in an IETF Internet protocol itself, please follow the IETF's <a href="https://www.ietf.org/standards/rfcs/vulnerabilities/"><u>reporting guidance</u></a>. If you believe you may have discovered an implementation vulnerability in a product, open source project, or service using QUIC or HTTP, then you should report these directly to the responsible party. Implementers or operators often provide their own publicly-available guidance and contact details to send reports. For example, the Cloudflare quiche <a href="https://github.com/cloudflare/quiche/security/policy"><u>security policy</u></a> is available in the Security tab of the GitHub repository.</p>
    <div>
      <h2>Summary and outlook</h2>
      <a href="#summary-and-outlook">
        
      </a>
    </div>
    <p>Cloudflare takes testing very seriously. While h3i has a limited feature set as a test HTTP/3 client, we believe it provides a strong framework that can be extended to a wider range of different cases and different protocols. For example, we'd like to add support for low-level HTTP/2.</p><p>We've designed h3i to integrate into a wide range of testing methodologies, from manual ad-hoc testing, to native Rust tests, to conformance testbenches built with scripting languages. We've had great success migrating our existing zoo of test tools to a single one that is more accessible and easier to maintain.</p><p>Now that you've read about h3i's capabilities, it's left as an exercise to the reader to go back to the example of HTTP/3 control streams and consider how you could write tests to exercise a server.</p><p>We encourage the community to experiment with h3i and provide feedback, and propose ideas or contributions to the <a href="https://github.com/cloudflare/quiche"><u>GitHub repository</u></a> as issues or Pull Requests.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/5rp4YDTbXm37OxK7dtjiKF/816c0eed08926b7d34842f4769808277/image4.png" />
          </figure><p></p> ]]></content:encoded>
            <category><![CDATA[QUIC]]></category>
            <category><![CDATA[HTTP3]]></category>
            <category><![CDATA[Testing]]></category>
            <category><![CDATA[Protocols]]></category>
            <category><![CDATA[Rust]]></category>
            <guid isPermaLink="false">2yX9ADcaKBprzyI9BaBoqN</guid>
            <dc:creator>Lucas Pardue</dc:creator>
            <dc:creator>Evan Rittenhouse</dc:creator>
        </item>
        <item>
            <title><![CDATA[Improved Cloudflare Workers testing via Vitest and workerd]]></title>
            <link>https://blog.cloudflare.com/workers-vitest-integration/</link>
            <pubDate>Fri, 15 Mar 2024 14:00:35 GMT</pubDate>
            <description><![CDATA[ Today, we’re excited to announce a new Workers Vitest integration - allowing you to write unit and integration tests via the popular testing framework, Vitest, that execute directly in our runtime, workerd ]]></description>
            <content:encoded><![CDATA[ <p></p><p></p><p>Today, we’re excited to announce a new Workers Vitest integration - allowing you to write unit and integration tests via the popular testing framework, <a href="https://vitest.dev/">Vitest</a>, that execute directly in our runtime, <a href="https://github.com/cloudflare/workerd">workerd</a>!</p><p>This integration provides you with the ability to test <b><i>anything</i></b> related to your Worker!</p><p>For the first time, you can write unit tests that run within the same <a href="https://github.com/cloudflare/workerd">runtime</a> that Cloudflare Workers run on in production, providing greater confidence that the behavior of your Worker in tests will be the same as when deployed to production. For integration tests, you can now write tests for Workers that are triggered by <a href="https://developers.cloudflare.com/workers/configuration/cron-triggers/">Cron Triggers</a> in addition to traditional <code>fetch()</code> events. You can also more easily test complex applications that interact with <a href="https://developers.cloudflare.com/kv/">KV</a>, <a href="https://www.cloudflare.com/developer-platform/products/r2/">R2</a>, <a href="https://developers.cloudflare.com/d1/">D1</a>, <a href="https://developers.cloudflare.com/queues/">Queues</a>, <a href="https://developers.cloudflare.com/workers/configuration/bindings/about-service-bindings/">Service Bindings</a>, and more Cloudflare products.</p><p>For all of your tests, you have access to <a href="https://vitest.dev/guide/features.html">Vitest features</a> like snapshots, mocks, timers, and spies.</p><p>In addition to increased testing and functionality, you’ll also notice other developer experience improvements like hot-module-reloading, watch mode on by default, and per-test isolated storage. Meaning that, as you develop and edit your tests, they’ll automatically re-run, without you having to restart your test runner.</p>
    <div>
      <h2>Get started testing Workers with Vitest</h2>
      <a href="#get-started-testing-workers-with-vitest">
        
      </a>
    </div>
    <p>The easiest way to get started with testing your Workers via Vitest is to start a new Workers project via our create-cloudflare tool:</p>
            <pre><code>npm create cloudflare@latest hello-world -- --type=hello-world</code></pre>
            <p>Running this command will scaffold a new project for you with the Workers Vitest integration already set up. An example unit test and integration test are also included.</p>
    <div>
      <h3>Manual install and setup instructions</h3>
      <a href="#manual-install-and-setup-instructions">
        
      </a>
    </div>
    <p>If you prefer to manually install and set up the Workers Vitest integration, begin by installing <code>@cloudflare/vitest-pool-workers</code> from npm:</p>
            <pre><code>$ npm install --save-dev @cloudflare/vitest-pool-workers</code></pre>
            <p><code>@cloudflare/vitest-pool-workers</code> has a peer dependency on a specific version of <code>vitest</code>. Modern versions of <code>npm</code> will install this automatically, but we recommend you install it explicitly too. Refer to the <a href="https://developers.cloudflare.com/workers/testing/vitest-integration/get-started/write-your-first-test/">getting started guide</a> for the current supported version. If you’re using TypeScript, add <code>@cloudflare/vitest-pool-workers</code> to your <code>tsconfig.json</code>’s <code>types</code> to get types for the <code>cloudflare:test</code> module:</p>
            <pre><code>{
  "compilerOptions": {
    "module": "esnext",
    "moduleResolution": "bundler",
    "lib": ["esnext"],
    "types": [
      "@cloudflare/workers-types/experimental",
      "@cloudflare/vitest-pool-workers"
    ]
  }
}</code></pre>
            <p>Then, enable the pool in your Vitest configuration file:</p>
            <pre><code>// vitest.config.js
import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config";

export default defineWorkersConfig({
  test: {
    poolOptions: {
      workers: {
        wrangler: { configPath: "./wrangler.toml" },
      },
    },
  },
});</code></pre>
            <p>After that, define a compatibility date after “2022-10-31” and enable the <a href="https://developers.cloudflare.com/workers/configuration/compatibility-dates/#nodejs-compatibility-flag"><code>nodejs_compat</code> compatibility flag</a> in your <code>wrangler.toml</code>:</p>
            <pre><code># wrangler.toml
main = "src/index.ts"
compatibility_date = "2024-01-01"
compatibility_flags = ["nodejs_compat"]</code></pre>
            
    <div>
      <h2>Test anything exported from a Worker</h2>
      <a href="#test-anything-exported-from-a-worker">
        
      </a>
    </div>
    <p>With the new Workers Vitest Integration, you can test anything exported from your Worker in both unit and integration-style tests. Within these tests, you can also test connected resources like R2, KV, and Durable Objects, as well as applications involving multiple Workers.</p>
    <div>
      <h3>Writing unit tests</h3>
      <a href="#writing-unit-tests">
        
      </a>
    </div>
    <p>In a Workers context, a unit test imports and directly calls functions from your Worker then asserts on their return values. Let’s say you have a Worker that looks like this:</p>
            <pre><code>export function add(a, b) {
  return a + b;
}

export default {
  async fetch(request) {
    const url = new URL(request.url);
    const a = parseInt(url.searchParams.get("a"));
    const b = parseInt(url.searchParams.get("b"));
    return new Response(add(a, b));
  }
}</code></pre>
            <p>After you’ve setup and installed the Workers Vitest integration, you can unit test this Worker by creating a new test file called <code>index.spec.js</code> with the following code:</p>
            <pre><code>import { env, createExecutionContext, waitOnExecutionContext, } from "cloudflare:test";
import { describe, it, expect } from "vitest";
import { add }, worker from "./src";

describe("Hello World worker", () =&gt; {
  it(“adds two numbers”, async () =&gt; {
    expect(add(2,3).toBe(5);
  });
  it("sends request (unit style)", async () =&gt; {
    const request = new Request("http://example.com/?a=3&amp;b=4");
    const ctx = createExecutionContext();
    const response = await worker.fetch(request, env, ctx);
    await waitOnExecutionContext(ctx);
    expect(await response.text()).toMatchInlineSnapshot(`"7"`);
  });
});</code></pre>
            <p>Using the Workers Vitest integration, you can write unit tests like these for any of your Workers.</p>
    <div>
      <h3>Writing integration tests</h3>
      <a href="#writing-integration-tests">
        
      </a>
    </div>
    <p>While unit tests are great for testing individual parts of your application, integration tests assess multiple units of functionality, ensuring that workflows and features work as expected. These are usually more complex than unit tests, but provide greater confidence that your app works as expected. In the Workers context, an integration test sends HTTP requests to your Worker and asserts on the HTTP responses.</p><p>With the Workers Vitest Integration, you can run integration tests by importing <code>SELF</code> from the new <code>cloudflare:test</code> utility like this:</p>
            <pre><code>// test/index.spec.ts
import { SELF } from "cloudflare:test";
import { it, expect } from "vitest";
import "../src";

// an integration test using SELF
it("sends request (integration style)", async () =&gt; {
   const response = await SELF.fetch("http://example.com/?a=3&amp;b=4");
   expect(await response.text()).toMatchInlineSnapshot(`"7"`);
});</code></pre>
            <p>When using <code>SELF</code> for integration tests, your Worker code runs in the same context as the test runner. This means you can use mocks to control your Worker.</p>
    <div>
      <h3>Testing different scenarios</h3>
      <a href="#testing-different-scenarios">
        
      </a>
    </div>
    <p>Whether you’re writing unit or integration tests, if your application uses Cloudflare Developer Platform products (e.g. KV, R2, <a href="https://www.cloudflare.com/developer-platform/products/d1/">D1</a>, Queues, or Durable Objects), you can test them. To demonstrate this, we have created a set of <a href="https://github.com/cloudflare/workers-sdk/tree/main/fixtures/vitest-pool-workers-examples">examples</a> to help get you started testing.</p>
    <div>
      <h2>Better testing experience === better testing</h2>
      <a href="#better-testing-experience-better-testing">
        
      </a>
    </div>
    <p>Having better testing tools makes it easier to test your projects right from the start, which leads to better overall quality and experience for your end users. The Workers Vitest integration provides that better experience, not just in terms of developer experience, but in making it easier to test your entire application.</p><p>The rest of this post will focus on <i>how</i> we built this new testing integration, diving into the internals of how Vitest works, the problems we encountered trying to get a framework to work within our runtime, and ultimately how we solved it and the improved DX that it unlocked.</p>
    <div>
      <h2>How Vitest traditionally works</h2>
      <a href="#how-vitest-traditionally-works">
        
      </a>
    </div>
    <p>When you start Vitest’s CLI, it first collects and sequences all your test files. By default, Vitest uses a “threads” pool, which spawns <a href="https://nodejs.org/api/worker_threads.html">Node.js worker threads</a> for isolating and running tests in parallel. Each thread gets a test file to run, dynamically requesting and evaluating code as needed. When the test runner imports a module, it sends a request to the host’s “Vite Node Server” which will either return raw JavaScript code transformed by Vite, or an external module path. If raw code is returned, it will be executed using the <a href="https://nodejs.org/api/vm.html#vmruninthiscontextcode-options"><code>node:vm</code> <code>runInThisContext()</code> function</a>. If a module path is returned, it will be imported using <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import">dynamic <code>import()</code></a>. Transforming user code with Vite allows hot-module-reloading (HMR) — when a module changes, it’s invalidated in the module cache and a new version will be returned when it’s next imported.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/bmF35IKzPZ9mTXK7lnTA8/c6924d8fd533ea835035fb67dbf0e0c4/Untitled-1.png" />
            
            </figure><p>Miniflare is a fully-local simulator for Cloudflare's Developer Platform. <a href="/miniflare/">Miniflare v2</a> provided a <a href="https://miniflare.dev/testing/vitest">custom environment</a> for Vitest that allowed you to run your tests <i>inside</i> the Workers sandbox. This meant you could import and call any function using Workers runtime APIs in your tests. You weren’t restricted to integration tests that just sent and received HTTP requests. In addition, this environment provided per-test isolated storage, automatically undoing any changes made at the end of each test. In Miniflare v2, this environment was relatively simple to implement. We’d already reimplemented Workers Runtime APIs in a Node.js environment, and could inject them using Vitest’s APIs into the global scope of the test runner.</p><p>By contrast, Miniflare v3 runs your Worker code <a href="/miniflare-and-workerd">inside the same <code>workerd</code> runtime</a> that Cloudflare uses in production. Running tests directly in <a href="https://github.com/cloudflare/workerd"><code>workerd</code></a> presented a challenge — <code>workerd</code> runs in its own process, separate from the Node.js worker thread, and it’s not possible to reference JavaScript classes across a process boundary.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/3Ngw1zLJWN4jfTko19ZhOn/e052b51d4bf2f00d89826cd02d2a2ad5/Untitled--1--1.png" />
            
            </figure>
    <div>
      <h2>Solving the problem with custom pools</h2>
      <a href="#solving-the-problem-with-custom-pools">
        
      </a>
    </div>
    <p>Instead, we use <a href="https://vitest.dev/advanced/pool.html">Vitest’s custom pools</a> feature to run the test runner in Cloudflare Workers running locally with <a href="/workerd-open-source-workers-runtime"><code>workerd</code></a>. A pool receives test files to run and decides how to execute them. By executing the runner inside <code>workerd</code>, tests have direct access to Workers runtime APIs as they’re running in a Worker. WebSockets are used to send and receive serialisable RPC messages between the Node.js host and <code>workerd</code> process. Note we’re running the exact same test runner code originally designed for a Node-context inside a Worker here. This means our Worker needs to provide Node’s built-in modules, support for dynamic code evaluation, and loading of arbitrary modules from disk with <a href="https://nodejs.org/api/esm.html#resolution-algorithm-specification">Node-resolution behavior</a>. The <a href="/workers-node-js-asynclocalstorage/"><code>nodejs_compat</code> compatibility flag</a> provides support for some of Node’s built-in modules, but does not solve our other problems. For that, we had to get creative…</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/2xktF5P98mL5puD3UaQnZx/0a66f121c5cc631009df97c3b0f33dd7/Untitled--2--1.png" />
            
            </figure>
    <div>
      <h2>Dynamic code evaluation</h2>
      <a href="#dynamic-code-evaluation">
        
      </a>
    </div>
    <p>For <a href="https://developers.cloudflare.com/workers/runtime-apis/web-standards/#javascript-standards">security reasons</a>, the Cloudflare Workers runtime does not allow dynamic code evaluation via <code>eval()</code> or <code>new Function()</code>. It also requires all modules to be defined ahead-of-time before execution starts. The test runner doesn't know what code to run until we start executing tests, so without lifting these restrictions, we have no way of executing the raw JavaScript code transformed by Vite nor importing arbitrary modules from disk. Fortunately, code that is only meant to run locally – like tests – has a much more relaxed security model than deployed code. To support local testing and other development-specific use-cases such as <a href="https://vitejs.dev/guide/api-vite-runtime">Vite’s new Runtime API</a>, we added <a href="https://github.com/cloudflare/workerd/pull/1338">“unsafe-eval bindings”</a> and <a href="https://github.com/cloudflare/workerd/pull/1423">“module-fallback services”</a> to <code>workerd</code>.</p><p>Unsafe-eval bindings provide local-only access to the <code>eval()</code> function, and <code>new Function()</code>/<code>new AsyncFunction()</code>/<code>new WebAssembly.Module()</code> constructors. By exposing these through a binding, we retain control over which code has access to these features.</p>
            <pre><code>// Type signature for unsafe-eval bindings
interface UnsafeEval {
  eval(script: string, name?: string): unknown;
  newFunction(script: string, name?: string, ...args: string[]): Function;
  newAsyncFunction(script: string, name?: string, ...args: string[]): AsyncFunction;
  newWasmModule(src: BufferSource): WebAssembly.Module;
}</code></pre>
            <p>Using the unsafe-eval binding <code>eval()</code> method, we were able to implement a <a href="https://github.com/cloudflare/workers-sdk/blob/main/packages/vitest-pool-workers/src/worker/lib/node/vm.ts">polyfill for the required <code>vm.runInThisContext()</code></a> function. While we could also implement loading of arbitrary modules from disk using unsafe-eval bindings, this would require us to rebuild <code>workerd</code>’s module resolution system in JavaScript. Instead, we allow workers to be configured with module fallback services. If enabled, imports that cannot be resolved by <code>workerd</code> become HTTP requests to the fallback service. These include the specifier, referrer, and whether it was an <code>import</code> or <code>require</code>. The service may respond with a module definition, or a redirect to another location if the resolved location doesn’t match the specifier. Requests originating from synchronous <code>require</code>s will block the main thread until the module is resolved. The Workers Vitest pool’s <a href="https://github.com/cloudflare/workers-sdk/blob/main/packages/vitest-pool-workers/src/pool/module-fallback.ts">fallback service</a> implements <a href="https://nodejs.org/api/esm.html#resolution-algorithm">Node-like resolution</a> with Node-style <a href="https://nodejs.org/api/esm.html#interoperability-with-commonjs">interoperability between CommonJS and ES modules</a>.</p>
    <div>
      <h2>Durable Objects as test runners</h2>
      <a href="#durable-objects-as-test-runners">
        
      </a>
    </div>
    <p>Now that we can run and import arbitrary code, the next step is to get Vitest’s thread worker running inside <code>workerd</code>. Every incoming request has its own request context. To improve overall performance, I/O objects such as streams, request/response bodies and WebSockets created in one request context cannot be used from another. This means if we want to use a WebSocket for RPC between the pool and our <code>workerd</code> processes, we need to make sure the WebSocket is only used from one request context. To coordinate this, we define a singleton Durable Object for accepting the RPC connection and running tests from. Functions using RPC such as resolving modules, reporting results and console logging will always use this singleton. We use <a href="https://github.com/cloudflare/miniflare/pull/639">Miniflare’s “magic proxy” system</a> to get a reference to the singleton’s stub in Node.js, and send a WebSocket upgrade request directly to it. After adding a few more Node.js polyfills, and a basic <code>cloudflare:test</code> module to provide access to bindings and a function for creating <code>ExecutionContext</code>s, we’re able to write basic Workers unit tests! 🎉</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/2vRactoOowJXYwInIONXC9/68880e0b835a82a299d434f859607a1c/Vitest-Pool-Workers-Architecture--4-.png" />
            
            </figure>
    <div>
      <h2>Integration tests with hot-module-reloading</h2>
      <a href="#integration-tests-with-hot-module-reloading">
        
      </a>
    </div>
    <p>In addition to unit tests, we support integration testing with a special <code>SELF</code> service binding in the <code>cloudflare:test</code> module. This points to a special <code>export default { fetch(...) {...} }</code> handler which uses Vite to import your Worker’s <code>main</code> module.</p><p>Using Vite’s transformation pipeline here means your handler gets hot-module-reloading (HMR) for free! When code is updated, the module cache is invalidated, tests are rerun, and subsequent requests will execute with new code. The same approach of wrapping user code handlers applies to Durable Objects too, providing the same HMR benefits.</p><p>Integration tests can be written by calling <code>SELF.fetch()</code>, which will dispatch a <code>fetch()</code> event to your user code in the same global scope as your test, but under a different request context. This means global mocks apply to your Worker’s execution, as do request context lifetime restrictions. In particular, if you forget to call <code>ctx.waitUntil()</code>, you’ll see an appropriate error message. This wouldn’t be the case if you called your Worker’s handler directly in a unit test, as you’d be running under the runner singleton’s Durable Object request context, whose lifetime is automatically extended.</p>
            <pre><code>// test/index.spec.ts
import { SELF } from "cloudflare:test";
import { it, expect } from "vitest";
import "../src/index";

it("sends request", async () =&gt; {
   const response = await SELF.fetch("https://example.com");
   expect(await response.text()).toMatchInlineSnapshot(`"body"`);
});</code></pre>
            
    <div>
      <h2>Isolated per-test storage</h2>
      <a href="#isolated-per-test-storage">
        
      </a>
    </div>
    <p>Most Workers applications will have at least one binding to a Cloudflare storage service, such as KV, R2 or D1. Ideally, tests should be self-contained and runnable in any order or on their own. To make this possible, writes to storage need to be undone at the end of each test, so reads by other tests aren’t affected. Whilst it’s possible to do this manually, it can be tricky to keep track of all writes and undo them in the correct order. For example, take the following two functions:</p>
            <pre><code>// helpers.ts
interface Env {
  NAMESPACE: KVNamespace;
}
// Get the current list stored in a KV namespace
export async function get(env: Env, key: string): Promise&lt;string[]&gt; {
  return await env.NAMESPACE.get(key, "json") ?? [];
}
// Add an item to the end of the list
export async function append(env: Env, key: string, item: string) {
  const value = await get(env, key);
  value.push(item);
  await env.NAMESPACE.put(key, JSON.stringify(value));
}</code></pre>
            <p>If we wanted to test these functions, we might write something like below. Note we have to keep track of all the keys we might write to, and restore their values at the end of tests, even if those tests fail.</p>
            <pre><code>// helpers.spec.ts
import { env } from "cloudflare:test";
import { beforeAll, beforeEach, afterEach, it, expect } from "vitest";
import { get, append } from "./helpers";

let startingList1: string | null;
let startingList2: string | null;
beforeEach(async () =&gt; {
  // Store values before each test
  startingList1 = await env.NAMESPACE.get("list 1");
  startingList2 = await env.NAMESPACE.get("list 2");
});
afterEach(async () =&gt; {
  // Restore starting values after each test
  if (startingList1 === null) {
    await env.NAMESPACE.delete("list 1");
  } else {
    await env.NAMESPACE.put("list 1", startingList1);
  }
  if (startingList2 === null) {
    await env.NAMESPACE.delete("list 2");
  } else {
    await env.NAMESPACE.put("list 2", startingList2);
  }
});

beforeAll(async () =&gt; {
  await append(env, "list 1", "one");
});

it("appends to one list", async () =&gt; {
  await append(env, "list 1", "two");
  expect(await get(env, "list 1")).toStrictEqual(["one", "two"]);
});

it("appends to two lists", async () =&gt; {
  await append(env, "list 1", "three");
  await append(env, "list 2", "four");
  expect(await get(env, "list 1")).toStrictEqual(["one", "three"]);
  expect(await get(env, "list 2")).toStrictEqual(["four"]);
});</code></pre>
            <p>This is slightly easier with the recently introduced <a href="https://vitest.dev/api/#ontestfinished"><code>onTestFinished()</code> hook</a>, but you still need to remember which keys were written to, or enumerate them at the start/end of tests. You’d also need to manage this for KV, R2, Durable Objects, caches and any other storage service you used. Ideally, the testing framework should just manage this all for you.</p><p>That’s exactly what the Workers Vitest pool does with the <code>isolatedStorage</code> option which is enabled by default. Any writes to storage performed in a test are automagically undone at the end of the test. To support seeding data in <code>beforeAll()</code> hooks, including those in nested <code>describe()</code>-blocks, a stack is used. Before each suite or test, a new frame is pushed to the storage stack. All writes performed by the test or associated <code>beforeEach()</code>/<code>afterEach()</code> hooks are written to the frame. After each suite or test, the top frame is popped from the storage stack, undoing any writes.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/4Ixe0KPm6lrn7dvt7N1AhY/525418edee96350e76dffebf7c95895d/Untitled--3--1.png" />
            
            </figure><p>Miniflare implements simulators for storage services <a href="https://github.com/cloudflare/miniflare/pull/656">on top of Durable Objects</a> with <a href="https://github.com/cloudflare/miniflare/discussions/525">a separate blob store</a>. When running locally, <code>workerd</code> uses SQLite for Durable Object storage. To implement isolated storage, we implement an on-disk stack of <code>.sqlite</code> database files by backing up the databases when “pushing”, and restoring backups when “popping”. Blobs stored in the separate store are retained through stack operations, and cleaned up at the end of each test run. Whilst this works, it involves copying lots of <code>.sqlite</code> files. Looking ahead, we’d like to explore using SQLite <a href="https://www.sqlite.org/lang_savepoint.html"><code>SAVEPOINTS</code></a> for a more efficient solution.</p>
    <div>
      <h2>Declarative request mocking</h2>
      <a href="#declarative-request-mocking">
        
      </a>
    </div>
    <p>In addition to storage, most Workers will make outbound <code>fetch()</code> requests. For tests, it’s often useful to mock responses to these requests. Miniflare already allows you to specify an <a href="https://undici.nodejs.org/#/docs/api/MockAgent"><code>undici</code> <code>MockAgent</code></a> to route all requests through. The <code>MockAgent</code> class provides a declarative interface for specifying requests to mock and the corresponding responses to return. This API is relatively simple, whilst being flexible enough for advanced use cases. We provide an instance of <code>MockAgent</code> as <code>fetchMock</code> in the <code>cloudflare:test</code> module.</p>
            <pre><code>import { fetchMock } from "cloudflare:test";
import { beforeAll, afterEach, it, expect } from "vitest";

beforeAll(() =&gt; {
  // Enable outbound request mocking...
  fetchMock.activate();
  // ...and throw errors if an outbound request isn't mocked
  fetchMock.disableNetConnect();
});
// Ensure we matched every mock we defined
afterEach(() =&gt; fetchMock.assertNoPendingInterceptors());

it("mocks requests", async () =&gt; {
  // Mock the first request to `https://example.com`
  fetchMock
    .get("https://example.com")
    .intercept({ path: "/" })
    .reply(200, "body");

  const response = await fetch("https://example.com/");
  expect(await response.text()).toBe("body");
});</code></pre>
            <p>To implement this, we bundled a stripped down version of <code>undici</code> containing just the <code>MockAgent</code> code. We then <a href="https://github.com/cloudflare/workers-sdk/blob/main/packages/vitest-pool-workers/src/worker/fetch-mock.ts">built a custom <code>undici</code> <code>Dispatcher</code></a> that used the Worker’s global <code>fetch()</code> function instead of <code>undici</code>’s built-in HTTP implementation based on <a href="https://github.com/nodejs/llhttp"><code>llhttp</code></a> and <a href="https://nodejs.org/api/net.html"><code>node:net</code></a>.</p>
    <div>
      <h2>Testing Durable Objects directly</h2>
      <a href="#testing-durable-objects-directly">
        
      </a>
    </div>
    <p>Finally, Miniflare v2’s custom Vitest environment provided support for accessing the instance methods and state of Durable Objects in tests directly. This allowed you to unit test Durable Objects like any other JavaScript class—you could mock particular methods and properties, or immediately call specific handlers like <code>alarm()</code>. To implement this in <code>workerd</code>, we rely on our existing wrapping of user Durable Objects for Vite transforms and hot-module reloading. When you call the <code>runInDurableObject(stub, callback)</code> function from <code>cloudflare:test</code>, we store <code>callback</code> in a global cache and send a special <code>fetch()</code> request to <code>stub</code> which is intercepted by the wrapper. The wrapper executes the <code>callback</code> in the request context of the Durable Object, and stores the result in the same cache. <code>runInDurableObject()</code> then reads from this cache, and returns the result.</p><p>Note that this assumes the Durable Object is running in the same isolate as the <code>runInDurableObject()</code> call. While this is true for same-Worker Durable Objects running locally, it means Durable Objects defined in auxiliary workers can’t be accessed directly.</p>
    <div>
      <h2>Try it out!</h2>
      <a href="#try-it-out">
        
      </a>
    </div>
    <p>We are excited to release the <code>@cloudflare/vitest-pool-workers</code> package on npm, and to provide an improved testing experience for you.</p><p>Make sure to read the <a href="https://developers.cloudflare.com/workers/testing/vitest-integration/get-started/write-your-first-test/">Write your first test guide</a> and begin writing unit and integration tests today! If you’ve been writing tests using one of our previous options, our <code>unstable_dev</code> <a href="https://developers.cloudflare.com/workers/testing/vitest-integration/get-started/migrate-from-unstable-dev/">migration guide</a> or our Miniflare 2 <a href="https://developers.cloudflare.com/workers/testing/vitest-integration/get-started/migrate-from-miniflare-2/">migration guide</a> should explain key differences and help you move your tests over quickly.</p><p>If you run into issues or have suggestions for improvements, please <a href="https://github.com/cloudflare/workers-sdk/issues/new/choose">file an issue</a> in our GitHub repo or reach out via our <a href="https://discord.com/invite/cloudflaredev">Developer Discord</a>.</p> ]]></content:encoded>
            <category><![CDATA[Cloudflare Workers]]></category>
            <category><![CDATA[Developers]]></category>
            <category><![CDATA[Testing]]></category>
            <guid isPermaLink="false">P0mpqczsiU6cJvsOQWpbi</guid>
            <dc:creator>Brendan Coll</dc:creator>
            <dc:creator>Adam Murray</dc:creator>
        </item>
    </channel>
</rss>