<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://christianhelle.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://christianhelle.com/" rel="alternate" type="text/html" /><updated>2026-03-06T16:24:56+00:00</updated><id>https://christianhelle.com/feed.xml</id><title type="html">Christian Helle’s Blog</title><subtitle>This blog is dedicated to the art of building software. I write about my creations, discoveries, and solutions to problems that I encounter while building software.</subtitle><entry><title type="html">Building a web crawler and broken link detector in Zig</title><link href="https://christianhelle.com/2026/03/building-argiope-web-crawler-broken-link-detector.html" rel="alternate" type="text/html" title="Building a web crawler and broken link detector in Zig" /><published>2026-03-05T00:00:00+00:00</published><updated>2026-03-05T00:00:00+00:00</updated><id>https://christianhelle.com/2026/03/building-argiope-web-crawler-broken-link-detector</id><content type="html" xml:base="https://christianhelle.com/2026/03/building-argiope-web-crawler-broken-link-detector.html"><![CDATA[<p>I recently built a web crawler for broken link detection and image downloading in <a href="https://ziglang.org/">Zig</a>. The tool can crawl websites, detect broken links, generate reports in multiple formats, and download images from web pages. I named it <a href="https://github.com/christianhelle/argiope">Argiope</a> after the genus of orb-weaving spiders, which seemed fitting for a web crawler.</p>

<p>The source code is available on GitHub at <a href="https://github.com/christianhelle/argiope">https://github.com/christianhelle/argiope</a>.</p>

<p>Like my previous Zig project, GitHub Copilot wrote most of the boilerplate, including the GitHub workflows, README, install scripts, and snapcraft.yaml file. The entire project took a few evenings to build.</p>

<h2 id="how-it-works">How it works</h2>

<p>The crawler uses a <a href="https://en.wikipedia.org/wiki/Breadth-first_search">Breadth-first search (BFS)</a> approach to traverse web pages. It starts with a seed URL, fetches the page, extracts all links, and adds them to a queue for processing. Each URL is normalized and checked against a visited set to avoid processing the same URL twice.</p>

<div class="language-zig highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">pub</span> <span class="k">const</span> <span class="n">Crawler</span> <span class="o">=</span> <span class="k">struct</span> <span class="p">{</span>
    <span class="n">allocator</span><span class="p">:</span> <span class="n">std</span><span class="p">.</span><span class="py">mem</span><span class="p">.</span><span class="py">Allocator</span><span class="p">,</span>
    <span class="n">base_url</span><span class="p">:</span> <span class="p">[]</span><span class="kt">u8</span><span class="p">,</span>
    <span class="n">base_host</span><span class="p">:</span> <span class="p">[]</span><span class="kt">u8</span><span class="p">,</span>
    <span class="n">queue</span><span class="p">:</span> <span class="n">std</span><span class="p">.</span><span class="nf">ArrayListUnmanaged</span><span class="p">(</span><span class="n">QueueEntry</span><span class="p">)</span> <span class="o">=</span> <span class="p">.</span><span class="py">empty</span><span class="p">,</span>
    <span class="n">visited</span><span class="p">:</span> <span class="n">std</span><span class="p">.</span><span class="nf">StringHashMapUnmanaged</span><span class="p">(</span><span class="k">void</span><span class="p">)</span> <span class="o">=</span> <span class="p">.</span><span class="py">empty</span><span class="p">,</span>
    <span class="n">results</span><span class="p">:</span> <span class="n">std</span><span class="p">.</span><span class="nf">ArrayListUnmanaged</span><span class="p">(</span><span class="n">CrawlResult</span><span class="p">)</span> <span class="o">=</span> <span class="p">.</span><span class="py">empty</span><span class="p">,</span>
    <span class="n">options</span><span class="p">:</span> <span class="n">CrawlOptions</span><span class="p">,</span>

    <span class="k">pub</span> <span class="k">fn</span> <span class="n">init</span><span class="p">(</span><span class="n">allocator</span><span class="p">:</span> <span class="n">std</span><span class="p">.</span><span class="py">mem</span><span class="p">.</span><span class="py">Allocator</span><span class="p">,</span> <span class="n">url</span><span class="p">:</span> <span class="p">[]</span><span class="k">const</span> <span class="kt">u8</span><span class="p">,</span> <span class="n">options</span><span class="p">:</span> <span class="n">CrawlOptions</span><span class="p">)</span> <span class="n">Crawler</span> <span class="p">{</span>
        <span class="k">const</span> <span class="n">base_url</span> <span class="o">=</span> <span class="n">allocator</span><span class="p">.</span><span class="nf">dupe</span><span class="p">(</span><span class="kt">u8</span><span class="p">,</span> <span class="n">url</span><span class="p">)</span> <span class="k">catch</span> <span class="s">""</span><span class="p">;</span>
        <span class="k">const</span> <span class="n">base_host</span> <span class="o">=</span> <span class="n">url_mod</span><span class="p">.</span><span class="nf">extractHost</span><span class="p">(</span><span class="n">base_url</span><span class="p">)</span> <span class="k">orelse</span> <span class="s">""</span><span class="p">;</span>
        <span class="k">return</span> <span class="o">.</span><span class="p">{</span>
            <span class="p">.</span><span class="py">allocator</span> <span class="o">=</span> <span class="n">allocator</span><span class="p">,</span>
            <span class="p">.</span><span class="py">base_url</span> <span class="o">=</span> <span class="n">base_url</span><span class="p">,</span>
            <span class="p">.</span><span class="py">base_host</span> <span class="o">=</span> <span class="n">base_host</span><span class="p">,</span>
            <span class="p">.</span><span class="py">options</span> <span class="o">=</span> <span class="n">options</span><span class="p">,</span>
        <span class="p">};</span>
    <span class="p">}</span>

    <span class="k">pub</span> <span class="k">fn</span> <span class="n">crawl</span><span class="p">(</span><span class="n">self</span><span class="p">:</span> <span class="o">*</span><span class="n">Crawler</span><span class="p">)</span> <span class="o">!</span><span class="k">void</span> <span class="p">{</span>
        <span class="k">try</span> <span class="n">self</span><span class="p">.</span><span class="py">queue</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="py">allocator</span><span class="p">,</span> <span class="o">.</span><span class="p">{</span>
            <span class="p">.</span><span class="py">url</span> <span class="o">=</span> <span class="k">try</span> <span class="n">self</span><span class="p">.</span><span class="py">allocator</span><span class="p">.</span><span class="nf">dupe</span><span class="p">(</span><span class="kt">u8</span><span class="p">,</span> <span class="n">self</span><span class="p">.</span><span class="py">base_url</span><span class="p">),</span>
            <span class="p">.</span><span class="py">depth</span> <span class="o">=</span> <span class="mi">0</span><span class="p">,</span>
        <span class="p">});</span>

        <span class="k">if</span> <span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="py">options</span><span class="p">.</span><span class="py">parallel</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">try</span> <span class="n">self</span><span class="p">.</span><span class="nf">crawlParallel</span><span class="p">();</span>
        <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
            <span class="k">try</span> <span class="n">self</span><span class="p">.</span><span class="nf">crawlSequential</span><span class="p">();</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">};</span>
</code></pre></div></div>

<p>The tool supports both sequential and parallel crawling. In parallel mode, a thread pool processes multiple URLs concurrently, which significantly speeds up crawling for sites with many links.</p>

<p>Domain restriction is enforced by extracting the host from each URL and comparing it to the base URL’s host. External links are still checked for broken status but not followed for further crawling. This keeps the crawler focused on the target site while still validating outbound links.</p>

<div class="language-zig highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">pub</span> <span class="k">fn</span> <span class="n">isInternal</span><span class="p">(</span><span class="n">self</span><span class="p">:</span> <span class="o">*</span><span class="k">const</span> <span class="n">Crawler</span><span class="p">,</span> <span class="n">url</span><span class="p">:</span> <span class="p">[]</span><span class="k">const</span> <span class="kt">u8</span><span class="p">)</span> <span class="k">bool</span> <span class="p">{</span>
    <span class="k">const</span> <span class="n">host</span> <span class="o">=</span> <span class="n">url_mod</span><span class="p">.</span><span class="nf">extractHost</span><span class="p">(</span><span class="n">url</span><span class="p">)</span> <span class="k">orelse</span> <span class="k">return</span> <span class="kc">false</span><span class="p">;</span>
    <span class="k">return</span> <span class="n">std</span><span class="p">.</span><span class="py">mem</span><span class="p">.</span><span class="nf">eql</span><span class="p">(</span><span class="kt">u8</span><span class="p">,</span> <span class="n">host</span><span class="p">,</span> <span class="n">self</span><span class="p">.</span><span class="py">base_host</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="html-parsing">HTML Parsing</h2>

<p>Rather than pulling in a full HTML parser dependency, I wrote a lightweight scanner that extracts links and image sources. It iterates through the HTML looking for opening tags and extracts <code class="language-plaintext highlighter-rouge">href</code> attributes from anchor tags and <code class="language-plaintext highlighter-rouge">src</code> attributes from image tags.</p>

<div class="language-zig highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">pub</span> <span class="k">fn</span> <span class="n">extractLinks</span><span class="p">(</span><span class="n">allocator</span><span class="p">:</span> <span class="n">std</span><span class="p">.</span><span class="py">mem</span><span class="p">.</span><span class="py">Allocator</span><span class="p">,</span> <span class="n">html</span><span class="p">:</span> <span class="p">[]</span><span class="k">const</span> <span class="kt">u8</span><span class="p">)</span> <span class="o">!</span><span class="p">[]</span><span class="n">Link</span> <span class="p">{</span>
    <span class="k">var</span> <span class="n">links</span><span class="p">:</span> <span class="n">std</span><span class="p">.</span><span class="nf">ArrayListUnmanaged</span><span class="p">(</span><span class="n">Link</span><span class="p">)</span> <span class="o">=</span> <span class="p">.</span><span class="py">empty</span><span class="p">;</span>

    <span class="k">var</span> <span class="n">pos</span><span class="p">:</span> <span class="kt">usize</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
    <span class="k">while</span> <span class="p">(</span><span class="n">pos</span> <span class="o">&lt;</span> <span class="n">html</span><span class="p">.</span><span class="py">len</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">const</span> <span class="n">tag_start</span> <span class="o">=</span> <span class="n">std</span><span class="p">.</span><span class="py">mem</span><span class="p">.</span><span class="nf">indexOfPos</span><span class="p">(</span><span class="kt">u8</span><span class="p">,</span> <span class="n">html</span><span class="p">,</span> <span class="n">pos</span><span class="p">,</span> <span class="s">"&lt;"</span><span class="p">)</span> <span class="k">orelse</span> <span class="k">break</span><span class="p">;</span>
        <span class="n">pos</span> <span class="o">=</span> <span class="n">tag_start</span> <span class="o">+</span> <span class="mi">1</span><span class="p">;</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">pos</span> <span class="o">&gt;=</span> <span class="n">html</span><span class="p">.</span><span class="py">len</span><span class="p">)</span> <span class="k">break</span><span class="p">;</span>

        <span class="c">// Skip comments</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">pos</span> <span class="o">+</span> <span class="mi">2</span> <span class="o">&lt;</span> <span class="n">html</span><span class="p">.</span><span class="py">len</span> <span class="k">and</span> <span class="n">html</span><span class="p">[</span><span class="n">pos</span><span class="p">]</span> <span class="o">==</span> <span class="sc">'!'</span> <span class="k">and</span>
            <span class="n">html</span><span class="p">[</span><span class="n">pos</span> <span class="o">+</span> <span class="mi">1</span><span class="p">]</span> <span class="o">==</span> <span class="sc">'-'</span> <span class="k">and</span> <span class="n">html</span><span class="p">[</span><span class="n">pos</span> <span class="o">+</span> <span class="mi">2</span><span class="p">]</span> <span class="o">==</span> <span class="sc">'-'</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">const</span> <span class="n">comment_end</span> <span class="o">=</span> <span class="n">std</span><span class="p">.</span><span class="py">mem</span><span class="p">.</span><span class="nf">indexOfPos</span><span class="p">(</span><span class="kt">u8</span><span class="p">,</span> <span class="n">html</span><span class="p">,</span> <span class="n">pos</span><span class="p">,</span> <span class="s">"--&gt;"</span><span class="p">)</span> <span class="k">orelse</span> <span class="k">break</span><span class="p">;</span>
            <span class="n">pos</span> <span class="o">=</span> <span class="n">comment_end</span> <span class="o">+</span> <span class="mi">3</span><span class="p">;</span>
            <span class="k">continue</span><span class="p">;</span>
        <span class="p">}</span>

        <span class="c">// Read tag name and extract attributes...</span>
        <span class="k">const</span> <span class="n">tag_name</span> <span class="o">=</span> <span class="n">html</span><span class="p">[</span><span class="n">tag_name_start</span><span class="o">..</span><span class="n">pos</span><span class="p">];</span>

        <span class="c">// Determine what attribute we're looking for</span>
        <span class="k">const</span> <span class="n">attr_name</span><span class="p">:</span> <span class="o">?</span><span class="p">[]</span><span class="k">const</span> <span class="kt">u8</span> <span class="o">=</span> <span class="n">blk</span><span class="p">:</span> <span class="p">{</span>
            <span class="k">if</span> <span class="p">(</span><span class="n">asciiEqlIgnoreCase</span><span class="p">(</span><span class="n">tag_name</span><span class="p">,</span> <span class="s">"a"</span><span class="p">)</span> <span class="k">or</span>
                <span class="n">asciiEqlIgnoreCase</span><span class="p">(</span><span class="n">tag_name</span><span class="p">,</span> <span class="s">"link"</span><span class="p">)</span> <span class="k">or</span>
                <span class="n">asciiEqlIgnoreCase</span><span class="p">(</span><span class="n">tag_name</span><span class="p">,</span> <span class="s">"area"</span><span class="p">))</span>
            <span class="p">{</span>
                <span class="k">break</span> <span class="p">:</span><span class="n">blk</span> <span class="s">"href"</span><span class="p">;</span>
            <span class="p">}</span>
            <span class="k">if</span> <span class="p">(</span><span class="n">asciiEqlIgnoreCase</span><span class="p">(</span><span class="n">tag_name</span><span class="p">,</span> <span class="s">"img"</span><span class="p">)</span> <span class="k">or</span>
                <span class="n">asciiEqlIgnoreCase</span><span class="p">(</span><span class="n">tag_name</span><span class="p">,</span> <span class="s">"script"</span><span class="p">)</span> <span class="k">or</span>
                <span class="n">asciiEqlIgnoreCase</span><span class="p">(</span><span class="n">tag_name</span><span class="p">,</span> <span class="s">"source"</span><span class="p">))</span>
            <span class="p">{</span>
                <span class="k">break</span> <span class="p">:</span><span class="n">blk</span> <span class="s">"src"</span><span class="p">;</span>
            <span class="p">}</span>
            <span class="k">break</span> <span class="p">:</span><span class="n">blk</span> <span class="kc">null</span><span class="p">;</span>
        <span class="p">};</span>

        <span class="c">// Extract and store the attribute value...</span>
    <span class="p">}</span>

    <span class="k">return</span> <span class="n">links</span><span class="p">.</span><span class="nf">toOwnedSlice</span><span class="p">(</span><span class="n">allocator</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The scanner also handles <code class="language-plaintext highlighter-rouge">srcset</code> attributes on image tags, parsing the comma-separated list of image URLs. It skips JavaScript, mailto, tel, data URLs, and fragment-only links.</p>

<h2 id="url-normalization">URL Normalization</h2>

<p>URL handling is surprisingly complex. Relative URLs need to be resolved against the base URL, query parameters may need to be normalized, and trailing slashes should be handled consistently.</p>

<div class="language-zig highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">pub</span> <span class="k">fn</span> <span class="n">resolve</span><span class="p">(</span><span class="n">allocator</span><span class="p">:</span> <span class="n">std</span><span class="p">.</span><span class="py">mem</span><span class="p">.</span><span class="py">Allocator</span><span class="p">,</span> <span class="n">base</span><span class="p">:</span> <span class="p">[]</span><span class="k">const</span> <span class="kt">u8</span><span class="p">,</span> <span class="n">href</span><span class="p">:</span> <span class="p">[]</span><span class="k">const</span> <span class="kt">u8</span><span class="p">)</span> <span class="o">!</span><span class="p">[]</span><span class="kt">u8</span> <span class="p">{</span>
    <span class="c">// Absolute URL</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">std</span><span class="p">.</span><span class="py">mem</span><span class="p">.</span><span class="nf">indexOf</span><span class="p">(</span><span class="kt">u8</span><span class="p">,</span> <span class="n">href</span><span class="p">,</span> <span class="s">"://"</span><span class="p">)</span> <span class="o">!=</span> <span class="kc">null</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">return</span> <span class="n">allocator</span><span class="p">.</span><span class="nf">dupe</span><span class="p">(</span><span class="kt">u8</span><span class="p">,</span> <span class="n">href</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="c">// Protocol-relative URL</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">std</span><span class="p">.</span><span class="py">mem</span><span class="p">.</span><span class="nf">startsWith</span><span class="p">(</span><span class="kt">u8</span><span class="p">,</span> <span class="n">href</span><span class="p">,</span> <span class="s">"//"</span><span class="p">))</span> <span class="p">{</span>
        <span class="k">const</span> <span class="n">proto</span> <span class="o">=</span> <span class="n">extractProtocol</span><span class="p">(</span><span class="n">base</span><span class="p">)</span> <span class="k">orelse</span> <span class="s">"https"</span><span class="p">;</span>
        <span class="k">return</span> <span class="n">std</span><span class="p">.</span><span class="py">fmt</span><span class="p">.</span><span class="nf">allocPrint</span><span class="p">(</span><span class="n">allocator</span><span class="p">,</span> <span class="s">"{s}:{s}"</span><span class="p">,</span> <span class="o">.</span><span class="p">{</span> <span class="n">proto</span><span class="p">,</span> <span class="n">href</span> <span class="p">});</span>
    <span class="p">}</span>

    <span class="c">// Absolute path</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">href</span><span class="p">.</span><span class="py">len</span> <span class="o">&gt;</span> <span class="mi">0</span> <span class="k">and</span> <span class="n">href</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">==</span> <span class="sc">'/'</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">const</span> <span class="n">origin</span> <span class="o">=</span> <span class="k">try</span> <span class="n">extractOrigin</span><span class="p">(</span><span class="n">allocator</span><span class="p">,</span> <span class="n">base</span><span class="p">);</span>
        <span class="k">defer</span> <span class="n">allocator</span><span class="p">.</span><span class="nf">free</span><span class="p">(</span><span class="n">origin</span><span class="p">);</span>
        <span class="k">return</span> <span class="n">std</span><span class="p">.</span><span class="py">fmt</span><span class="p">.</span><span class="nf">allocPrint</span><span class="p">(</span><span class="n">allocator</span><span class="p">,</span> <span class="s">"{s}{s}"</span><span class="p">,</span> <span class="o">.</span><span class="p">{</span> <span class="n">origin</span><span class="p">,</span> <span class="n">href</span> <span class="p">});</span>
    <span class="p">}</span>

    <span class="c">// Relative path</span>
    <span class="k">const</span> <span class="n">base_dir</span> <span class="o">=</span> <span class="n">extractDirectory</span><span class="p">(</span><span class="n">base</span><span class="p">);</span>
    <span class="k">return</span> <span class="n">std</span><span class="p">.</span><span class="py">fmt</span><span class="p">.</span><span class="nf">allocPrint</span><span class="p">(</span><span class="n">allocator</span><span class="p">,</span> <span class="s">"{s}/{s}"</span><span class="p">,</span> <span class="o">.</span><span class="p">{</span> <span class="n">base_dir</span><span class="p">,</span> <span class="n">href</span> <span class="p">});</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">normalize</code> function ensures URLs are in a consistent form by converting to lowercase (for the scheme and host), removing default ports, and collapsing path segments.</p>

<h2 id="http-client">HTTP Client</h2>

<p>The tool uses Zig’s standard library HTTP client with custom timeout and redirect handling. Each request is wrapped with a timeout to avoid hanging on unresponsive servers.</p>

<div class="language-zig highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">pub</span> <span class="k">const</span> <span class="n">FetchOptions</span> <span class="o">=</span> <span class="k">struct</span> <span class="p">{</span>
    <span class="n">max_redirects</span><span class="p">:</span> <span class="kt">u8</span> <span class="o">=</span> <span class="mi">5</span><span class="p">,</span>
    <span class="n">timeout_ms</span><span class="p">:</span> <span class="kt">u32</span> <span class="o">=</span> <span class="mi">10_000</span><span class="p">,</span>
    <span class="n">max_body_size</span><span class="p">:</span> <span class="kt">usize</span> <span class="o">=</span> <span class="mi">10</span> <span class="o">*</span> <span class="mi">1024</span> <span class="o">*</span> <span class="mi">1024</span><span class="p">,</span>
<span class="p">};</span>

<span class="k">pub</span> <span class="k">fn</span> <span class="n">fetch</span><span class="p">(</span>
    <span class="n">client</span><span class="p">:</span> <span class="o">*</span><span class="n">std</span><span class="p">.</span><span class="py">http</span><span class="p">.</span><span class="py">Client</span><span class="p">,</span>
    <span class="n">allocator</span><span class="p">:</span> <span class="n">std</span><span class="p">.</span><span class="py">mem</span><span class="p">.</span><span class="py">Allocator</span><span class="p">,</span>
    <span class="n">url</span><span class="p">:</span> <span class="p">[]</span><span class="k">const</span> <span class="kt">u8</span><span class="p">,</span>
    <span class="n">options</span><span class="p">:</span> <span class="n">FetchOptions</span><span class="p">,</span>
<span class="p">)</span> <span class="o">!</span><span class="n">FetchResult</span> <span class="p">{</span>
    <span class="k">const</span> <span class="n">uri</span> <span class="o">=</span> <span class="k">try</span> <span class="n">std</span><span class="p">.</span><span class="py">Uri</span><span class="p">.</span><span class="nf">parse</span><span class="p">(</span><span class="n">url</span><span class="p">);</span>

    <span class="k">var</span> <span class="n">req</span> <span class="o">=</span> <span class="k">try</span> <span class="n">client</span><span class="p">.</span><span class="nf">open</span><span class="p">(.</span><span class="py">GET</span><span class="p">,</span> <span class="n">uri</span><span class="p">,</span> <span class="o">.</span><span class="p">{</span>
        <span class="p">.</span><span class="py">server_header_buffer</span> <span class="o">=</span> <span class="k">try</span> <span class="n">allocator</span><span class="p">.</span><span class="nf">alloc</span><span class="p">(</span><span class="kt">u8</span><span class="p">,</span> <span class="mi">16384</span><span class="p">),</span>
    <span class="p">});</span>
    <span class="k">defer</span> <span class="n">req</span><span class="p">.</span><span class="nf">deinit</span><span class="p">();</span>

    <span class="n">req</span><span class="p">.</span><span class="nf">send</span><span class="p">()</span> <span class="k">catch</span> <span class="p">|</span><span class="n">err</span><span class="p">|</span> <span class="p">{</span>
        <span class="k">return</span> <span class="n">FetchResult</span><span class="p">{</span>
            <span class="p">.</span><span class="py">status</span> <span class="o">=</span> <span class="mi">0</span><span class="p">,</span>
            <span class="p">.</span><span class="py">body</span> <span class="o">=</span> <span class="kc">null</span><span class="p">,</span>
            <span class="p">.</span><span class="py">error_msg</span> <span class="o">=</span> <span class="k">try</span> <span class="n">allocator</span><span class="p">.</span><span class="nf">dupe</span><span class="p">(</span><span class="kt">u8</span><span class="p">,</span> <span class="nb">@errorName</span><span class="p">(</span><span class="n">err</span><span class="p">)),</span>
        <span class="p">};</span>
    <span class="p">};</span>

    <span class="k">try</span> <span class="n">req</span><span class="p">.</span><span class="nf">wait</span><span class="p">();</span>

    <span class="k">const</span> <span class="n">status</span> <span class="o">=</span> <span class="n">@intFromEnum</span><span class="p">(</span><span class="n">req</span><span class="p">.</span><span class="py">response</span><span class="p">.</span><span class="py">status</span><span class="p">);</span>

    <span class="c">// Handle redirects</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">status</span> <span class="o">&gt;=</span> <span class="mi">300</span> <span class="k">and</span> <span class="n">status</span> <span class="o">&lt;</span> <span class="mi">400</span> <span class="k">and</span> <span class="n">options</span><span class="p">.</span><span class="py">max_redirects</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">const</span> <span class="n">location</span> <span class="o">=</span> <span class="n">req</span><span class="p">.</span><span class="py">response</span><span class="p">.</span><span class="py">headers</span><span class="p">.</span><span class="nf">getFirstValue</span><span class="p">(</span><span class="s">"location"</span><span class="p">)</span> <span class="k">orelse</span> <span class="p">{</span>
            <span class="k">return</span> <span class="k">error</span><span class="p">.</span><span class="py">InvalidRedirect</span><span class="p">;</span>
        <span class="p">};</span>
        <span class="c">// Follow redirect...</span>
    <span class="p">}</span>

    <span class="c">// Read response body...</span>
    <span class="k">const</span> <span class="n">body</span> <span class="o">=</span> <span class="k">try</span> <span class="n">req</span><span class="p">.</span><span class="nf">reader</span><span class="p">().</span><span class="nf">readAllAlloc</span><span class="p">(</span><span class="n">allocator</span><span class="p">,</span> <span class="n">options</span><span class="p">.</span><span class="py">max_body_size</span><span class="p">);</span>

    <span class="k">return</span> <span class="n">FetchResult</span><span class="p">{</span>
        <span class="p">.</span><span class="py">status</span> <span class="o">=</span> <span class="nb">@intCast</span><span class="p">(</span><span class="n">status</span><span class="p">),</span>
        <span class="p">.</span><span class="py">body</span> <span class="o">=</span> <span class="n">body</span><span class="p">,</span>
        <span class="p">.</span><span class="py">error_msg</span> <span class="o">=</span> <span class="kc">null</span><span class="p">,</span>
    <span class="p">};</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The client handles HTTP redirects up to a configurable limit and collects both the status code and response body. Errors during the request are captured and returned as part of the result rather than propagated, allowing the crawler to continue processing other URLs.</p>

<h2 id="command-line-interface">Command Line Interface</h2>

<p>The CLI supports two main commands: <code class="language-plaintext highlighter-rouge">check</code> for broken link detection and <code class="language-plaintext highlighter-rouge">images</code> for downloading images. Options include crawl depth, timeout, request delay, and output format.</p>

<div class="language-zig highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">pub</span> <span class="k">const</span> <span class="n">Command</span> <span class="o">=</span> <span class="k">enum</span> <span class="p">{</span>
    <span class="n">check</span><span class="p">,</span>
    <span class="n">images</span><span class="p">,</span>
    <span class="n">help</span><span class="p">,</span>
    <span class="n">version_cmd</span><span class="p">,</span>
<span class="p">};</span>

<span class="k">pub</span> <span class="k">const</span> <span class="n">Options</span> <span class="o">=</span> <span class="k">struct</span> <span class="p">{</span>
    <span class="n">command</span><span class="p">:</span> <span class="n">Command</span> <span class="o">=</span> <span class="p">.</span><span class="py">help</span><span class="p">,</span>
    <span class="n">url</span><span class="p">:</span> <span class="o">?</span><span class="p">[]</span><span class="k">const</span> <span class="kt">u8</span> <span class="o">=</span> <span class="kc">null</span><span class="p">,</span>
    <span class="n">depth</span><span class="p">:</span> <span class="kt">u16</span> <span class="o">=</span> <span class="mi">3</span><span class="p">,</span>
    <span class="n">timeout_ms</span><span class="p">:</span> <span class="kt">u32</span> <span class="o">=</span> <span class="mi">10_000</span><span class="p">,</span>
    <span class="n">delay_ms</span><span class="p">:</span> <span class="kt">u32</span> <span class="o">=</span> <span class="mi">100</span><span class="p">,</span>
    <span class="n">output_dir</span><span class="p">:</span> <span class="p">[]</span><span class="k">const</span> <span class="kt">u8</span> <span class="o">=</span> <span class="s">"./download"</span><span class="p">,</span>
    <span class="n">verbose</span><span class="p">:</span> <span class="k">bool</span> <span class="o">=</span> <span class="kc">false</span><span class="p">,</span>
    <span class="n">parallel</span><span class="p">:</span> <span class="k">bool</span> <span class="o">=</span> <span class="kc">false</span><span class="p">,</span>
    <span class="n">report</span><span class="p">:</span> <span class="o">?</span><span class="p">[]</span><span class="k">const</span> <span class="kt">u8</span> <span class="o">=</span> <span class="kc">null</span><span class="p">,</span>
    <span class="n">report_format</span><span class="p">:</span> <span class="n">ReportFormat</span> <span class="o">=</span> <span class="p">.</span><span class="py">text</span><span class="p">,</span>
    <span class="n">include_positives</span><span class="p">:</span> <span class="k">bool</span> <span class="o">=</span> <span class="kc">false</span><span class="p">,</span>
<span class="p">};</span>

<span class="k">pub</span> <span class="k">fn</span> <span class="n">parseArgs</span><span class="p">(</span><span class="n">args</span><span class="p">:</span> <span class="p">[]</span><span class="k">const</span> <span class="p">[]</span><span class="k">const</span> <span class="kt">u8</span><span class="p">)</span> <span class="o">!</span><span class="n">Options</span> <span class="p">{</span>
    <span class="k">var</span> <span class="n">opts</span> <span class="o">=</span> <span class="n">Options</span><span class="p">{};</span>

    <span class="k">if</span> <span class="p">(</span><span class="n">args</span><span class="p">.</span><span class="py">len</span> <span class="o">&lt;</span> <span class="mi">2</span><span class="p">)</span> <span class="k">return</span> <span class="n">opts</span><span class="p">;</span>

    <span class="k">var</span> <span class="n">i</span><span class="p">:</span> <span class="kt">usize</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
    <span class="k">while</span> <span class="p">(</span><span class="n">i</span> <span class="o">&lt;</span> <span class="n">args</span><span class="p">.</span><span class="py">len</span><span class="p">)</span> <span class="p">:</span> <span class="p">(</span><span class="n">i</span> <span class="o">+=</span> <span class="mi">1</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">const</span> <span class="n">arg</span> <span class="o">=</span> <span class="n">args</span><span class="p">[</span><span class="n">i</span><span class="p">];</span>

        <span class="k">if</span> <span class="p">(</span><span class="n">std</span><span class="p">.</span><span class="py">mem</span><span class="p">.</span><span class="nf">eql</span><span class="p">(</span><span class="kt">u8</span><span class="p">,</span> <span class="n">arg</span><span class="p">,</span> <span class="s">"check"</span><span class="p">))</span> <span class="p">{</span>
            <span class="n">opts</span><span class="p">.</span><span class="py">command</span> <span class="o">=</span> <span class="p">.</span><span class="py">check</span><span class="p">;</span>
        <span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">std</span><span class="p">.</span><span class="py">mem</span><span class="p">.</span><span class="nf">eql</span><span class="p">(</span><span class="kt">u8</span><span class="p">,</span> <span class="n">arg</span><span class="p">,</span> <span class="s">"images"</span><span class="p">))</span> <span class="p">{</span>
            <span class="n">opts</span><span class="p">.</span><span class="py">command</span> <span class="o">=</span> <span class="p">.</span><span class="py">images</span><span class="p">;</span>
        <span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">std</span><span class="p">.</span><span class="py">mem</span><span class="p">.</span><span class="nf">startsWith</span><span class="p">(</span><span class="kt">u8</span><span class="p">,</span> <span class="n">arg</span><span class="p">,</span> <span class="s">"--depth"</span><span class="p">))</span> <span class="p">{</span>
            <span class="c">// Parse depth value...</span>
        <span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">std</span><span class="p">.</span><span class="py">mem</span><span class="p">.</span><span class="nf">startsWith</span><span class="p">(</span><span class="kt">u8</span><span class="p">,</span> <span class="n">arg</span><span class="p">,</span> <span class="s">"--timeout"</span><span class="p">))</span> <span class="p">{</span>
            <span class="c">// Parse timeout value...</span>
        <span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">std</span><span class="p">.</span><span class="py">mem</span><span class="p">.</span><span class="nf">startsWith</span><span class="p">(</span><span class="kt">u8</span><span class="p">,</span> <span class="n">arg</span><span class="p">,</span> <span class="s">"-"</span><span class="p">))</span> <span class="p">{</span>
            <span class="n">opts</span><span class="p">.</span><span class="py">url</span> <span class="o">=</span> <span class="n">arg</span><span class="p">;</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="k">return</span> <span class="n">opts</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The parser iterates through command-line arguments, identifying commands, flags, and values. It handles both short (<code class="language-plaintext highlighter-rouge">-v</code>) and long (<code class="language-plaintext highlighter-rouge">--verbose</code>) flag formats.</p>

<h2 id="report-generation">Report Generation</h2>

<p>The tool generates reports in three formats: plain text, Markdown, and HTML. Reports can include just broken links or all checked URLs depending on the <code class="language-plaintext highlighter-rouge">--include-positives</code> flag.</p>

<div class="language-zig highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">pub</span> <span class="k">fn</span> <span class="n">write</span><span class="p">(</span>
    <span class="n">allocator</span><span class="p">:</span> <span class="n">std</span><span class="p">.</span><span class="py">mem</span><span class="p">.</span><span class="py">Allocator</span><span class="p">,</span>
    <span class="n">path</span><span class="p">:</span> <span class="p">[]</span><span class="k">const</span> <span class="kt">u8</span><span class="p">,</span>
    <span class="n">format</span><span class="p">:</span> <span class="n">cli_mod</span><span class="p">.</span><span class="py">ReportFormat</span><span class="p">,</span>
    <span class="n">url</span><span class="p">:</span> <span class="p">[]</span><span class="k">const</span> <span class="kt">u8</span><span class="p">,</span>
    <span class="n">results</span><span class="p">:</span> <span class="p">[]</span><span class="k">const</span> <span class="n">crawler_mod</span><span class="p">.</span><span class="py">CrawlResult</span><span class="p">,</span>
    <span class="n">summary</span><span class="p">:</span> <span class="n">summary_mod</span><span class="p">.</span><span class="py">CheckSummary</span><span class="p">,</span>
    <span class="n">include_positives</span><span class="p">:</span> <span class="k">bool</span><span class="p">,</span>
<span class="p">)</span> <span class="o">!</span><span class="k">void</span> <span class="p">{</span>
    <span class="k">const</span> <span class="n">file</span> <span class="o">=</span> <span class="k">try</span> <span class="n">std</span><span class="p">.</span><span class="py">fs</span><span class="p">.</span><span class="nf">cwd</span><span class="p">().</span><span class="nf">createFile</span><span class="p">(</span><span class="n">path</span><span class="p">,</span> <span class="o">.</span><span class="p">{</span> <span class="p">.</span><span class="py">truncate</span> <span class="o">=</span> <span class="kc">true</span> <span class="p">});</span>
    <span class="k">defer</span> <span class="n">file</span><span class="p">.</span><span class="nf">close</span><span class="p">();</span>

    <span class="k">var</span> <span class="n">buf</span><span class="p">:</span> <span class="p">[</span><span class="mi">65536</span><span class="p">]</span><span class="kt">u8</span> <span class="o">=</span> <span class="k">undefined</span><span class="p">;</span>
    <span class="k">var</span> <span class="n">fw</span> <span class="o">=</span> <span class="n">file</span><span class="p">.</span><span class="nf">writer</span><span class="p">(</span><span class="o">&amp;</span><span class="n">buf</span><span class="p">);</span>
    <span class="k">const</span> <span class="n">w</span> <span class="o">=</span> <span class="o">&amp;</span><span class="n">fw</span><span class="p">.</span><span class="py">interface</span><span class="p">;</span>

    <span class="k">switch</span> <span class="p">(</span><span class="n">format</span><span class="p">)</span> <span class="p">{</span>
        <span class="p">.</span><span class="py">text</span> <span class="o">=&gt;</span> <span class="k">try</span> <span class="n">writeText</span><span class="p">(</span><span class="n">w</span><span class="p">,</span> <span class="n">url</span><span class="p">,</span> <span class="n">results</span><span class="p">,</span> <span class="n">summary</span><span class="p">,</span> <span class="n">include_positives</span><span class="p">),</span>
        <span class="p">.</span><span class="py">markdown</span> <span class="o">=&gt;</span> <span class="k">try</span> <span class="n">writeMarkdown</span><span class="p">(</span><span class="n">w</span><span class="p">,</span> <span class="n">url</span><span class="p">,</span> <span class="n">results</span><span class="p">,</span> <span class="n">summary</span><span class="p">,</span> <span class="n">include_positives</span><span class="p">),</span>
        <span class="p">.</span><span class="py">html</span> <span class="o">=&gt;</span> <span class="k">try</span> <span class="n">writeHtml</span><span class="p">(</span><span class="n">allocator</span><span class="p">,</span> <span class="n">w</span><span class="p">,</span> <span class="n">url</span><span class="p">,</span> <span class="n">results</span><span class="p">,</span> <span class="n">summary</span><span class="p">,</span> <span class="n">include_positives</span><span class="p">),</span>
    <span class="p">}</span>

    <span class="k">try</span> <span class="n">w</span><span class="p">.</span><span class="nf">flush</span><span class="p">();</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The HTML report is self-contained with inline CSS and uses a card-based layout with color-coded status badges. This makes it suitable for embedding in CI/CD pipelines or sharing as a standalone file.</p>

<h2 id="usage">Usage</h2>

<p>The basic usage is straightforward. Run <code class="language-plaintext highlighter-rouge">argiope check &lt;url&gt;</code> to scan a website for broken links:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ argiope check https://christianhelle.com --depth 3

Crawling https://christianhelle.com (depth=3, timeout=10s)...

------------------------------------------------------------------------
Status   Type       Time(ms)   URL
------------------------------------------------------------------------
404      internal   45         https://christianhelle.com/missing-page
------------------------------------------------------------------------

Summary:
  Total URLs checked: 127
  OK:                 126
  Broken:             1
  Errors:             0
  Internal:           115
  External:           12

Timing:
  Total crawl time:   2345ms
  Avg response time:  18ms
  Min response time:  8ms
  Max response time:  156ms
</code></pre></div></div>

<p>For downloading images, use the <code class="language-plaintext highlighter-rouge">images</code> command:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ argiope images https://example.com/gallery -o ./images

Downloading images from https://example.com/gallery...

Downloaded: page_1/image_1.jpg
Downloaded: page_1/image_2.jpg
Downloaded: page_2/image_1.png
...

Downloaded 42 images to ./images
</code></pre></div></div>

<p>Generate a report file instead of printing to the console:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>argiope check https://christianhelle.com --report report.html --report-format html

argiope check https://christianhelle.com --report report.md --report-format markdown --include-positives
</code></pre></div></div>

<p>Use parallel crawling for faster processing on sites with many links:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>argiope check https://christianhelle.com --parallel --depth 5
</code></pre></div></div>

<h2 id="distribution">Distribution</h2>

<p>Like my previous Zig project, I wanted simple distribution across platforms. GitHub Copilot generated the installation scripts and snapcraft configuration.</p>

<p>The <code class="language-plaintext highlighter-rouge">install.sh</code> script downloads the latest release for Linux or macOS:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/usr/bin/env bash</span>
<span class="nb">set</span> <span class="nt">-e</span>

<span class="nv">OS</span><span class="o">=</span><span class="si">$(</span><span class="nb">uname</span> <span class="nt">-s</span> | <span class="nb">tr</span> <span class="s1">'[:upper:]'</span> <span class="s1">'[:lower:]'</span><span class="si">)</span>
<span class="nv">ARCH</span><span class="o">=</span><span class="si">$(</span><span class="nb">uname</span> <span class="nt">-m</span><span class="si">)</span>

<span class="k">case</span> <span class="nv">$ARCH</span> <span class="k">in
    </span>x86_64<span class="p">)</span> <span class="nv">ARCH</span><span class="o">=</span><span class="s2">"x86_64"</span> <span class="p">;;</span>
    aarch64|arm64<span class="p">)</span> <span class="nv">ARCH</span><span class="o">=</span><span class="s2">"aarch64"</span> <span class="p">;;</span>
    <span class="k">*</span><span class="p">)</span> <span class="nb">echo</span> <span class="s2">"Unsupported architecture: </span><span class="nv">$ARCH</span><span class="s2">"</span><span class="p">;</span> <span class="nb">exit </span>1 <span class="p">;;</span>
<span class="k">esac</span>

<span class="k">case</span> <span class="nv">$OS</span> <span class="k">in
    </span>linux<span class="p">)</span> <span class="nv">PLATFORM</span><span class="o">=</span><span class="s2">"linux-</span><span class="nv">$ARCH</span><span class="s2">"</span> <span class="p">;;</span>
    darwin<span class="p">)</span> <span class="nv">PLATFORM</span><span class="o">=</span><span class="s2">"macos-</span><span class="nv">$ARCH</span><span class="s2">"</span> <span class="p">;;</span>
    <span class="k">*</span><span class="p">)</span> <span class="nb">echo</span> <span class="s2">"Unsupported OS: </span><span class="nv">$OS</span><span class="s2">"</span><span class="p">;</span> <span class="nb">exit </span>1 <span class="p">;;</span>
<span class="k">esac</span>

<span class="nv">URL</span><span class="o">=</span><span class="s2">"https://github.com/christianhelle/argiope/releases/latest/download/argiope-</span><span class="nv">$PLATFORM</span><span class="s2">"</span>

curl <span class="nt">-L</span> <span class="s2">"</span><span class="nv">$URL</span><span class="s2">"</span> <span class="nt">-o</span> argiope
<span class="nb">chmod</span> +x argiope
<span class="nb">sudo mv </span>argiope /usr/local/bin/

<span class="nb">echo</span> <span class="s2">"argiope installed successfully!"</span>
</code></pre></div></div>

<p>For Windows users, <code class="language-plaintext highlighter-rouge">install.ps1</code> does the same:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="bp">$Error</span><span class="n">ActionPreference</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Stop"</span><span class="w">

</span><span class="nv">$url</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"https://github.com/christianhelle/argiope/releases/latest/download/argiope-windows-x86_64.exe"</span><span class="w">
</span><span class="nv">$dest</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"</span><span class="nv">$</span><span class="nn">env</span><span class="p">:</span><span class="nv">USERPROFILE</span><span class="s2">\bin\argiope.exe"</span><span class="w">

</span><span class="n">New-Item</span><span class="w"> </span><span class="nt">-ItemType</span><span class="w"> </span><span class="nx">Directory</span><span class="w"> </span><span class="nt">-Force</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="p">(</span><span class="n">Split-Path</span><span class="w"> </span><span class="nv">$dest</span><span class="p">)</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Out-Null</span><span class="w">
</span><span class="nx">Invoke-WebRequest</span><span class="w"> </span><span class="nt">-Uri</span><span class="w"> </span><span class="nv">$url</span><span class="w"> </span><span class="nt">-OutFile</span><span class="w"> </span><span class="nv">$dest</span><span class="w">

</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"argiope installed to </span><span class="nv">$dest</span><span class="s2">"</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Add </span><span class="nv">$</span><span class="nn">env</span><span class="p">:</span><span class="nv">USERPROFILE</span><span class="s2">\bin to your PATH if needed."</span><span class="w">
</span></code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">snapcraft.yaml</code> configuration allows publishing to the Snap Store:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">name</span><span class="pi">:</span> <span class="s">argiope</span>
<span class="na">base</span><span class="pi">:</span> <span class="s">core22</span>
<span class="na">version</span><span class="pi">:</span> <span class="s2">"</span><span class="s">0.1.0"</span>
<span class="na">summary</span><span class="pi">:</span> <span class="s">A web crawler for broken-link detection</span>
<span class="na">description</span><span class="pi">:</span> <span class="pi">|</span>
  <span class="s">A fast, multi-threaded web crawler that detects broken links,</span>
  <span class="s">generates reports, and downloads images.</span>
<span class="na">grade</span><span class="pi">:</span> <span class="s">stable</span>
<span class="na">confinement</span><span class="pi">:</span> <span class="s">strict</span>

<span class="na">apps</span><span class="pi">:</span>
  <span class="na">argiope</span><span class="pi">:</span>
    <span class="na">command</span><span class="pi">:</span> <span class="s">bin/argiope</span>
    <span class="na">plugs</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">network</span>
      <span class="pi">-</span> <span class="s">home</span>
</code></pre></div></div>

<p>The GitHub Actions workflow builds binaries for all platforms and attaches them to releases:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">jobs</span><span class="pi">:</span>
  <span class="na">build</span><span class="pi">:</span>
    <span class="na">strategy</span><span class="pi">:</span>
      <span class="na">matrix</span><span class="pi">:</span>
        <span class="na">include</span><span class="pi">:</span>
          <span class="pi">-</span> <span class="na">os</span><span class="pi">:</span> <span class="s">ubuntu-latest</span>
            <span class="na">target</span><span class="pi">:</span> <span class="s">x86_64-linux</span>
          <span class="pi">-</span> <span class="na">os</span><span class="pi">:</span> <span class="s">ubuntu-latest</span>
            <span class="na">target</span><span class="pi">:</span> <span class="s">aarch64-linux</span>
          <span class="pi">-</span> <span class="na">os</span><span class="pi">:</span> <span class="s">macos-latest</span>
            <span class="na">target</span><span class="pi">:</span> <span class="s">x86_64-macos</span>
          <span class="pi">-</span> <span class="na">os</span><span class="pi">:</span> <span class="s">macos-latest</span>
            <span class="na">target</span><span class="pi">:</span> <span class="s">aarch64-macos</span>
          <span class="pi">-</span> <span class="na">os</span><span class="pi">:</span> <span class="s">windows-latest</span>
            <span class="na">target</span><span class="pi">:</span> <span class="s">x86_64-windows</span>
</code></pre></div></div>

<h2 id="conclusion">Conclusion</h2>

<p>Building Argiope was a great exercise in working with Zig’s standard library, particularly the HTTP client and file system APIs. The tool is fast, produces a single static binary with zero dependencies, and runs on Linux, macOS, and Windows.</p>

<p>If you need to check your website for broken links or download images from web pages, give Argiope a try. The source code is on GitHub at <a href="https://github.com/christianhelle/argiope">https://github.com/christianhelle/argiope</a>.</p>]]></content><author><name>Christian Helle</name></author><category term="Zig" /><category term="CLI" /><summary type="html"><![CDATA[I recently built a web crawler for broken link detection and image downloading in Zig. The tool can crawl websites, detect broken links, generate reports in multiple formats, and download images from web pages. I named it Argiope after the genus of orb-weaving spiders, which seemed fitting for a web crawler.]]></summary></entry><entry><title type="html">Modernizing REST API Client Code Generator with the New Visual Studio Extensibility Model</title><link href="https://christianhelle.com/2026/02/building-rest-api-client-code-generator-with-new-vs-extensibility-model.html" rel="alternate" type="text/html" title="Modernizing REST API Client Code Generator with the New Visual Studio Extensibility Model" /><published>2026-02-22T00:00:00+00:00</published><updated>2026-02-22T00:00:00+00:00</updated><id>https://christianhelle.com/2026/02/building-rest-api-client-code-generator-with-new-vs-extensibility-model</id><content type="html" xml:base="https://christianhelle.com/2026/02/building-rest-api-client-code-generator-with-new-vs-extensibility-model.html"><![CDATA[<p>I recently rebuilt the <strong>REST API Client Code Generator</strong> extension for Visual Studio from the ground up using the new <strong>Visual Studio.Extensibility</strong> model. This migration allowed the extension to run out-of-process and leverage the full power of .NET 8.0. In this post, I’ll walk through the architectural changes, the challenges of the old model, and how the new extensibility API simplifies modern extension development.</p>

<p>The source code for the new extension is available on <a href="https://github.com/christianhelle/apiclientcodegen/tree/master/src/VSIX/ApiClientCodeGen.VSIX.Extensibility">GitHub</a>.</p>

<h2 id="the-evolution-of-an-extension">The Evolution of an Extension</h2>

<p>The original version of this extension (source code available on <a href="https://github.com/christianhelle/apiclientcodegen/tree/master/src/VSIX/ApiClientCodeGen.VSIX.Dev17">GitHub</a>) was built using the traditional Visual Studio SDK. It served its purpose well, but as with any software built on older frameworks, it began to show its age.</p>

<h3 id="the-dependency-hell-of-in-process-extensions">The “Dependency Hell” of In-Process Extensions</h3>

<p>One of the most significant pain points with the old Visual Studio SDK is that extensions run <strong>in-process</strong> with Visual Studio. This means your extension shares the same memory space and dependencies as the IDE itself.</p>

<p>A classic example of this is <code class="language-plaintext highlighter-rouge">Newtonsoft.Json</code>. If your extension depends on version 13.0.1, but Visual Studio has loaded version 12.0.3 for its own internal use, you enter “Dependency Hell”. You’re forced to use binding redirects, assembly loading hacks, or simply hope for the best.</p>

<p>By moving to the new out-of-process extensibility model, the extension runs in its own isolated process. This architecture provides several key benefits:</p>

<ol>
  <li><strong>True Isolation</strong>: The extension runs on .NET 8.0, completely independent of the .NET Framework 4.8 runtime that powers Visual Studio.</li>
  <li><strong>No More Binding Redirects</strong>: I can use any version of any library I want—including <code class="language-plaintext highlighter-rouge">System.Text.Json</code> or newer versions of <code class="language-plaintext highlighter-rouge">Newtonsoft.Json</code>—without clashing with Visual Studio’s dependencies.</li>
  <li><strong>Improved Stability</strong>: If the extension crashes, it doesn’t take down the entire IDE with it.</li>
</ol>

<h2 id="from-custom-tools-to-explicit-commands">From “Custom Tools” to Explicit Commands</h2>

<p>In the previous version, the extension relied on the <strong>Single File Generator</strong> (or “Custom Tool”) mechanism. This is a legacy feature where you select a file in Solution Explorer, go to the Properties window, and type a magic string like <code class="language-plaintext highlighter-rouge">RefitterCodeGenerator</code> into the “Custom Tool” field.</p>

<p>While this felt “integrated,” it had significant drawbacks:</p>

<ul>
  <li><strong>Discoverability</strong>: Users had to know the magic string to type.</li>
  <li><strong>Silent Failures</strong>: If the generation failed, it was often difficult to see why without digging into the Output window.</li>
  <li><strong>Blocking the UI</strong>: The generation happened on the UI thread, freezing Visual Studio for large API specifications.</li>
</ul>

<p>The new extension abandons this pattern in favor of <strong>explicit context menu commands</strong>. You now simply right-click an OpenAPI file and select <strong>“Generate Client Code”</strong>.</p>

<h3 id="architecture-comparison-entry-points">Architecture Comparison: Entry Points</h3>

<p>The difference in how the extension is initialized is striking. The old model used the <code class="language-plaintext highlighter-rouge">AsyncPackage</code> class, decorated with a multitude of attributes to register menus, tool windows, and options pages.</p>

<p><strong>Old VSPackage Entry Point - <a href="https://github.com/christianhelle/apiclientcodegen/blob/master/src/VSIX/ApiClientCodeGen.VSIX.Shared/VsPackage.cs"><code class="language-plaintext highlighter-rouge">VsPackage.cs</code></a>:</strong></p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="nf">Guid</span><span class="p">(</span><span class="s">"47AFE4E1-5A52-4FE1-8CA7-EDB8310BDA4A"</span><span class="p">)]</span>
<span class="p">[</span><span class="nf">ProvideMenuResource</span><span class="p">(</span><span class="s">"Menus.ctmenu"</span><span class="p">,</span> <span class="m">1</span><span class="p">)]</span>
<span class="p">[</span><span class="nf">ProvideUIContextRule</span><span class="p">(...)]</span>
<span class="p">[</span><span class="nf">ProvideOptionPage</span><span class="p">(</span><span class="k">typeof</span><span class="p">(</span><span class="n">GeneralOptionPage</span><span class="p">),</span> <span class="n">VsixName</span><span class="p">,</span> <span class="n">GeneralOptionPage</span><span class="p">.</span><span class="n">Name</span><span class="p">,</span> <span class="m">0</span><span class="p">,</span> <span class="m">0</span><span class="p">,</span> <span class="k">true</span><span class="p">)]</span>
<span class="k">public</span> <span class="k">sealed</span> <span class="k">class</span> <span class="nc">VsPackage</span> <span class="p">:</span> <span class="n">AsyncPackage</span>
<span class="p">{</span>
    <span class="k">private</span> <span class="k">readonly</span> <span class="n">ICommandInitializer</span><span class="p">[]</span> <span class="n">commands</span> <span class="p">=</span> <span class="p">{</span>
        <span class="k">new</span> <span class="nf">AutoRestCodeGeneratorCustomToolSetter</span><span class="p">(),</span>
        <span class="k">new</span> <span class="nf">NSwagCodeGeneratorCustomToolSetter</span><span class="p">(),</span>
        <span class="c1">// ...</span>
    <span class="p">};</span>

    <span class="k">protected</span> <span class="k">override</span> <span class="k">async</span> <span class="n">Task</span> <span class="nf">InitializeAsync</span><span class="p">(</span>
        <span class="n">CancellationToken</span> <span class="n">cancellationToken</span><span class="p">,</span>
        <span class="n">IProgress</span><span class="p">&lt;</span><span class="n">ServiceProgressData</span><span class="p">&gt;</span> <span class="n">progress</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="k">await</span> <span class="n">JoinableTaskFactory</span><span class="p">.</span><span class="nf">SwitchToMainThreadAsync</span><span class="p">(</span><span class="n">cancellationToken</span><span class="p">);</span>
        <span class="k">await</span> <span class="k">base</span><span class="p">.</span><span class="nf">InitializeAsync</span><span class="p">(</span><span class="n">cancellationToken</span><span class="p">,</span> <span class="n">progress</span><span class="p">);</span>

        <span class="k">foreach</span> <span class="p">(</span><span class="kt">var</span> <span class="n">command</span> <span class="k">in</span> <span class="n">commands</span><span class="p">)</span>
            <span class="k">await</span> <span class="n">command</span><span class="p">.</span><span class="nf">InitializeAsync</span><span class="p">(</span><span class="k">this</span><span class="p">,</span> <span class="n">cancellationToken</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The new model uses a cleaner, more modern approach. The entry point inherits from <code class="language-plaintext highlighter-rouge">Extension</code>, and we use <strong>Dependency Injection</strong> right out of the box.</p>

<p><strong>New Extension Entry Point - <a href="https://github.com/christianhelle/apiclientcodegen/blob/master/src/VSIX/ApiClientCodeGen.VSIX.Extensibility/ExtensionEntrypoint.cs"><code class="language-plaintext highlighter-rouge">ExtensionEntrypoint.cs</code></a>:</strong></p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="n">VisualStudioContribution</span><span class="p">]</span>
<span class="k">internal</span> <span class="k">class</span> <span class="nc">ExtensionEntrypoint</span> <span class="p">:</span> <span class="n">Extension</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="k">override</span> <span class="n">ExtensionConfiguration</span> <span class="n">ExtensionConfiguration</span> <span class="p">=&gt;</span> <span class="k">new</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="n">Metadata</span> <span class="p">=</span> <span class="k">new</span><span class="p">(</span>
            <span class="n">id</span><span class="p">:</span> <span class="s">"f7530eb1-1ce9-46ac-8fab-165b68cf3d61"</span><span class="p">,</span>
            <span class="n">version</span><span class="p">:</span> <span class="n">ExtensionAssemblyVersion</span><span class="p">,</span>
            <span class="n">displayName</span><span class="p">:</span> <span class="s">"REST API Client Code Generator (PREVIEW)"</span><span class="p">,</span>
            <span class="n">description</span><span class="p">:</span> <span class="s">"Generate REST API client code from OpenAPI/Swagger specifications"</span><span class="p">)</span>
    <span class="p">};</span>

    <span class="p">[</span><span class="n">VisualStudioContribution</span><span class="p">]</span>
    <span class="k">public</span> <span class="k">static</span> <span class="n">MenuConfiguration</span> <span class="n">GenerateMenu</span> <span class="p">=&gt;</span> <span class="k">new</span><span class="p">(</span><span class="s">"%ApiClientCodeGenerator.GroupDisplayName%"</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="n">Placements</span> <span class="p">=</span> <span class="p">[</span><span class="n">KnownPlacements</span><span class="p">.</span><span class="n">ItemNode</span><span class="p">],</span>
        <span class="n">Children</span> <span class="p">=</span> <span class="p">[</span>
            <span class="n">MenuChild</span><span class="p">.</span><span class="n">Command</span><span class="p">&lt;</span><span class="n">Commands</span><span class="p">.</span><span class="n">GenerateRefitterCommand</span><span class="p">&gt;(),</span>
            <span class="n">MenuChild</span><span class="p">.</span><span class="n">Command</span><span class="p">&lt;</span><span class="n">Commands</span><span class="p">.</span><span class="n">GenerateNSwagCommand</span><span class="p">&gt;(),</span>
            <span class="c1">// ...</span>
        <span class="p">],</span>
    <span class="p">};</span>

    <span class="k">protected</span> <span class="k">override</span> <span class="k">void</span> <span class="nf">InitializeServices</span><span class="p">(</span><span class="n">IServiceCollection</span> <span class="n">serviceCollection</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="n">serviceCollection</span><span class="p">.</span><span class="n">AddSingleton</span><span class="p">&lt;</span><span class="n">ExtensionSettingsProvider</span><span class="p">&gt;();</span>
        <span class="k">base</span><span class="p">.</span><span class="nf">InitializeServices</span><span class="p">(</span><span class="n">serviceCollection</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Notice how we can register services like <code class="language-plaintext highlighter-rouge">ExtensionSettingsProvider</code> using standard .NET dependency injection patterns (<code class="language-plaintext highlighter-rouge">InitializeServices</code>).</p>

<h2 id="modernizing-commands-and-async-execution">Modernizing Commands and Async Execution</h2>

<p>One of the biggest improvements in the new model is that everything is <strong>async by default</strong>. In the old SDK, you had to carefully manage thread switching to avoid “UI delays” or deadlocks. The original version of the extension uses single file generators as a custom tool. A custom tool is a COM component that implements the <a href="https://learn.microsoft.com/en-us/dotnet/api/microsoft.visualstudio.shell.interop.ivssinglefilegenerator?view=visualstudiosdk-2022&amp;WT.mc_id=DT-MVP-5004822"><code class="language-plaintext highlighter-rouge">IVsSingleFileGenerator</code></a> interface. <a href="https://learn.microsoft.com/en-us/visualstudio/extensibility/internals/implementing-single-file-generators?view=visualstudio?WT.mc_id=DT-MVP-5004822">Implementing single-file generators</a> in 2026 feels really outdated.</p>

<p><strong>Old Command Implementation - <a href="https://github.com/christianhelle/apiclientcodegen/blob/master/src/VSIX/ApiClientCodeGen.VSIX.Shared/CustomTool/SingleFileCodeGenerator.cs"><code class="language-plaintext highlighter-rouge">SingleFileCodeGenerator.cs</code></a>:</strong></p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="nf">ComVisible</span><span class="p">(</span><span class="k">true</span><span class="p">)]</span>
<span class="k">public</span> <span class="k">abstract</span> <span class="k">class</span> <span class="nc">SingleFileCodeGenerator</span> <span class="p">:</span> <span class="n">IVsSingleFileGenerator</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="kt">int</span> <span class="nf">Generate</span><span class="p">(</span>
        <span class="kt">string</span> <span class="n">wszInputFilePath</span><span class="p">,</span>
        <span class="kt">string</span> <span class="n">bstrInputFileContents</span><span class="p">,</span>
        <span class="kt">string</span> <span class="n">wszDefaultNamespace</span><span class="p">,</span>
        <span class="n">IntPtr</span><span class="p">[]</span> <span class="n">rgbOutputFileContents</span><span class="p">,</span>
        <span class="k">out</span> <span class="kt">uint</span> <span class="n">pcbOutput</span><span class="p">,</span>
        <span class="n">IVsGeneratorProgress</span> <span class="n">pGenerateProgress</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="c1">// Strict thread affinity check required</span>
        <span class="k">if</span> <span class="p">(!</span><span class="n">TestingUtility</span><span class="p">.</span><span class="n">IsRunningFromUnitTest</span><span class="p">)</span>
            <span class="n">ThreadHelper</span><span class="p">.</span><span class="nf">ThrowIfNotOnUIThread</span><span class="p">();</span>

        <span class="c1">// Blocking generation on the UI thread</span>
        <span class="kt">var</span> <span class="n">codeGenerator</span> <span class="p">=</span> <span class="n">Factory</span><span class="p">.</span><span class="nf">Create</span><span class="p">(...);</span>
        <span class="kt">var</span> <span class="n">code</span> <span class="p">=</span> <span class="n">codeGenerator</span><span class="p">.</span><span class="nf">GenerateCode</span><span class="p">();</span>

        <span class="c1">// Report progress</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>In the new model, we simply implement <code class="language-plaintext highlighter-rouge">ExecuteCommandAsync</code> and use the <code class="language-plaintext highlighter-rouge">IClientContext</code> to interact with the IDE.</p>

<p><strong>New Command Implementation (<a href="https://github.com/christianhelle/apiclientcodegen/blob/master/src/VSIX/ApiClientCodeGen.VSIX.Shared/Commands/Refitter/RefitterCommand.cs"><code class="language-plaintext highlighter-rouge">RefitterCommands.cs</code></a>):</strong></p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="n">VisualStudioContribution</span><span class="p">]</span>
<span class="k">public</span> <span class="k">class</span> <span class="nc">GenerateRefitterCommand</span><span class="p">(</span><span class="n">TraceSource</span> <span class="n">traceSource</span><span class="p">,</span> <span class="n">ExtensionSettingsProvider</span> <span class="n">settingsProvider</span><span class="p">)</span>
    <span class="p">:</span> <span class="nf">GenerateRefitterBaseCommand</span><span class="p">(</span><span class="n">traceSource</span><span class="p">,</span> <span class="n">settingsProvider</span><span class="p">)</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="k">override</span> <span class="k">async</span> <span class="n">Task</span> <span class="nf">ExecuteCommandAsync</span><span class="p">(</span>
        <span class="n">IClientContext</span> <span class="n">context</span><span class="p">,</span>
        <span class="n">CancellationToken</span> <span class="n">cancellationToken</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="c1">// Fully async execution without UI thread blocking</span>
        <span class="k">await</span> <span class="nf">GenerateCodeAsync</span><span class="p">(</span>
            <span class="k">await</span> <span class="n">context</span><span class="p">.</span><span class="nf">GetInputFileAsync</span><span class="p">(</span><span class="n">cancellationToken</span><span class="p">),</span>
            <span class="k">await</span> <span class="n">context</span><span class="p">.</span><span class="nf">GetDefaultNamespaceAsync</span><span class="p">(</span><span class="n">cancellationToken</span><span class="p">),</span>
            <span class="n">cancellationToken</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="background-progress-reporting">Background Progress Reporting</h3>

<p>Since our code generation can take a few seconds (or more for large specs), providing feedback is essential. The new SDK makes it incredibly easy to show a progress indicator in the Visual Studio status bar or a background task window.</p>

<p><img src="/assets/images/rapicgen-background-task-progress.png" alt="Background Task Progress" /></p>

<p>Here’s how easy it is to implement:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">async</span> <span class="n">Task</span> <span class="nf">GenerateCodeAsync</span><span class="p">(...)</span>
<span class="p">{</span>
    <span class="k">using</span> <span class="nn">var</span> <span class="n">progress</span> <span class="p">=</span> <span class="k">await</span> <span class="n">Extensibility</span><span class="p">.</span><span class="nf">Shell</span><span class="p">().</span><span class="nf">StartProgressReportingAsync</span><span class="p">(</span>
        <span class="s">"Generating code with Refitter"</span><span class="p">,</span>
        <span class="k">new</span> <span class="nf">ProgressReporterOptions</span><span class="p">(</span><span class="k">true</span><span class="p">),</span> <span class="c1">// true = indeterminate/cancellable</span>
        <span class="n">cancellationToken</span><span class="p">);</span>

    <span class="n">progress</span><span class="p">.</span><span class="nf">Report</span><span class="p">(</span><span class="m">10</span><span class="p">,</span> <span class="s">$"Starting Refitter code generation for: </span><span class="p">{</span><span class="n">inputFilename</span><span class="p">}</span><span class="s">"</span><span class="p">);</span>

    <span class="c1">// Do the heavy lifting...</span>
    <span class="kt">var</span> <span class="n">csharpCode</span> <span class="p">=</span> <span class="k">await</span> <span class="nf">GenerateCodeInternalAsync</span><span class="p">(...,</span> <span class="n">progress</span><span class="p">,</span> <span class="n">cancellationToken</span><span class="p">);</span>

    <span class="n">progress</span><span class="p">.</span><span class="nf">Report</span><span class="p">(</span><span class="m">90</span><span class="p">,</span> <span class="s">"Writing generated code..."</span><span class="p">);</span>
    <span class="k">await</span> <span class="n">File</span><span class="p">.</span><span class="nf">WriteAllTextAsync</span><span class="p">(</span><span class="n">outputFile</span><span class="p">,</span> <span class="n">csharpCode</span><span class="p">,</span> <span class="n">cancellationToken</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="a-new-settings-experience">A New Settings Experience</h2>

<p>The old Visual Studio SDK used the <code class="language-plaintext highlighter-rouge">DialogPage</code> class to create options pages, which often looked like standard Windows Forms property grids. The new model introduces a modern, declarative way to define settings.</p>

<p>Here’s an example of how settings are defined in the new model. We simply define a static class with properties decorated with the <code class="language-plaintext highlighter-rouge">[VisualStudioContribution]</code> attribute:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">internal</span> <span class="k">static</span> <span class="k">class</span> <span class="nc">GeneralSettings</span>
<span class="p">{</span>
    <span class="p">[</span><span class="n">VisualStudioContribution</span><span class="p">]</span>
    <span class="k">internal</span> <span class="k">static</span> <span class="n">SettingCategory</span> <span class="n">GeneralCategory</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="p">}</span> <span class="p">=</span> <span class="k">new</span><span class="p">(</span><span class="s">"general"</span><span class="p">,</span> <span class="s">"%Settings.General.DisplayName%"</span><span class="p">,</span> <span class="n">SettingsRoot</span><span class="p">.</span><span class="n">RootCategory</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="n">Description</span> <span class="p">=</span> <span class="s">"%Settings.General.Description%"</span><span class="p">,</span>
        <span class="n">GenerateObserverClass</span> <span class="p">=</span> <span class="k">true</span><span class="p">,</span>
        <span class="n">Order</span> <span class="p">=</span> <span class="m">0</span><span class="p">,</span>
    <span class="p">};</span>

    <span class="p">[</span><span class="n">VisualStudioContribution</span><span class="p">]</span>
    <span class="k">internal</span> <span class="k">static</span> <span class="n">Setting</span><span class="p">.</span><span class="n">String</span> <span class="n">JavaPath</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="p">}</span> <span class="p">=</span> <span class="k">new</span><span class="p">(</span>
        <span class="s">"javaPath"</span><span class="p">,</span>
        <span class="s">"%Settings.JavaPath.DisplayName%"</span><span class="p">,</span>
        <span class="n">GeneralCategory</span><span class="p">,</span>
        <span class="kt">string</span><span class="p">.</span><span class="n">Empty</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="n">Description</span> <span class="p">=</span> <span class="s">"%Settings.JavaPath.Description%"</span><span class="p">,</span>
    <span class="p">};</span>

    <span class="c1">// ... other settings</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Reading these settings is also straightforward and fully asynchronous:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">class</span> <span class="nc">ExtensionSettingsProvider</span><span class="p">(</span><span class="n">VisualStudioExtensibility</span> <span class="n">extensibility</span><span class="p">)</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="k">async</span> <span class="n">Task</span><span class="p">&lt;</span><span class="n">IGeneralOptions</span><span class="p">&gt;</span> <span class="nf">GetGeneralOptionsAsync</span><span class="p">(</span><span class="n">CancellationToken</span> <span class="n">cancellationToken</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="kt">var</span> <span class="n">values</span> <span class="p">=</span> <span class="k">await</span> <span class="n">extensibility</span><span class="p">.</span><span class="nf">Settings</span><span class="p">().</span><span class="nf">ReadEffectiveValuesAsync</span><span class="p">(</span>
        <span class="p">[</span>
            <span class="n">GeneralSettings</span><span class="p">.</span><span class="n">JavaPath</span><span class="p">,</span>
            <span class="n">GeneralSettings</span><span class="p">.</span><span class="n">NpmPath</span><span class="p">,</span>
            <span class="n">GeneralSettings</span><span class="p">.</span><span class="n">NSwagPath</span><span class="p">,</span>
            <span class="c1">// ...</span>
        <span class="p">],</span>
        <span class="n">cancellationToken</span><span class="p">);</span>

        <span class="k">return</span> <span class="k">new</span> <span class="nf">GeneralOptions</span><span class="p">(</span><span class="n">values</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>I’ve migrated all the tool-specific settings to this new API. Here is what the new settings UI looks like:</p>

<p><img src="/assets/images/rapicgen-vs-settings-overview.png" alt="Settings Overview" /></p>

<p>We can now define settings for each generator, such as NSwag, AutoRest, and Refitter, with rich UI support.</p>

<p><img src="/assets/images/rapicgen-vs-settings-nswag.png" alt="NSwag Settings" /></p>

<h2 id="dialogs-and-user-input">Dialogs and User Input</h2>

<p>Sometimes we need input from the user, like a URL for an OpenAPI specification. In the old days, I would have had to build a custom WPF Window or Windows Form. Now, I can use the built-in <code class="language-plaintext highlighter-rouge">ShowPromptAsync</code> method for a consistent, native look and feel.</p>

<p><img src="/assets/images/rapicgen-add-new-dialog-v2.png" alt="Add New OpenAPI File Dialog" /></p>

<p>This snippet from <a href="https://github.com/christianhelle/apiclientcodegen/blob/master/src/VSIX/ApiClientCodeGen.VSIX.Extensibility/CommandExtensions.cs"><code class="language-plaintext highlighter-rouge">CommandExtensions.cs</code></a> shows how we prompt for a URL:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">static</span> <span class="k">async</span> <span class="n">Task</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">?&gt;</span> <span class="nf">AddNewOpenApiFileAsync</span><span class="p">(</span>
    <span class="k">this</span> <span class="n">Command</span> <span class="n">command</span><span class="p">,</span>
    <span class="n">IClientContext</span> <span class="n">context</span><span class="p">,</span>
    <span class="n">CancellationToken</span> <span class="n">cancellationToken</span><span class="p">)</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">inputUrl</span> <span class="p">=</span> <span class="k">await</span> <span class="n">command</span><span class="p">.</span><span class="n">Extensibility</span><span class="p">.</span><span class="nf">Shell</span><span class="p">().</span><span class="nf">ShowPromptAsync</span><span class="p">(</span>
        <span class="s">"Enter URL to OpenAPI Specifications"</span><span class="p">,</span>
        <span class="k">new</span> <span class="n">InputPromptOptions</span>
        <span class="p">{</span>
            <span class="n">DefaultText</span> <span class="p">=</span> <span class="s">"Example: https://petstore3.swagger.io/api/v3/openapi.json"</span><span class="p">,</span>
            <span class="n">Icon</span> <span class="p">=</span> <span class="n">ImageMoniker</span><span class="p">.</span><span class="n">KnownValues</span><span class="p">.</span><span class="n">URLInputBox</span><span class="p">,</span>
            <span class="n">Title</span> <span class="p">=</span> <span class="s">"REST API Client Code Generator"</span><span class="p">,</span>
        <span class="p">},</span>
        <span class="n">cancellationToken</span><span class="p">);</span>

    <span class="k">return</span> <span class="n">inputUrl</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="creating-custom-dialogs-with-xaml">Creating Custom Dialogs with XAML</h3>

<p>For more complex scenarios, like the <strong>About Dialog</strong>, the new model allows us to define UI using standard XAML data templates. This is a huge win for maintainability, as we can separate the UI definition from the logic.</p>

<p><img src="/assets/images/rapicgen-about-dialog.png" alt="About Dialog" /></p>

<p>Here’s a snippet of the XAML definition for the About Dialog:</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;DataTemplate</span> <span class="na">xmlns=</span><span class="s">"http://schemas.microsoft.com/winfx/2006/xaml/presentation"</span>
              <span class="na">xmlns:x=</span><span class="s">"http://schemas.microsoft.com/winfx/2006/xaml"</span>
              <span class="na">xmlns:vs=</span><span class="s">"http://schemas.microsoft.com/visualstudio/extensibility/2022/xaml"</span><span class="nt">&gt;</span>
  <span class="nt">&lt;StackPanel</span> <span class="na">Orientation=</span><span class="s">"Vertical"</span> <span class="na">MinWidth=</span><span class="s">"500"</span> <span class="na">Margin=</span><span class="s">"20"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;TextBlock</span> <span class="na">FontSize=</span><span class="s">"18"</span> <span class="na">FontWeight=</span><span class="s">"Bold"</span> <span class="na">Margin=</span><span class="s">"0,0,0,10"</span>
               <span class="na">Text=</span><span class="s">"{Binding DisplayName}"</span> <span class="nt">/&gt;</span>

    <span class="nt">&lt;TextBlock</span> <span class="na">FontSize=</span><span class="s">"12"</span> <span class="na">Margin=</span><span class="s">"0,0,0,5"</span>
               <span class="na">Text=</span><span class="s">"{Binding Description}"</span> <span class="na">TextWrapping=</span><span class="s">"Wrap"</span> <span class="nt">/&gt;</span>

    <span class="nt">&lt;StackPanel</span> <span class="na">Orientation=</span><span class="s">"Horizontal"</span> <span class="na">Margin=</span><span class="s">"0,10,0,5"</span><span class="nt">&gt;</span>
      <span class="nt">&lt;TextBlock</span> <span class="na">FontWeight=</span><span class="s">"SemiBold"</span> <span class="na">Text=</span><span class="s">"Version: "</span> <span class="nt">/&gt;</span>
      <span class="nt">&lt;TextBlock</span> <span class="na">Text=</span><span class="s">"{Binding Version}"</span> <span class="nt">/&gt;</span>
    <span class="nt">&lt;/StackPanel&gt;</span>

    <span class="c">&lt;!-- ... more UI elements ... --&gt;</span>
  <span class="nt">&lt;/StackPanel&gt;</span>
<span class="nt">&lt;/DataTemplate&gt;</span>
</code></pre></div></div>

<p>And the backing C# model uses the <code class="language-plaintext highlighter-rouge">RemoteUserControl</code> base class:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">internal</span> <span class="k">class</span> <span class="nc">AboutDialog</span><span class="p">(</span>
    <span class="n">VisualStudioExtensibility</span> <span class="n">extensibility</span><span class="p">,</span>
    <span class="kt">string</span> <span class="n">displayName</span><span class="p">,</span>
    <span class="kt">string</span> <span class="n">description</span><span class="p">,</span>
    <span class="kt">string</span> <span class="n">version</span><span class="p">)</span>
    <span class="p">:</span> <span class="nf">RemoteUserControl</span><span class="p">(</span><span class="k">new</span> <span class="nf">AboutDialogData</span><span class="p">(</span><span class="n">extensibility</span><span class="p">,</span> <span class="n">displayName</span><span class="p">,</span> <span class="n">description</span><span class="p">,</span> <span class="n">version</span><span class="p">))</span>
<span class="p">{</span>
    <span class="p">[</span><span class="n">DataContract</span><span class="p">]</span>
    <span class="k">internal</span> <span class="k">class</span> <span class="nc">AboutDialogData</span> <span class="p">:</span> <span class="n">NotifyPropertyChangedObject</span>
    <span class="p">{</span>
        <span class="p">[</span><span class="n">DataMember</span><span class="p">]</span>
        <span class="k">public</span> <span class="kt">string</span> <span class="n">DisplayName</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span>

        <span class="p">[</span><span class="n">DataMember</span><span class="p">]</span>
        <span class="k">public</span> <span class="kt">string</span> <span class="n">Description</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span>

        <span class="p">[</span><span class="n">DataMember</span><span class="p">]</span>
        <span class="k">public</span> <span class="kt">string</span> <span class="n">Version</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span>

        <span class="c1">// ...</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This clean separation of concerns makes building custom UI in Visual Studio extensions much more pleasant.</p>

<h2 id="code-reuse-the-core-library">Code Reuse: The Core Library</h2>

<p>Despite the massive architectural shift in the VSIX project, the “brains” of the operation—the code generation logic—remain largely unchanged.</p>

<p>Both the old (Dev17) and new (Extensibility) projects reference the same <a href="https://github.com/christianhelle/apiclientcodegen/tree/master/src/Core/ApiClientCodeGen.Core"><strong>Core Library</strong></a>. This ensures that regardless of which version of the extension you use, the generated code quality and features remain identical. This made the migration much smoother, as I only had to focus on the “plumbing” of the extension rather than rewriting the generators themselves.</p>

<h2 id="conclusion">Conclusion</h2>

<p>Migrating to the new Visual Studio Extensibility model has been a significant step forward. The performance gains from .NET 8.0, the stability of the out-of-process model, and the improved developer experience with modern APIs make it well worth the effort.</p>

<p>If you’re a Visual Studio extension author, I highly recommend looking into this new model. And if you’re a user of the REST API Client Code Generator, I hope you enjoy the faster, more stable experience!</p>]]></content><author><name>Christian Helle</name></author><category term="Visual Studio" /><category term="REST" /><summary type="html"><![CDATA[I recently rebuilt the REST API Client Code Generator extension for Visual Studio from the ground up using the new Visual Studio.Extensibility model. This migration allowed the extension to run out-of-process and leverage the full power of .NET 8.0. In this post, I’ll walk through the architectural changes, the challenges of the old model, and how the new extensibility API simplifies modern extension development.]]></summary></entry><entry><title type="html">Building a fast line of code counter app in Zig</title><link href="https://christianhelle.com/2026/02/building-clocz-zig-line-counter.html" rel="alternate" type="text/html" title="Building a fast line of code counter app in Zig" /><published>2026-02-10T00:00:00+00:00</published><updated>2026-02-10T00:00:00+00:00</updated><id>https://christianhelle.com/2026/02/building-clocz-zig-line-counter</id><content type="html" xml:base="https://christianhelle.com/2026/02/building-clocz-zig-line-counter.html"><![CDATA[<p>I recently wrote a CLI tool for counting lines of code in <a href="https://ziglang.org/">Zig</a>. This was nothing more than a coding exercise and an experiment to see how fast a Zig compiled tool would be compared to the well-known tool <a href="https://github.com/AlDanial/cloc">cloc</a>, which was originally written in Perl.</p>

<p>The source code is available on GitHub at <a href="https://github.com/christianhelle/clocz">https://github.com/christianhelle/clocz</a>.</p>

<p>It only took me an evening to build, and GitHub Copilot wrote the GitHub workflows, README, install scripts, and the snapcraft.yaml file.</p>

<h2 id="how-it-works">How it works</h2>

<p>The implementation is quite straightforward. It uses <code class="language-plaintext highlighter-rouge">std.fs.Dir.iterate</code> to walk the directory tree. For each file found, a job is spawned in a <code class="language-plaintext highlighter-rouge">std.Thread.Pool</code>. This allows the tool to process multiple files in parallel, maximizing I/O and CPU usage.</p>

<div class="language-zig highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fn</span> <span class="n">walkDir</span><span class="p">(</span>
    <span class="n">allocator</span><span class="p">:</span> <span class="n">std</span><span class="p">.</span><span class="py">mem</span><span class="p">.</span><span class="py">Allocator</span><span class="p">,</span>
    <span class="n">dir</span><span class="p">:</span> <span class="n">std</span><span class="p">.</span><span class="py">fs</span><span class="p">.</span><span class="py">Dir</span><span class="p">,</span>
    <span class="n">dir_path</span><span class="p">:</span> <span class="p">[]</span><span class="k">const</span> <span class="kt">u8</span><span class="p">,</span>
    <span class="n">results</span><span class="p">:</span> <span class="o">*</span><span class="n">results_mod</span><span class="p">.</span><span class="py">Results</span><span class="p">,</span>
    <span class="n">pool</span><span class="p">:</span> <span class="o">*</span><span class="n">std</span><span class="p">.</span><span class="py">Thread</span><span class="p">.</span><span class="py">Pool</span><span class="p">,</span>
<span class="p">)</span> <span class="o">!</span><span class="k">void</span> <span class="p">{</span>
    <span class="k">var</span> <span class="n">iter</span> <span class="o">=</span> <span class="n">dir</span><span class="p">.</span><span class="nf">iterate</span><span class="p">();</span>
    <span class="k">while</span> <span class="p">(</span><span class="k">try</span> <span class="n">iter</span><span class="p">.</span><span class="nf">next</span><span class="p">())</span> <span class="p">|</span><span class="n">entry</span><span class="p">|</span> <span class="p">{</span>
        <span class="c">// ... (skip hidden files and node_modules)</span>

        <span class="k">const</span> <span class="n">entry_path</span> <span class="o">=</span> <span class="k">try</span> <span class="n">std</span><span class="p">.</span><span class="py">fs</span><span class="p">.</span><span class="py">path</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="n">allocator</span><span class="p">,</span> <span class="o">&amp;.</span><span class="p">{</span> <span class="n">dir_path</span><span class="p">,</span> <span class="n">entry</span><span class="p">.</span><span class="py">name</span> <span class="p">});</span>
        <span class="k">switch</span> <span class="p">(</span><span class="n">entry</span><span class="p">.</span><span class="py">kind</span><span class="p">)</span> <span class="p">{</span>
            <span class="p">.</span><span class="py">directory</span> <span class="o">=&gt;</span> <span class="p">{</span>
                <span class="c">// ... (recursive call)</span>
            <span class="p">},</span>
            <span class="p">.</span><span class="py">file</span> <span class="o">=&gt;</span> <span class="p">{</span>
                <span class="c">// Ownership of entry_path transfers to the job; the job frees it.</span>
                <span class="n">pool</span><span class="p">.</span><span class="nf">spawn</span><span class="p">(</span><span class="n">processFile</span><span class="p">,</span> <span class="o">.</span><span class="p">{</span><span class="n">JobContext</span><span class="p">{</span>
                    <span class="p">.</span><span class="py">allocator</span> <span class="o">=</span> <span class="n">allocator</span><span class="p">,</span>
                    <span class="p">.</span><span class="py">path</span> <span class="o">=</span> <span class="n">entry_path</span><span class="p">,</span>
                    <span class="p">.</span><span class="py">results</span> <span class="o">=</span> <span class="n">results</span><span class="p">,</span>
                <span class="p">}})</span> <span class="k">catch</span> <span class="p">{</span>
                    <span class="n">allocator</span><span class="p">.</span><span class="nf">free</span><span class="p">(</span><span class="n">entry_path</span><span class="p">);</span>
                <span class="p">};</span>
            <span class="p">},</span>
            <span class="k">else</span> <span class="o">=&gt;</span> <span class="n">allocator</span><span class="p">.</span><span class="nf">free</span><span class="p">(</span><span class="n">entry_path</span><span class="p">),</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>I added some basic filtering to skip hidden files (starting with <code class="language-plaintext highlighter-rouge">.</code>) and directories like <code class="language-plaintext highlighter-rouge">node_modules</code> and <code class="language-plaintext highlighter-rouge">vendor</code>, as these usually contain dependencies rather than source code. There’s also a file size limit of 128MB to avoid loading massive files into memory.</p>

<p>For language detection, I went with a simple extension-based lookup. The file extension is extracted, lower cased, and matched against a list of known languages. It supports over 60 languages, handling single-line and block comments specific to each language.</p>

<div class="language-zig highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">const</span> <span class="n">entries</span> <span class="o">=</span> <span class="p">[</span><span class="mi">_</span><span class="p">]</span><span class="n">Entry</span><span class="p">{</span>
    <span class="c">// C family</span>
    <span class="o">.</span><span class="p">{</span> <span class="p">.</span><span class="py">ext</span> <span class="o">=</span> <span class="s">"c"</span><span class="p">,</span>        <span class="p">.</span><span class="py">lang</span> <span class="o">=</span> <span class="n">cStyle</span><span class="p">(</span><span class="s">"C"</span><span class="p">)</span> <span class="p">},</span>
    <span class="o">.</span><span class="p">{</span> <span class="p">.</span><span class="py">ext</span> <span class="o">=</span> <span class="s">"h"</span><span class="p">,</span>        <span class="p">.</span><span class="py">lang</span> <span class="o">=</span> <span class="n">cStyle</span><span class="p">(</span><span class="s">"C"</span><span class="p">)</span> <span class="p">},</span>
    <span class="o">.</span><span class="p">{</span> <span class="p">.</span><span class="py">ext</span> <span class="o">=</span> <span class="s">"cpp"</span><span class="p">,</span>      <span class="p">.</span><span class="py">lang</span> <span class="o">=</span> <span class="n">cStyle</span><span class="p">(</span><span class="s">"C++"</span><span class="p">)</span> <span class="p">},</span>
    <span class="o">.</span><span class="p">{</span> <span class="p">.</span><span class="py">ext</span> <span class="o">=</span> <span class="s">"cc"</span><span class="p">,</span>       <span class="p">.</span><span class="py">lang</span> <span class="o">=</span> <span class="n">cStyle</span><span class="p">(</span><span class="s">"C++"</span><span class="p">)</span> <span class="p">},</span>
    <span class="o">.</span><span class="p">{</span> <span class="p">.</span><span class="py">ext</span> <span class="o">=</span> <span class="s">"cxx"</span><span class="p">,</span>      <span class="p">.</span><span class="py">lang</span> <span class="o">=</span> <span class="n">cStyle</span><span class="p">(</span><span class="s">"C++"</span><span class="p">)</span> <span class="p">},</span>
    <span class="o">.</span><span class="p">{</span> <span class="p">.</span><span class="py">ext</span> <span class="o">=</span> <span class="s">"hpp"</span><span class="p">,</span>      <span class="p">.</span><span class="py">lang</span> <span class="o">=</span> <span class="n">cStyle</span><span class="p">(</span><span class="s">"C++"</span><span class="p">)</span> <span class="p">},</span>
    <span class="o">.</span><span class="p">{</span> <span class="p">.</span><span class="py">ext</span> <span class="o">=</span> <span class="s">"hxx"</span><span class="p">,</span>      <span class="p">.</span><span class="py">lang</span> <span class="o">=</span> <span class="n">cStyle</span><span class="p">(</span><span class="s">"C++"</span><span class="p">)</span> <span class="p">},</span>
    <span class="o">.</span><span class="p">{</span> <span class="p">.</span><span class="py">ext</span> <span class="o">=</span> <span class="s">"cs"</span><span class="p">,</span>       <span class="p">.</span><span class="py">lang</span> <span class="o">=</span> <span class="n">cStyle</span><span class="p">(</span><span class="s">"C#"</span><span class="p">)</span> <span class="p">},</span>
    <span class="o">.</span><span class="p">{</span> <span class="p">.</span><span class="py">ext</span> <span class="o">=</span> <span class="s">"java"</span><span class="p">,</span>     <span class="p">.</span><span class="py">lang</span> <span class="o">=</span> <span class="n">cStyle</span><span class="p">(</span><span class="s">"Java"</span><span class="p">)</span> <span class="p">},</span>
    <span class="c">// and more...</span>

<span class="k">pub</span> <span class="k">fn</span> <span class="n">detect</span><span class="p">(</span><span class="n">path</span><span class="p">:</span> <span class="p">[]</span><span class="k">const</span> <span class="kt">u8</span><span class="p">)</span> <span class="o">?</span><span class="n">Language</span> <span class="p">{</span>
    <span class="k">const</span> <span class="n">ext</span> <span class="o">=</span> <span class="n">std</span><span class="p">.</span><span class="py">fs</span><span class="p">.</span><span class="py">path</span><span class="p">.</span><span class="nf">extension</span><span class="p">(</span><span class="n">path</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">ext</span><span class="p">.</span><span class="py">len</span> <span class="o">&lt;=</span> <span class="mi">1</span><span class="p">)</span> <span class="k">return</span> <span class="kc">null</span><span class="p">;</span>
    <span class="k">const</span> <span class="n">ext_no_dot</span> <span class="o">=</span> <span class="n">ext</span><span class="p">[</span><span class="mi">1</span><span class="o">..</span><span class="p">];</span>

    <span class="c">// Lowercase into a small stack buffer (extensions are short)</span>
    <span class="k">var</span> <span class="n">buf</span><span class="p">:</span> <span class="p">[</span><span class="mi">32</span><span class="p">]</span><span class="kt">u8</span> <span class="o">=</span> <span class="k">undefined</span><span class="p">;</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">ext_no_dot</span><span class="p">.</span><span class="py">len</span> <span class="o">&gt;</span> <span class="n">buf</span><span class="p">.</span><span class="py">len</span><span class="p">)</span> <span class="k">return</span> <span class="kc">null</span><span class="p">;</span>
    <span class="k">const</span> <span class="n">ext_lower</span> <span class="o">=</span> <span class="n">std</span><span class="p">.</span><span class="py">ascii</span><span class="p">.</span><span class="nf">lowerString</span><span class="p">(</span><span class="n">buf</span><span class="p">[</span><span class="mi">0</span><span class="o">..</span><span class="n">ext_no_dot</span><span class="p">.</span><span class="py">len</span><span class="p">],</span> <span class="n">ext_no_dot</span><span class="p">);</span>

    <span class="k">for</span> <span class="p">(</span><span class="n">entries</span><span class="p">)</span> <span class="p">|</span><span class="n">entry</span><span class="p">|</span> <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">std</span><span class="p">.</span><span class="py">mem</span><span class="p">.</span><span class="nf">eql</span><span class="p">(</span><span class="kt">u8</span><span class="p">,</span> <span class="n">entry</span><span class="p">.</span><span class="py">ext</span><span class="p">,</span> <span class="n">ext_lower</span><span class="p">))</span> <span class="k">return</span> <span class="n">entry</span><span class="p">.</span><span class="py">lang</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="kc">null</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The actual line counting logic is a single-pass scan over the file content. It counts blank lines, single-line comments, block comments, and code lines. It uses a state machine-like approach to track if it’s inside a block comment, which handles nested comments and comments starting mid-line.</p>

<div class="language-zig highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">pub</span> <span class="k">const</span> <span class="n">Counts</span> <span class="o">=</span> <span class="k">struct</span> <span class="p">{</span>
    <span class="n">files</span><span class="p">:</span> <span class="kt">u64</span> <span class="o">=</span> <span class="mi">0</span><span class="p">,</span>
    <span class="n">blank</span><span class="p">:</span> <span class="kt">u64</span> <span class="o">=</span> <span class="mi">0</span><span class="p">,</span>
    <span class="n">comment</span><span class="p">:</span> <span class="kt">u64</span> <span class="o">=</span> <span class="mi">0</span><span class="p">,</span>
    <span class="n">code</span><span class="p">:</span> <span class="kt">u64</span> <span class="o">=</span> <span class="mi">0</span><span class="p">,</span>
<span class="p">};</span>

<span class="k">pub</span> <span class="k">fn</span> <span class="n">countLines</span><span class="p">(</span><span class="n">buf</span><span class="p">:</span> <span class="p">[]</span><span class="k">const</span> <span class="kt">u8</span><span class="p">,</span> <span class="n">lang</span><span class="p">:</span> <span class="n">languages</span><span class="p">.</span><span class="py">Language</span><span class="p">)</span> <span class="n">Counts</span> <span class="p">{</span>
    <span class="k">var</span> <span class="n">counts</span> <span class="o">=</span> <span class="n">Counts</span><span class="p">{</span> <span class="p">.</span><span class="py">files</span> <span class="o">=</span> <span class="mi">1</span> <span class="p">};</span>
    <span class="k">var</span> <span class="n">in_block</span> <span class="o">=</span> <span class="kc">false</span><span class="p">;</span>

    <span class="c">// Strip a trailing newline so we don't count an extra blank line for it.</span>
    <span class="k">const</span> <span class="n">data</span> <span class="o">=</span> <span class="k">if</span> <span class="p">(</span><span class="n">buf</span><span class="p">.</span><span class="py">len</span> <span class="o">&gt;</span> <span class="mi">0</span> <span class="k">and</span> <span class="n">buf</span><span class="p">[</span><span class="n">buf</span><span class="p">.</span><span class="py">len</span> <span class="o">-</span> <span class="mi">1</span><span class="p">]</span> <span class="o">==</span> <span class="sc">'\n'</span><span class="p">)</span> <span class="n">buf</span><span class="p">[</span><span class="mi">0</span> <span class="o">..</span> <span class="n">buf</span><span class="p">.</span><span class="py">len</span> <span class="o">-</span> <span class="mi">1</span><span class="p">]</span> <span class="k">else</span> <span class="n">buf</span><span class="p">;</span>

    <span class="k">var</span> <span class="n">lines</span> <span class="o">=</span> <span class="n">std</span><span class="p">.</span><span class="py">mem</span><span class="p">.</span><span class="nf">splitScalar</span><span class="p">(</span><span class="kt">u8</span><span class="p">,</span> <span class="n">data</span><span class="p">,</span> <span class="sc">'\n'</span><span class="p">);</span>
    <span class="k">while</span> <span class="p">(</span><span class="n">lines</span><span class="p">.</span><span class="nf">next</span><span class="p">())</span> <span class="p">|</span><span class="n">raw_line</span><span class="p">|</span> <span class="p">{</span>
        <span class="c">// Trim \r for Windows line endings and leading/trailing whitespace.</span>
        <span class="k">const</span> <span class="n">line</span> <span class="o">=</span> <span class="n">std</span><span class="p">.</span><span class="py">mem</span><span class="p">.</span><span class="nf">trim</span><span class="p">(</span><span class="kt">u8</span><span class="p">,</span> <span class="n">raw_line</span><span class="p">,</span> <span class="s">" </span><span class="se">\t\r</span><span class="s">"</span><span class="p">);</span>

        <span class="k">if</span> <span class="p">(</span><span class="n">line</span><span class="p">.</span><span class="py">len</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
            <span class="n">counts</span><span class="p">.</span><span class="py">blank</span> <span class="o">+=</span> <span class="mi">1</span><span class="p">;</span>
            <span class="k">continue</span><span class="p">;</span>
        <span class="p">}</span>

        <span class="c">// Inside a block comment.</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">in_block</span><span class="p">)</span> <span class="p">{</span>
            <span class="n">counts</span><span class="p">.</span><span class="py">comment</span> <span class="o">+=</span> <span class="mi">1</span><span class="p">;</span>
            <span class="k">if</span> <span class="p">(</span><span class="n">lang</span><span class="p">.</span><span class="py">block_comment_end</span><span class="p">)</span> <span class="p">|</span><span class="n">bce</span><span class="p">|</span> <span class="p">{</span>
                <span class="k">if</span> <span class="p">(</span><span class="n">std</span><span class="p">.</span><span class="py">mem</span><span class="p">.</span><span class="nf">indexOf</span><span class="p">(</span><span class="kt">u8</span><span class="p">,</span> <span class="n">line</span><span class="p">,</span> <span class="n">bce</span><span class="p">)</span> <span class="o">!=</span> <span class="kc">null</span><span class="p">)</span> <span class="p">{</span>
                    <span class="n">in_block</span> <span class="o">=</span> <span class="kc">false</span><span class="p">;</span>
                <span class="p">}</span>
            <span class="p">}</span>
            <span class="k">continue</span><span class="p">;</span>
        <span class="p">}</span>

        <span class="c">// ... (check for start of block comment or single line comment)</span>

        <span class="c">// It's a code line.</span>
        <span class="n">counts</span><span class="p">.</span><span class="py">code</span> <span class="o">+=</span> <span class="mi">1</span><span class="p">;</span>

        <span class="c">// Check whether a block comment opens mid-line (e.g. `x = 1; /* start`).</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">lang</span><span class="p">.</span><span class="py">block_comment_start</span><span class="p">)</span> <span class="p">|</span><span class="n">bcs</span><span class="p">|</span> <span class="p">{</span>
            <span class="k">if</span> <span class="p">(</span><span class="n">std</span><span class="p">.</span><span class="py">mem</span><span class="p">.</span><span class="nf">indexOf</span><span class="p">(</span><span class="kt">u8</span><span class="p">,</span> <span class="n">line</span><span class="p">,</span> <span class="n">bcs</span><span class="p">))</span> <span class="p">|</span><span class="n">bcs_pos</span><span class="p">|</span> <span class="p">{</span>
                <span class="c">// ... (check if block comment ends on same line)</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="k">return</span> <span class="n">counts</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="command-line-parsing">Command Line Parsing</h2>

<p>Currently the tool only really a single functional argument, which is the folder to scan. I manually iterate through the arguments provided by <code class="language-plaintext highlighter-rouge">std.process.argsAlloc</code>. The implementation checks for flags like <code class="language-plaintext highlighter-rouge">-h</code> or <code class="language-plaintext highlighter-rouge">--help</code> and interprets any non-flag argument as the target directory path. If an unknown option is encountered, it helpfully prints the usage information and exits.</p>

<div class="language-zig highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">pub</span> <span class="k">fn</span> <span class="n">init</span><span class="p">(</span><span class="n">allocator</span><span class="p">:</span> <span class="n">std</span><span class="p">.</span><span class="py">mem</span><span class="p">.</span><span class="py">Allocator</span><span class="p">)</span> <span class="o">!</span><span class="n">Cli</span> <span class="p">{</span>
    <span class="k">const</span> <span class="n">args</span> <span class="o">=</span> <span class="k">try</span> <span class="n">std</span><span class="p">.</span><span class="py">process</span><span class="p">.</span><span class="nf">argsAlloc</span><span class="p">(</span><span class="n">allocator</span><span class="p">);</span>
    <span class="k">defer</span> <span class="n">std</span><span class="p">.</span><span class="py">process</span><span class="p">.</span><span class="nf">argsFree</span><span class="p">(</span><span class="n">allocator</span><span class="p">,</span> <span class="n">args</span><span class="p">);</span>

    <span class="c">// ...</span>

    <span class="k">var</span> <span class="n">path</span><span class="p">:</span> <span class="p">[]</span><span class="k">const</span> <span class="kt">u8</span> <span class="o">=</span> <span class="s">"."</span><span class="p">;</span>
    <span class="k">for</span> <span class="p">(</span><span class="n">args</span><span class="p">[</span><span class="mi">1</span><span class="o">..</span><span class="p">])</span> <span class="p">|</span><span class="n">arg</span><span class="p">|</span> <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">std</span><span class="p">.</span><span class="py">mem</span><span class="p">.</span><span class="nf">startsWith</span><span class="p">(</span><span class="kt">u8</span><span class="p">,</span> <span class="n">arg</span><span class="p">,</span> <span class="s">"-h"</span><span class="p">)</span> <span class="k">or</span> <span class="n">std</span><span class="p">.</span><span class="py">mem</span><span class="p">.</span><span class="nf">startsWith</span><span class="p">(</span><span class="kt">u8</span><span class="p">,</span> <span class="n">arg</span><span class="p">,</span> <span class="s">"--help"</span><span class="p">))</span> <span class="p">{</span>
            <span class="n">options</span><span class="p">.</span><span class="py">help</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span>
        <span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">std</span><span class="p">.</span><span class="py">mem</span><span class="p">.</span><span class="nf">startsWith</span><span class="p">(</span><span class="kt">u8</span><span class="p">,</span> <span class="n">arg</span><span class="p">,</span> <span class="s">"-v"</span><span class="p">)</span> <span class="k">or</span> <span class="n">std</span><span class="p">.</span><span class="py">mem</span><span class="p">.</span><span class="nf">startsWith</span><span class="p">(</span><span class="kt">u8</span><span class="p">,</span> <span class="n">arg</span><span class="p">,</span> <span class="s">"--version"</span><span class="p">))</span> <span class="p">{</span>
            <span class="n">options</span><span class="p">.</span><span class="py">version</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span>
        <span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">arg</span><span class="p">.</span><span class="py">len</span> <span class="o">&gt;</span> <span class="mi">0</span> <span class="k">and</span> <span class="n">arg</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">!=</span> <span class="sc">'-'</span><span class="p">)</span> <span class="p">{</span>
            <span class="n">path</span> <span class="o">=</span> <span class="n">arg</span><span class="p">;</span>
        <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
            <span class="c">// ... (print error and exit)</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="c">// ...</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="performance-and-progress">Performance and Progress</h2>

<p>One of the goals was to see how fast Zig can be compared to the original Perl version. The tool reads files into memory buffers using <code class="language-plaintext highlighter-rouge">readToEndAlloc</code> (up to 128MB) and quickly scans for line breaks and comments. It also detects binary files to avoid counting them as source code.</p>

<div class="language-zig highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">/// Returns true if the buffer looks like a binary file (null byte in first 8KB).</span>
<span class="k">pub</span> <span class="k">fn</span> <span class="n">isBinary</span><span class="p">(</span><span class="n">buf</span><span class="p">:</span> <span class="p">[]</span><span class="k">const</span> <span class="kt">u8</span><span class="p">)</span> <span class="k">bool</span> <span class="p">{</span>
    <span class="k">const</span> <span class="n">check</span> <span class="o">=</span> <span class="n">@min</span><span class="p">(</span><span class="n">buf</span><span class="p">.</span><span class="py">len</span><span class="p">,</span> <span class="mi">8192</span><span class="p">);</span>
    <span class="k">return</span> <span class="n">std</span><span class="p">.</span><span class="py">mem</span><span class="p">.</span><span class="nf">indexOfScalar</span><span class="p">(</span><span class="kt">u8</span><span class="p">,</span> <span class="n">buf</span><span class="p">[</span><span class="mi">0</span><span class="o">..</span><span class="n">check</span><span class="p">],</span> <span class="mi">0</span><span class="p">)</span> <span class="o">!=</span> <span class="kc">null</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>While the main thread and worker threads are busy scanning, a separate thread runs a simple progress loop. It sleeps for 100ms and prints the current count of scanned files (<code class="language-plaintext highlighter-rouge">\rScanning... {d} files</code>), giving visual feedback without slowing down the processing.</p>

<div class="language-zig highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">pub</span> <span class="k">fn</span> <span class="n">loop</span><span class="p">(</span><span class="n">self</span><span class="p">:</span> <span class="o">*</span><span class="n">ProgressPrinter</span><span class="p">)</span> <span class="k">void</span> <span class="p">{</span>
    <span class="k">const</span> <span class="n">stderr</span> <span class="o">=</span> <span class="n">std</span><span class="p">.</span><span class="py">fs</span><span class="p">.</span><span class="py">File</span><span class="p">.</span><span class="nf">stderr</span><span class="p">();</span>
    <span class="k">while</span> <span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="py">running</span><span class="p">.</span><span class="nf">load</span><span class="p">(.</span><span class="py">acquire</span><span class="p">))</span> <span class="p">{</span>
        <span class="n">std</span><span class="p">.</span><span class="py">Thread</span><span class="p">.</span><span class="nf">sleep</span><span class="p">(</span><span class="mi">100</span> <span class="o">*</span> <span class="n">std</span><span class="p">.</span><span class="py">time</span><span class="p">.</span><span class="py">ns_per_ms</span><span class="p">);</span>
        <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">self</span><span class="p">.</span><span class="py">running</span><span class="p">.</span><span class="nf">load</span><span class="p">(.</span><span class="py">acquire</span><span class="p">))</span> <span class="k">break</span><span class="p">;</span>
        <span class="k">const</span> <span class="n">n</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="py">results</span><span class="p">.</span><span class="py">files_scanned</span><span class="p">.</span><span class="nf">load</span><span class="p">(.</span><span class="py">monotonic</span><span class="p">);</span>
        <span class="k">var</span> <span class="n">buf</span><span class="p">:</span> <span class="p">[</span><span class="mi">64</span><span class="p">]</span><span class="kt">u8</span> <span class="o">=</span> <span class="k">undefined</span><span class="p">;</span>
        <span class="k">const</span> <span class="n">msg</span> <span class="o">=</span> <span class="n">std</span><span class="p">.</span><span class="py">fmt</span><span class="p">.</span><span class="nf">bufPrint</span><span class="p">(</span><span class="o">&amp;</span><span class="n">buf</span><span class="p">,</span> <span class="s">"</span><span class="se">\r</span><span class="s">Scanning... {d} files"</span><span class="p">,</span> <span class="o">.</span><span class="p">{</span><span class="n">n</span><span class="p">})</span> <span class="k">catch</span> <span class="k">break</span><span class="p">;</span>
        <span class="n">stderr</span><span class="p">.</span><span class="nf">writeAll</span><span class="p">(</span><span class="n">msg</span><span class="p">)</span> <span class="k">catch</span> <span class="p">{};</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The final output is a sorted table showing the number of files, blank lines, comments, and code lines for each language, along with the total time taken and files processed per second.</p>

<h2 id="usage">Usage</h2>

<p>The CLI is simple. You run <code class="language-plaintext highlighter-rouge">clocz</code> to scan the current directory, or <code class="language-plaintext highlighter-rouge">clocz [path]</code> to scan a specific directory. The <code class="language-plaintext highlighter-rouge">-h</code> and <code class="language-plaintext highlighter-rouge">-v</code> flags show help and version info respectively.</p>

<p>Here’s an example of the output for scanning a reasonably large system with a backend written in C# and a React frontend, running on a 10-year-old laptop with an Intel Core i7-7660U @ 4x 4GHz CPU with 8GB of RAM:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Scanned 10822 files

------------------------------------------------------------------------
Language                          files    blank    comment       code
------------------------------------------------------------------------
C#                                 4045    26273      19500     170743
TSX                                 720     5119       1248      58860
TypeScript                          726     4123       2585      43640
Markdown                             59     1347          0       4480
CSS                                   9      370         52       2889
PowerShell                           40      460        305       2046
HTML                                  4       32         12       1619
JavaScript                            4      193        167       1034
Shell                                 4       22         14        108
XML                                   1        0          0          9
------------------------------------------------------------------------
SUM:                               5612    37939      23883     285428
------------------------------------------------------------------------
Time=0.50s  (11203.0 files/s)
</code></pre></div></div>

<p>The tool is a single static binary with zero external dependencies, making it easy to distribute and run on Linux, macOS, and Windows.</p>

<h2 id="distribution">Distribution</h2>

<p>Since I’m lazy and wanted this to be easy to use, I asked GitHub Copilot to help me set up the distribution channels. It generated the <code class="language-plaintext highlighter-rouge">install.sh</code> and <code class="language-plaintext highlighter-rouge">install.ps1</code> scripts for quick installation on Linux/macOS and Windows respectively using the latest Release version on Github.</p>

<p>It also wrote the <code class="language-plaintext highlighter-rouge">snapcraft.yaml</code> file so I could publish <code class="language-plaintext highlighter-rouge">clocz</code> to the Snap Store.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">name</span><span class="pi">:</span> <span class="s">clocz</span>
<span class="na">base</span><span class="pi">:</span> <span class="s">core22</span>
<span class="na">version</span><span class="pi">:</span> <span class="s1">'</span><span class="s">0.1.0'</span>
<span class="na">summary</span><span class="pi">:</span> <span class="s">A fast line counter written in Zig</span>
<span class="na">description</span><span class="pi">:</span> <span class="pi">|</span>
  <span class="s">A fast, multi-threaded command-line tool for counting lines of code.</span>
<span class="na">grade</span><span class="pi">:</span> <span class="s">stable</span>
<span class="na">confinement</span><span class="pi">:</span> <span class="s">strict</span>
</code></pre></div></div>

<p>The GitHub Actions workflow (<code class="language-plaintext highlighter-rouge">release.yml</code>) builds binaries for Linux (x86_64, aarch64), macOS (x86_64, aarch64), and Windows (x86_64) and attaches them to the GitHub Release.</p>

<h2 id="conclusion">Conclusion</h2>

<p>I was pleasantly surprised by how productive I could be with Zig after working with it on-and-off for half a year, building a performant and cross-platform tool in such a short amount of time. If you want to check it out or contribute, the source code is on GitHub at <a href="https://github.com/christianhelle/clocz">https://github.com/christianhelle/clocz</a>.</p>]]></content><author><name>Christian Helle</name></author><category term="Zig" /><category term="CLI" /><summary type="html"><![CDATA[I recently wrote a CLI tool for counting lines of code in Zig. This was nothing more than a coding exercise and an experiment to see how fast a Zig compiled tool would be compared to the well-known tool cloc, which was originally written in Perl.]]></summary></entry><entry><title type="html">Integration Testing REST APIs with .http Files and HTTP File Runner</title><link href="https://christianhelle.com/2026/01/integration-testing-with-httprunner.html" rel="alternate" type="text/html" title="Integration Testing REST APIs with .http Files and HTTP File Runner" /><published>2026-01-26T00:00:00+00:00</published><updated>2026-01-26T00:00:00+00:00</updated><id>https://christianhelle.com/2026/01/integration-testing-with-httprunner</id><content type="html" xml:base="https://christianhelle.com/2026/01/integration-testing-with-httprunner.html"><![CDATA[<p>Integration testing REST APIs is a crucial part of ensuring the reliability of micro-services and web applications. While there are many tools available, using simple <code class="language-plaintext highlighter-rouge">.http</code> files offers a lightweight and version-controllable approach that I really love.</p>

<p>In this post, I’ll explore how to use <strong>HTTP File Runner</strong> (or <code class="language-plaintext highlighter-rouge">httprunner</code>), a command-line tool I built in Rust, to execute advanced integration test scenarios using <code class="language-plaintext highlighter-rouge">.http</code> files. We’ll cover everything from variable management to conditional execution and CI/CD integration.</p>

<h2 id="getting-started">Getting Started</h2>

<p>First, ensure you have <code class="language-plaintext highlighter-rouge">httprunner</code> installed. You can install it via a simple script or download a release from the <a href="https://github.com/christianhelle/httprunner">GitHub repository</a>.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Linux/macOS</span>
curl <span class="nt">-fsSL</span> https://christianhelle.com/httprunner/install | bash

<span class="c"># Windows</span>
irm https://christianhelle.com/httprunner/install.ps1 | iex
</code></pre></div></div>

<p>If you’re on Ubuntu then you can also install it using <code class="language-plaintext highlighter-rouge">snap</code></p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>snap <span class="nb">install </span>httprunner
</code></pre></div></div>

<p>Once installed, you can run any <code class="language-plaintext highlighter-rouge">.http</code> file:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>httprunner tests.http
</code></pre></div></div>

<h2 id="global-variables">Global Variables</h2>

<p>You can define global variables at the top of your <code class="language-plaintext highlighter-rouge">.http</code> file using the <code class="language-plaintext highlighter-rouge">@</code> syntax. This is perfect for values that are reused across multiple requests, like a base URL.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>@HostAddress = https://httpbin.org
@ContentType = application/json

GET {{HostAddress}}/get
Content-Type: {{ContentType}}
</code></pre></div></div>

<h2 id="built-in-functions">Built-in Functions</h2>

<p><code class="language-plaintext highlighter-rouge">httprunner</code> provides built-in functions for dynamic value generation in your <code class="language-plaintext highlighter-rouge">.http</code> files. Functions are case-insensitive and automatically generate values when the request is executed.</p>

<h3 id="available-functions">Available Functions</h3>

<h4 id="guid---generate-uuid"><code class="language-plaintext highlighter-rouge">guid()</code> - Generate UUID</h4>

<p>Generates a new UUID v4 (Universally Unique Identifier) in simple format (32 hex characters without dashes).</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>POST https://api.example.com/users
Content-Type: application/json

{
  "id": "guid()",
  "requestId": "GUID()"
}
</code></pre></div></div>

<h4 id="string---generate-random-string"><code class="language-plaintext highlighter-rouge">string()</code> - Generate Random String</h4>

<p>Generates a random alphanumeric string of 20 characters.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>POST https://api.example.com/test
Content-Type: application/json

{
  "sessionKey": "string()",
  "token": "STRING()"
}
</code></pre></div></div>

<h4 id="number---generate-random-number"><code class="language-plaintext highlighter-rouge">number()</code> - Generate Random Number</h4>

<p>Generates a random number between 0 and 100 (inclusive).</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>POST https://api.example.com/data
Content-Type: application/json

{
  "randomValue": "number()",
  "percentage": "NUMBER()"
}
</code></pre></div></div>

<h4 id="base64_encode---base64-encoding"><code class="language-plaintext highlighter-rouge">base64_encode()</code> - Base64 Encoding</h4>

<p>Encodes a string to Base64 format. The string must be enclosed in single quotes.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>POST https://api.example.com/auth
Content-Type: application/json

{
  "credentials": "base64_encode('username:password')",
  "token": "BASE64_ENCODE('Hello, World!')"
}
</code></pre></div></div>

<h4 id="upper---convert-to-uppercase"><code class="language-plaintext highlighter-rouge">upper()</code> - Convert to Uppercase</h4>

<p>Converts a string to uppercase. The string must be enclosed in single quotes.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>POST https://api.example.com/data
Content-Type: application/json

{
  "code": "upper('hello, world')",
  "shout": "UPPER('quiet text')"
}
</code></pre></div></div>

<h4 id="lower---convert-to-lowercase"><code class="language-plaintext highlighter-rouge">lower()</code> - Convert to Lowercase</h4>

<p>Converts a string to lowercase. The string must be enclosed in single quotes.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>POST https://api.example.com/data
Content-Type: application/json

{
  "normalized": "lower('HELLO, WORLD')",
  "lowercase": "LOWER('Mixed Case Text')"
}
</code></pre></div></div>

<h4 id="name---generate-full-name"><code class="language-plaintext highlighter-rouge">name()</code> - Generate Full Name</h4>

<p>Generates a random full name (first name + last name).</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>POST https://api.example.com/users
Content-Type: application/json

{
  "fullName": "name()",
  "displayName": "NAME()"
}
</code></pre></div></div>

<h4 id="first_name---generate-first-name"><code class="language-plaintext highlighter-rouge">first_name()</code> - Generate First Name</h4>

<p>Generates a random first name.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>POST https://api.example.com/users
Content-Type: application/json

{
  "firstName": "first_name()",
  "givenName": "FIRST_NAME()"
}
</code></pre></div></div>

<h4 id="last_name---generate-last-name"><code class="language-plaintext highlighter-rouge">last_name()</code> - Generate Last Name</h4>

<p>Generates a random last name.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>POST https://api.example.com/users
Content-Type: application/json

{
  "lastName": "last_name()",
  "surname": "LAST_NAME()"
}
</code></pre></div></div>

<h4 id="address---generate-address"><code class="language-plaintext highlighter-rouge">address()</code> - Generate Address</h4>

<p>Generates a random full mailing address (street, city, postal code, country).</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>POST https://api.example.com/users
Content-Type: application/json

{
  "streetAddress": "address()",
  "mailingAddress": "ADDRESS()"
}
</code></pre></div></div>

<h4 id="email---generate-email-address"><code class="language-plaintext highlighter-rouge">email()</code> - Generate Email Address</h4>

<p>Generates a random email address in the format <a href="mailto:firstname.lastname@domain.com">firstname.lastname@domain.com</a>.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>POST https://api.example.com/users
Content-Type: application/json

{
  "email": "email()",
  "contactEmail": "EMAIL()"
}
</code></pre></div></div>

<h4 id="job_title---generate-job-title"><code class="language-plaintext highlighter-rouge">job_title()</code> - Generate Job Title</h4>

<p>Generates a random job title.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>POST https://api.example.com/users
Content-Type: application/json

{
  "title": "job_title()",
  "position": "JOB_TITLE()"
}
</code></pre></div></div>

<h4 id="lorem_ipsumn---generate-lorem-ipsum-text"><code class="language-plaintext highlighter-rouge">lorem_ipsum(n)</code> - Generate Lorem Ipsum Text</h4>

<p>Generates Lorem Ipsum placeholder text with the specified number of words. The parameter <code class="language-plaintext highlighter-rouge">n</code> determines how many words to generate.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>POST https://api.example.com/content
Content-Type: application/json

{
  "summary": "lorem_ipsum(20)",
  "description": "LOREM_IPSUM(50)",
  "content": "Lorem_Ipsum(100)"
}
</code></pre></div></div>

<h4 id="getdate---get-current-date"><code class="language-plaintext highlighter-rouge">getdate()</code> - Get Current Date</h4>

<p>Returns the current local date in <code class="language-plaintext highlighter-rouge">YYYY-MM-DD</code> format.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>POST https://api.example.com/events
Content-Type: application/json

{
  "eventDate": "getdate()",
  "createdDate": "GETDATE()"
}
</code></pre></div></div>

<h4 id="gettime---get-current-time"><code class="language-plaintext highlighter-rouge">gettime()</code> - Get Current Time</h4>

<p>Returns the current local time in <code class="language-plaintext highlighter-rouge">HH:MM:SS</code> format (24-hour).</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>POST https://api.example.com/logs
Content-Type: application/json

{
  "timestamp": "gettime()",
  "logTime": "GETTIME()"
}
</code></pre></div></div>

<h4 id="getdatetime---get-current-date-and-time"><code class="language-plaintext highlighter-rouge">getdatetime()</code> - Get Current Date and Time</h4>

<p>Returns the current local date and time in <code class="language-plaintext highlighter-rouge">YYYY-MM-DD HH:MM:SS</code> format.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>POST https://api.example.com/records
Content-Type: application/json

{
  "createdAt": "getdatetime()",
  "timestamp": "GETDATETIME()"
}
</code></pre></div></div>

<h4 id="getutcdatetime---get-current-utc-date-and-time"><code class="language-plaintext highlighter-rouge">getutcdatetime()</code> - Get Current UTC Date and Time</h4>

<p>Returns the current UTC date and time in <code class="language-plaintext highlighter-rouge">YYYY-MM-DD HH:MM:SS</code> format.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>POST https://api.example.com/records
Content-Type: application/json

{
  "utcTimestamp": "getutcdatetime()",
  "serverTime": "GETUTCDATETIME()"
}
</code></pre></div></div>

<h2 id="environment-variables">Environment Variables</h2>

<p>For different environments (development, staging, production), you shouldn’t hard code values. <code class="language-plaintext highlighter-rouge">httprunner</code> supports loading variables from a <code class="language-plaintext highlighter-rouge">http-client.env.json</code> file, compatible with the VS Code REST Client extension. In most cases, the environment file would contain secrets like API keys or tokens that you don’t want to commit to version control.</p>

<p>Create a <code class="language-plaintext highlighter-rouge">http-client.env.json</code> file:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"dev"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"HostAddress"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://dev-api.example.com"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"ApiKey"</span><span class="p">:</span><span class="w"> </span><span class="s2">"dev-secret"</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"prod"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"HostAddress"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://api.example.com"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"ApiKey"</span><span class="p">:</span><span class="w"> </span><span class="s2">"prod-secret"</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Reference these variables in your <code class="language-plaintext highlighter-rouge">.http</code> file:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>GET {{HostAddress}}/users
Authorization: Bearer {{ApiKey}}
</code></pre></div></div>

<p>Then run <code class="language-plaintext highlighter-rouge">httprunner</code> with the <code class="language-plaintext highlighter-rouge">--env</code> flag:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>httprunner tests.http <span class="nt">--env</span> dev
</code></pre></div></div>

<h2 id="generating-environment-files">Generating Environment Files</h2>

<p>Manually managing <code class="language-plaintext highlighter-rouge">http-client.env.json</code> files can be tedious, especially when dealing with short-lived tokens or multiple environments. I recommend scripting the generation of this file.</p>

<p>Here is an example PowerShell script that fetches access tokens from Azure CLI and generates a comprehensive environment file for localhost, Docker, and development environments:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$management_api_tokens</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">az</span><span class="w"> </span><span class="nx">account</span><span class="w"> </span><span class="nx">get-access-token</span><span class="w"> </span><span class="se">`
</span><span class="w">  </span><span class="nt">--scope</span><span class="w"> </span><span class="nx">app://api.example.net/dev/management_api/.default</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">ConvertFrom-Json</span><span class="w">

</span><span class="nv">$simulator_tokens</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">az</span><span class="w"> </span><span class="nx">account</span><span class="w"> </span><span class="nx">get-access-token</span><span class="w"> </span><span class="se">`
</span><span class="w">  </span><span class="nt">--scope</span><span class="w"> </span><span class="nx">app://api.example.net/dev/simulator/.default</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">ConvertFrom-Json</span><span class="w">

</span><span class="nv">$environment</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w">
  </span><span class="nx">localhost</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w">
    </span><span class="nx">authorization</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Bearer "</span><span class="w"> </span><span class="err">+</span><span class="w"> </span><span class="nv">$management_api_tokens</span><span class="err">.</span><span class="nx">accessToken</span><span class="w">
    </span><span class="nx">simulator_authorization</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Bearer "</span><span class="w"> </span><span class="err">+</span><span class="w"> </span><span class="nv">$simulator_tokens</span><span class="err">.</span><span class="nx">accessToken</span><span class="w">
    </span><span class="nx">cpo</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"http://localhost:8900"</span><span class="w">
    </span><span class="nx">simulator</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"http://localhost:8901"</span><span class="w">
    </span><span class="nx">management_api</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"http://localhost:8150"</span><span class="w">
  </span><span class="p">}</span><span class="w">
  </span><span class="nx">docker</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w">
    </span><span class="nx">authorization</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Bearer "</span><span class="w"> </span><span class="err">+</span><span class="w"> </span><span class="nv">$management_api_tokens</span><span class="err">.</span><span class="nx">accessToken</span><span class="w">
    </span><span class="nx">simulator_authorization</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Bearer "</span><span class="w"> </span><span class="err">+</span><span class="w"> </span><span class="nv">$simulator_tokens</span><span class="err">.</span><span class="nx">accessToken</span><span class="w">
    </span><span class="nx">cpo</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"http://host.docker.internal:8900"</span><span class="w">
    </span><span class="nx">simulator</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"http://host.docker.internal:8901"</span><span class="w">
    </span><span class="nx">management_api</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"http://host.docker.internal:8150"</span><span class="w">
  </span><span class="p">}</span><span class="w">
  </span><span class="nx">dev</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w">
    </span><span class="nx">authorization</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Bearer "</span><span class="w"> </span><span class="err">+</span><span class="w"> </span><span class="nv">$management_api_tokens</span><span class="err">.</span><span class="nx">accessToken</span><span class="w">
    </span><span class="nx">simulator_authorization</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Bearer "</span><span class="w"> </span><span class="err">+</span><span class="w"> </span><span class="nv">$simulator_tokens</span><span class="err">.</span><span class="nx">accessToken</span><span class="w">
    </span><span class="nx">cpo</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"https://ocpi.example.net"</span><span class="w">
    </span><span class="nx">simulator</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"https://ocpi-simulator.example.net"</span><span class="w">
    </span><span class="nx">management_api</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"https://management-api.example.net"</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="n">Set-Content</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="o">.</span><span class="nx">/http-client.env.json</span><span class="w"> </span><span class="nt">-Value</span><span class="w"> </span><span class="p">(</span><span class="nv">$environment</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">ConvertTo-Json</span><span class="w"> </span><span class="nt">-Depth</span><span class="w"> </span><span class="nx">10</span><span class="p">)</span><span class="w">
</span></code></pre></div></div>

<h2 id="delays">Delays</h2>

<p>Rate limiting is a common constraint when testing APIs. <code class="language-plaintext highlighter-rouge">httprunner</code> allows you to introduce delays either globally or per request. My personal use case would be eventually consistent systems where you want to wait for a certain state before proceeding.</p>

<p>To add a delay between every request in a run, use the CLI flag:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>httprunner tests.http <span class="nt">--delay</span> 500
</code></pre></div></div>

<p>For more granular control, use comments in your <code class="language-plaintext highlighter-rouge">.http</code> file:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code># @pre-delay 1000
# @post-delay 500
GET https://httpbin.org/get
</code></pre></div></div>

<ul>
  <li><code class="language-plaintext highlighter-rouge">@pre-delay</code>: Wait before sending the request (in milliseconds).</li>
  <li><code class="language-plaintext highlighter-rouge">@post-delay</code>: Wait after the request completes (in milliseconds).</li>
</ul>

<h2 id="timeouts">Timeouts</h2>

<p>Network conditions can be unpredictable. You can configure timeouts to fail tests if an API is too slow. If you’re testing against a local development server, you might want to set a shorter timeout than the default 30 seconds.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code># Wait up to 5 seconds for a response
# @timeout 5000 ms
GET https://api.example.net/delay/2

# Custom connection timeout (default is 30s)
# @connection-timeout 10 s
GET https://api.example.net/get
</code></pre></div></div>

<p>Supported units include <code class="language-plaintext highlighter-rouge">ms</code>, <code class="language-plaintext highlighter-rouge">s</code>, and <code class="language-plaintext highlighter-rouge">m</code>.</p>

<h2 id="request-chaining">Request Chaining</h2>

<p>One of the most powerful features for integration testing is chaining requests—using data from a previous response in a subsequent request. <code class="language-plaintext highlighter-rouge">httprunner</code> supports extracting values from headers, JSON bodies, and even the original request data.</p>

<h3 id="request-variable-syntax">Request Variable Syntax</h3>

<p>The syntax for request variables is:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{{&lt;request_name&gt;.&lt;source&gt;.&lt;part&gt;.&lt;path&gt;}}
</code></pre></div></div>

<ul>
  <li><strong>request_name</strong>: The name defined via <code class="language-plaintext highlighter-rouge"># @name &lt;name&gt;</code></li>
  <li><strong>source</strong>: <code class="language-plaintext highlighter-rouge">request</code> or <code class="language-plaintext highlighter-rouge">response</code></li>
  <li><strong>part</strong>: <code class="language-plaintext highlighter-rouge">body</code> or <code class="language-plaintext highlighter-rouge">headers</code></li>
  <li><strong>path</strong>: The specific value to extract</li>
</ul>

<h3 id="extraction-patterns">Extraction Patterns</h3>

<ul>
  <li><strong>JSON Bodies</strong>: Use JSONPath syntax (e.g., <code class="language-plaintext highlighter-rouge">$.user.id</code>, <code class="language-plaintext highlighter-rouge">$.items[0].name</code>).</li>
  <li><strong>Headers</strong>: Use the header name (e.g., <code class="language-plaintext highlighter-rouge">Content-Type</code>, <code class="language-plaintext highlighter-rouge">Location</code>).</li>
  <li><strong>Full Body</strong>: Use <code class="language-plaintext highlighter-rouge">*</code> to extract the entire body.</li>
</ul>

<h3 id="example-scenario">Example Scenario</h3>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code># @name create_user
POST https://api.example.com/users
Content-Type: application/json

{
  "username": "test_user",
  "role": "admin"
}

###

# @name login
POST https://api.example.com/login
Content-Type: application/json

{
  "username": "{{create_user.request.body.$.username}}",
  "password": "default_password"
}

###

# Use the token from login response
GET https://api.example.com/admin/dashboard
Authorization: Bearer {{login.response.body.$.token}}
X-User-Role: {{create_user.request.body.$.role}}
</code></pre></div></div>

<h2 id="conditional-execution">Conditional Execution</h2>

<p>Complex test scenarios often require conditional logic. You might want to skip a cleanup step if the creation failed, or run specific tests only if a feature flag is enabled.</p>

<p><code class="language-plaintext highlighter-rouge">httprunner</code> provides <code class="language-plaintext highlighter-rouge">@dependsOn</code> and <code class="language-plaintext highlighter-rouge">@if</code> directives.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code># @name create_user
POST https://api.example.com/users
...

###

# Only run if create_user succeeded (HTTP 2xx)
# @dependsOn create_user
POST https://api.example.com/users/{{create_user.response.body.$.id}}/activate

###

# Only run if the user status is "active"
# @if create_user.response.body.$.status active
GET https://api.example.com/users/{{create_user.response.body.$.id}}
</code></pre></div></div>

<h2 id="assertions">Assertions</h2>

<p>No test is complete without verification. <code class="language-plaintext highlighter-rouge">httprunner</code> allows you to assert response status, headers, and body content directly in your <code class="language-plaintext highlighter-rouge">.http</code> file.</p>

<h3 id="basic-assertions">Basic Assertions</h3>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>GET https://httpbin.org/json

EXPECTED_RESPONSE_STATUS 200
EXPECTED_RESPONSE_HEADERS "Content-Type: application/json"
EXPECTED_RESPONSE_BODY "slideshow"
</code></pre></div></div>

<h3 id="variable-substitution-in-assertions">Variable Substitution in Assertions</h3>

<p>Crucially, <strong>variables are fully supported in assertions</strong>, allowing you to validate that a response matches input parameters or previous request data.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>@expected_status=200
@user_id=123

# @name create_user
POST https://api.example.com/users
{ "id": "{{user_id}}" }

###

GET https://api.example.com/users/{{user_id}}

EXPECTED_RESPONSE_STATUS {{expected_status}}
EXPECTED_RESPONSE_BODY "{{user_id}}"
EXPECTED_RESPONSE_HEADERS "Location: /users/{{user_id}}"
</code></pre></div></div>

<p>This makes dynamic testing significantly easier, as you don’t need to hardcode expected values.</p>

<p>If any assertion fails, <code class="language-plaintext highlighter-rouge">httprunner</code> will report the test as failed and exit with a non-zero status code, which is essential for CI/CD pipelines.</p>

<p>Note: For requests that have assertions that expect failed responses (e.g., testing error handling), you can use <code class="language-plaintext highlighter-rouge">EXPECTED_RESPONSE_STATUS</code> to specify the expected failure status code (like 400 or 404). The “failed” request will no longer be flagged as failed, but instead will be validated against the expected status code and assertions.</p>

<h2 id="verbose-mode">Verbose Mode</h2>

<p>When developing or debugging, you often need more insight into what <code class="language-plaintext highlighter-rouge">httprunner</code> is doing.</p>

<p>Use the <code class="language-plaintext highlighter-rouge">--verbose</code> flag to see detailed request and response information, including full headers and bodies. Add <code class="language-plaintext highlighter-rouge">--pretty-json</code> to format JSON payloads for readability.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>httprunner tests.http <span class="nt">--verbose</span> <span class="nt">--pretty-json</span>
</code></pre></div></div>

<h2 id="discovery-mode">Discovery Mode</h2>

<p>If you have tests scattered across multiple directories, use <code class="language-plaintext highlighter-rouge">--discover</code> to recursively find and execute all <code class="language-plaintext highlighter-rouge">.http</code> files.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>httprunner <span class="nt">--discover</span> <span class="nt">--verbose</span>
</code></pre></div></div>

<h2 id="report-generation-and-logging">Report Generation and Logging</h2>

<p>For long-running tests or CI pipelines, console output isn’t enough. <code class="language-plaintext highlighter-rouge">httprunner</code> can generate structured reports and detailed logs.</p>

<h3 id="reports">Reports</h3>

<p>Generate summary reports in Markdown (default) or HTML using the <code class="language-plaintext highlighter-rouge">--report</code> flag. HTML reports include responsive styling and dark mode support.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Generate Markdown report</span>
httprunner tests.http <span class="nt">--report</span>

<span class="c"># Generate HTML report</span>
httprunner tests.http <span class="nt">--report</span> html
</code></pre></div></div>

<h3 id="logging">Logging</h3>

<p>Use <code class="language-plaintext highlighter-rouge">--log</code> to save all console output to a file. This is useful for auditing and post-execution analysis.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>httprunner tests.http <span class="nt">--log</span> execution.log
</code></pre></div></div>

<h2 id="export-mode">Export Mode</h2>

<p>Sometimes you need the raw HTTP data for documentation or manual replay. The <code class="language-plaintext highlighter-rouge">--export</code> flag saves every request and response to individual, timestamped files.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>httprunner tests.http <span class="nt">--export</span>
</code></pre></div></div>

<p>This will create files like <code class="language-plaintext highlighter-rouge">GET_users_request_1738016400.log</code> and <code class="language-plaintext highlighter-rouge">GET_users_response_1738016400.log</code> containing the exact payload sent and received.</p>

<h2 id="insecure-https">Insecure HTTPS</h2>

<p>When testing against local development environments or staging servers with self-signed certificates, SSL validation can be a blocker. Use <code class="language-plaintext highlighter-rouge">--insecure</code> to bypass these checks.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>httprunner https://localhost:5001/api/tests.http <span class="nt">--insecure</span>
</code></pre></div></div>

<p><strong>Note:</strong> Only use this in trusted environments!</p>

<h2 id="telemetry">Telemetry</h2>

<p><code class="language-plaintext highlighter-rouge">httprunner</code> collects anonymous usage data to help improve the tool. If you prefer to opt-out, you can disable it via a flag or environment variable.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Disable via CLI</span>
httprunner tests.http <span class="nt">--no-telemetry</span>

<span class="c"># Disable via Environment Variable</span>
<span class="nb">export </span><span class="nv">HTTPRUNNER_TELEMETRY_OPTOUT</span><span class="o">=</span>1
<span class="c"># or</span>
<span class="nb">export </span><span class="nv">DO_NOT_TRACK</span><span class="o">=</span>1
</code></pre></div></div>

<h2 id="cicd-integration">CI/CD Integration</h2>

<p>Automating these tests in a CI/CD pipeline is straightforward. Since <code class="language-plaintext highlighter-rouge">httprunner</code> is available as a Docker image and a standalone binary, it fits easily into GitHub Actions or GitLab CI.</p>

<p>Here is an example <strong>GitHub Actions</strong> workflow:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">name</span><span class="pi">:</span> <span class="s">Integration Tests</span>

<span class="na">on</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">push</span><span class="pi">,</span> <span class="nv">pull_request</span><span class="pi">]</span>

<span class="na">jobs</span><span class="pi">:</span>
  <span class="na">test</span><span class="pi">:</span>
    <span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-latest</span>
    <span class="na">steps</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/checkout@v4</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Install HTTP Runner</span>
        <span class="na">run</span><span class="pi">:</span> <span class="s">snap install httprunner</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Run API Tests</span>
        <span class="na">run</span><span class="pi">:</span> <span class="s">tests/api.http --env staging --report markdown</span>
</code></pre></div></div>

<p>Or, if you prefer installing the binary:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Install HTTP Runner</span>
  <span class="na">run</span><span class="pi">:</span> <span class="s">curl -fsSL https://christianhelle.com/httprunner/install | bash</span>

<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Run Tests</span>
  <span class="na">run</span><span class="pi">:</span> <span class="s">httprunner tests/*.http --env staging --report html</span>
</code></pre></div></div>

<p>This setup ensures that every change is validated against your API, providing fast feedback on regressions.</p>

<h2 id="conclusion">Conclusion</h2>

<p>By combining the simplicity of <code class="language-plaintext highlighter-rouge">.http</code> files with the advanced features of <code class="language-plaintext highlighter-rouge">httprunner</code>, you can build a robust integration testing suite that lives right alongside your code. It’s version-controlled, easy to read, and powerful enough for complex scenarios involving authentication, chaining, and conditional logic.</p>

<p>Give it a try and let me know what you think!</p>]]></content><author><name>Christian Helle</name></author><category term="Testing" /><category term="Integration Testing" /><category term="REST" /><category term="HTTP" /><category term="Rust" /><summary type="html"><![CDATA[Integration testing REST APIs is a crucial part of ensuring the reliability of micro-services and web applications. While there are many tools available, using simple .http files offers a lightweight and version-controllable approach that I really love.]]></summary></entry><entry><title type="html">Rewriting HTTP File Runner in Rust (from Zig)</title><link href="https://christianhelle.com/2025/10/httprunner-zig-to-rust-rewrite.html" rel="alternate" type="text/html" title="Rewriting HTTP File Runner in Rust (from Zig)" /><published>2025-10-27T00:00:00+00:00</published><updated>2025-10-27T00:00:00+00:00</updated><id>https://christianhelle.com/2025/10/httprunner-zig-to-rust-rewrite</id><content type="html" xml:base="https://christianhelle.com/2025/10/httprunner-zig-to-rust-rewrite.html"><![CDATA[<p>A few months ago, I wrote about <a href="/2025/06/http-file-runner-zig-tool">HTTP File Runner</a>, a command-line tool I built in Zig to execute <code class="language-plaintext highlighter-rouge">.http</code> files from the terminal. The project was a successful learning exercise and a genuinely useful tool. However, I recently completed a full rewrite of the project from Zig to Rust. This wasn’t a decision made lightly or based on preferences—it was a <strong>technical necessity</strong>.</p>

<h2 id="the-critical-problem-https-certificate-validation">The Critical Problem: HTTPS Certificate Validation</h2>

<p>The primary driver for this migration was a <strong>blocking technical limitation</strong> in Zig’s standard library. The issue was simple but insurmountable: Zig’s HTTP client (<code class="language-plaintext highlighter-rouge">std.http</code>) cannot be configured to bypass certificate validation. This makes testing against development environments with self-signed certificates, a fundamental requirement for any serious HTTP testing tool, needlessly difficult, or impossible.</p>

<h3 id="the-failed-solution">The Failed Solution</h3>

<p>I explored integrating libcurl to work around this limitation, but the cross-platform compilation complexity proved prohibitive. Zig’s excellent cross-compilation support ironically became a liability when trying to integrate C libraries with complex build requirements across multiple platforms.</p>

<p>This wasn’t about preferring one language over another—the Zig implementation simply couldn’t meet basic requirements for development environment testing.</p>

<h2 id="migration-overview">Migration Overview</h2>

<p>The migration was fully AI assisted, comprehensive, touching every aspect of the project:</p>

<ul>
  <li><strong>54 commits</strong> across the migration branch</li>
  <li><strong>3,419 lines added, 2,634 lines removed</strong></li>
  <li><strong>54 files changed</strong></li>
  <li><strong>12 core modules</strong> successfully ported</li>
  <li><strong>100% feature parity</strong> maintained</li>
</ul>

<p>You can see the complete details in <a href="https://github.com/christianhelle/httprunner/pull/43">Pull Request #43</a>.</p>

<h2 id="architecture-from-zig-to-rust">Architecture: From Zig to Rust</h2>

<h3 id="module-structure">Module Structure</h3>

<p>The Rust implementation maintains a clean, modular architecture:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>src/
├── main.rs              # Application entry point
├── cli.rs               # Command-line interface with clap
├── types.rs             # Core data structures
├── colors.rs            # Terminal color utilities
├── parser.rs            # HTTP file parsing
├── environment.rs       # Environment file loading
├── runner.rs            # HTTP request execution with reqwest
├── assertions.rs        # Response assertion validation
├── request_variables.rs # Request variable substitution
├── processor.rs         # Request processing pipeline
├── discovery.rs         # File discovery with walkdir
├── log.rs              # Logging functionality
└── upgrade.rs          # Self-update feature
</code></pre></div></div>

<h3 id="key-dependencies">Key Dependencies</h3>

<p>The Rust ecosystem provided mature, battle-tested libraries for every requirement:</p>

<ul>
  <li><strong>clap</strong>: Modern command-line argument parsing with declarative macros</li>
  <li><strong>reqwest</strong>: Full-featured HTTP client with TLS control</li>
  <li><strong>colored</strong>: Cross-platform terminal colors</li>
  <li><strong>serde_json</strong>: JSON parsing for environment files and chaining requests variables</li>
  <li><strong>walkdir</strong>: Efficient directory traversal</li>
  <li><strong>anyhow</strong>: Ergonomic error handling with context chaining</li>
</ul>

<h2 id="feature-parity-verification">Feature Parity Verification</h2>

<p>All features from the Zig implementation are fully supported in the Rust version:</p>

<table>
  <thead>
    <tr>
      <th>Feature</th>
      <th>Status</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>HTTP methods (GET, POST, PUT, DELETE, PATCH)</td>
      <td>✅ 100%</td>
    </tr>
    <tr>
      <td>Variable substitution</td>
      <td>✅ 100%</td>
    </tr>
    <tr>
      <td>Request variables &amp; chaining</td>
      <td>✅ 100%</td>
    </tr>
    <tr>
      <td>Response assertions (status, body, headers)</td>
      <td>✅ 100%</td>
    </tr>
    <tr>
      <td>Environment files (http-client.env.json)</td>
      <td>✅ 100%</td>
    </tr>
    <tr>
      <td>File discovery mode (–discover)</td>
      <td>✅ 100%</td>
    </tr>
    <tr>
      <td>Verbose mode (–verbose)</td>
      <td>✅ 100%</td>
    </tr>
    <tr>
      <td>Logging to file (–log)</td>
      <td>✅ 100%</td>
    </tr>
    <tr>
      <td>Version information (–version)</td>
      <td>✅ 100%</td>
    </tr>
    <tr>
      <td>Self-upgrade (–upgrade)</td>
      <td>✅ 100%</td>
    </tr>
    <tr>
      <td>Colored output with emojis</td>
      <td>✅ 100%</td>
    </tr>
    <tr>
      <td>Custom headers</td>
      <td>✅ 100%</td>
    </tr>
    <tr>
      <td>Request body support</td>
      <td>✅ 100%</td>
    </tr>
    <tr>
      <td>Multiple file processing</td>
      <td>✅ 100%</td>
    </tr>
  </tbody>
</table>

<h2 id="technical-improvements">Technical Improvements</h2>

<h3 id="better-error-handling">Better Error Handling</h3>

<p><strong>Zig</strong>: Manual error unions and explicit error propagation</p>

<div class="language-zig highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">const</span> <span class="n">result</span> <span class="o">=</span> <span class="k">try</span> <span class="n">parseHttpFile</span><span class="p">(</span><span class="n">allocator</span><span class="p">,</span> <span class="n">file_path</span><span class="p">);</span>
<span class="k">defer</span> <span class="n">result</span><span class="p">.</span><span class="nf">deinit</span><span class="p">();</span>
</code></pre></div></div>

<p><strong>Rust</strong>: <code class="language-plaintext highlighter-rouge">anyhow::Result</code> with context chaining and detailed error messages</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">let</span> <span class="n">result</span> <span class="o">=</span> <span class="nf">parse_http_file</span><span class="p">(</span><span class="n">file_path</span><span class="p">)</span>
    <span class="nf">.context</span><span class="p">(</span><span class="s">"Failed to parse HTTP file"</span><span class="p">)</span><span class="o">?</span><span class="p">;</span>
</code></pre></div></div>

<h3 id="type-safety-and-memory-management">Type Safety and Memory Management</h3>

<p><strong>Zig</strong>: Explicit memory management with <code class="language-plaintext highlighter-rouge">defer</code> statements</p>

<div class="language-zig highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">const</span> <span class="n">allocator</span> <span class="o">=</span> <span class="n">std</span><span class="p">.</span><span class="py">heap</span><span class="p">.</span><span class="py">page_allocator</span><span class="p">;</span>
<span class="k">var</span> <span class="n">list</span> <span class="o">=</span> <span class="k">try</span> <span class="n">std</span><span class="p">.</span><span class="nf">ArrayList</span><span class="p">(</span><span class="kt">u8</span><span class="p">).</span><span class="nf">initCapacity</span><span class="p">(</span><span class="n">allocator</span><span class="p">,</span> <span class="mi">1024</span><span class="p">);</span>
<span class="k">defer</span> <span class="n">list</span><span class="p">.</span><span class="nf">deinit</span><span class="p">();</span>
</code></pre></div></div>

<p><strong>Rust</strong>: Ownership system with compile-time guarantees</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">let</span> <span class="k">mut</span> <span class="n">list</span> <span class="o">=</span> <span class="nn">Vec</span><span class="p">::</span><span class="nf">with_capacity</span><span class="p">(</span><span class="mi">1024</span><span class="p">);</span>
<span class="c1">// Automatically dropped when out of scope</span>
</code></pre></div></div>

<h3 id="http-client-capabilities">HTTP Client Capabilities</h3>

<p><strong>Zig</strong>: Limited <code class="language-plaintext highlighter-rouge">std.http</code> with no TLS configuration options</p>

<p><strong>Rust</strong>: Full TLS control, connection pooling, timeout management, and certificate validation options</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">let</span> <span class="n">client</span> <span class="o">=</span> <span class="nn">Client</span><span class="p">::</span><span class="nf">builder</span><span class="p">()</span>
    <span class="nf">.danger_accept_invalid_certs</span><span class="p">(</span><span class="n">allow_insecure</span><span class="p">)</span>
    <span class="nf">.timeout</span><span class="p">(</span><span class="nn">Duration</span><span class="p">::</span><span class="nf">from_secs</span><span class="p">(</span><span class="mi">30</span><span class="p">))</span>
    <span class="nf">.build</span><span class="p">()</span><span class="o">?</span><span class="p">;</span>
</code></pre></div></div>

<p>This is the critical improvement that drove the entire migration. The <code class="language-plaintext highlighter-rouge">reqwest</code> library provides the flexibility needed for real-world testing scenarios.</p>

<h3 id="cli-parsing">CLI Parsing</h3>

<p><strong>Zig</strong>: Manual argument parsing with custom logic</p>

<p><strong>Rust</strong>: Declarative <code class="language-plaintext highlighter-rouge">clap</code> with automatic help generation</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">#[derive(Parser)]</span>
<span class="nd">#[command(name</span> <span class="nd">=</span> <span class="s">"httprunner"</span><span class="nd">)]</span>
<span class="nd">#[command(about</span> <span class="nd">=</span> <span class="s">"Run .http files from the command line"</span><span class="nd">)]</span>
<span class="k">struct</span> <span class="n">Cli</span> <span class="p">{</span>
    <span class="nd">#[arg(help</span> <span class="nd">=</span> <span class="s">"HTTP files to process"</span><span class="nd">)]</span>
    <span class="n">files</span><span class="p">:</span> <span class="nb">Vec</span><span class="o">&lt;</span><span class="n">PathBuf</span><span class="o">&gt;</span><span class="p">,</span>

    <span class="nd">#[arg(short,</span> <span class="nd">long,</span> <span class="nd">help</span> <span class="nd">=</span> <span class="s">"Enable verbose output"</span><span class="nd">)]</span>
    <span class="n">verbose</span><span class="p">:</span> <span class="nb">bool</span><span class="p">,</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="migration-process">Migration Process</h2>

<p>The migration followed a systematic, phased approach:</p>

<h3 id="phase-1-core-infrastructure">Phase 1: Core Infrastructure</h3>

<ul>
  <li>Set up Rust project structure with Cargo.toml</li>
  <li>Implemented build script for version generation</li>
  <li>Created core type definitions</li>
  <li>Added color utilities</li>
  <li>Built HTTP file parser</li>
</ul>

<h3 id="phase-2-http-execution">Phase 2: HTTP Execution</h3>

<ul>
  <li>Integrated <code class="language-plaintext highlighter-rouge">reqwest</code> for HTTP operations</li>
  <li>Implemented response assertions</li>
  <li>Added request variable substitution with JSONPath support</li>
  <li>Built environment file loader</li>
</ul>

<h3 id="phase-3-cli--features">Phase 3: CLI &amp; Features</h3>

<ul>
  <li>Implemented <code class="language-plaintext highlighter-rouge">clap</code>-based CLI interface</li>
  <li>Added file discovery mode</li>
  <li>Implemented logging functionality</li>
  <li>Added self-update feature</li>
  <li>Created comprehensive request processor</li>
</ul>

<h3 id="phase-4-infrastructure">Phase 4: Infrastructure</h3>

<ul>
  <li>Updated GitHub Actions workflows for Rust</li>
  <li>Migrated dev container to Rust toolchain</li>
  <li>Updated Docker configuration</li>
  <li>Modified release workflows for Rust binaries</li>
  <li>Added Snap packaging for Rust version</li>
</ul>

<h3 id="phase-5-cleanup--documentation">Phase 5: Cleanup &amp; Documentation</h3>

<ul>
  <li>Removed Zig implementation from main branch</li>
  <li>Updated all documentation for Rust</li>
  <li>Added migration guides</li>
  <li>Updated README with Rust instructions</li>
  <li>Added Cargo/Crates.io installation instructions</li>
</ul>

<h2 id="build-system-comparison">Build System Comparison</h2>

<table>
  <thead>
    <tr>
      <th>Task</th>
      <th>Zig (Legacy)</th>
      <th>Rust (Current)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Debug build</td>
      <td><code class="language-plaintext highlighter-rouge">zig build</code></td>
      <td><code class="language-plaintext highlighter-rouge">cargo build</code></td>
    </tr>
    <tr>
      <td>Release build</td>
      <td><code class="language-plaintext highlighter-rouge">zig build -Doptimize=ReleaseFast</code></td>
      <td><code class="language-plaintext highlighter-rouge">cargo build --release</code></td>
    </tr>
    <tr>
      <td>Run tests</td>
      <td><code class="language-plaintext highlighter-rouge">zig build test</code></td>
      <td><code class="language-plaintext highlighter-rouge">cargo test</code></td>
    </tr>
    <tr>
      <td>Format code</td>
      <td><code class="language-plaintext highlighter-rouge">zig fmt .</code></td>
      <td><code class="language-plaintext highlighter-rouge">cargo fmt</code></td>
    </tr>
    <tr>
      <td>Lint code</td>
      <td>N/A</td>
      <td><code class="language-plaintext highlighter-rouge">cargo clippy</code></td>
    </tr>
    <tr>
      <td>Clean</td>
      <td><code class="language-plaintext highlighter-rouge">rm -rf zig-out zig-cache</code></td>
      <td><code class="language-plaintext highlighter-rouge">cargo clean</code></td>
    </tr>
  </tbody>
</table>

<p>The Rust tooling ecosystem provides a more comprehensive development experience with integrated testing, formatting, linting, and dependency management.</p>

<h2 id="installation-now-even-easier">Installation: Now Even Easier</h2>

<p>The Rust version adds a new installation method via Cargo:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Install from crates.io</span>
cargo <span class="nb">install </span>httprunner
</code></pre></div></div>

<p>All previous installation methods remain supported:</p>

<ul>
  <li>Automated installation scripts (Linux/macOS/Windows)</li>
  <li>Snap Store: <code class="language-plaintext highlighter-rouge">snap install httprunner</code></li>
  <li>Manual download from GitHub Releases</li>
  <li>Docker: <code class="language-plaintext highlighter-rouge">docker pull christianhelle/httprunner</code></li>
  <li>Build from source: <code class="language-plaintext highlighter-rouge">cargo build --release</code></li>
</ul>

<h2 id="the-zig-legacy">The Zig Legacy</h2>

<p>The original Zig implementation has been preserved in a separate repository: <a href="https://github.com/christianhelle/httprunner-zig">christianhelle/httprunner-zig</a>. While it’s no longer actively maintained, it remains available as a historical reference and testament to Zig’s capabilities within its limitations.</p>

<p>The Zig version was an excellent learning experience, and I genuinely enjoyed working with the language. Zig’s simplicity, zero-cost abstractions, and explicit nature made it a pleasure to write. The limitation that forced this migration wasn’t a reflection on Zig as a language—it was simply a missing feature in the standard library’s HTTP implementation.</p>

<h2 id="lessons-learned">Lessons Learned</h2>

<h3 id="when-to-rewrite">When to Rewrite</h3>

<p>This migration taught me important lessons about when a rewrite is justified:</p>

<p>✅ <strong>Good reasons to rewrite:</strong></p>

<ul>
  <li>Blocking technical limitations that prevent core functionality</li>
  <li>Ecosystem maturity issues affecting long-term maintainability</li>
  <li>Fundamental architectural problems that can’t be incrementally improved</li>
</ul>

<p>❌ <strong>Bad reasons to rewrite:</strong></p>

<ul>
  <li>Language preference or “grass is greener” syndrome</li>
  <li>Minor inconveniences that can be worked around</li>
  <li>Wanting to try new technologies without clear benefits</li>
</ul>

<h3 id="language-selection-matters">Language Selection Matters</h3>

<p>While both Zig and Rust are excellent systems programming languages, their ecosystems have different maturity levels:</p>

<p><strong>Zig Strengths:</strong></p>

<ul>
  <li>Simpler syntax and learning curve</li>
  <li>Excellent cross-compilation support</li>
  <li>No hidden control flow</li>
  <li>Explicit and predictable behavior</li>
</ul>

<p><strong>Rust Strengths:</strong></p>

<ul>
  <li>Mature ecosystem with battle-tested libraries</li>
  <li>Comprehensive standard library and crate ecosystem</li>
  <li>Strong compile-time guarantees via ownership system</li>
  <li>Extensive tooling (cargo, clippy, rustfmt)</li>
</ul>

<p>For a production tool that needs to work reliably across various environments and scenarios, Rust’s ecosystem maturity proved decisive.</p>

<h2 id="performance-comparison">Performance Comparison</h2>

<p>Both implementations are fast, but with different characteristics:</p>

<p><strong>Binary Size:</strong></p>

<ul>
  <li>Zig: ~700KB (optimized for binary size)</li>
  <li>Rust (release): ~1.7MB (with all release build optimization and optimized for size)</li>
</ul>

<p><strong>Startup Time:</strong></p>

<ul>
  <li>Both: Instant (&lt; 10ms)</li>
</ul>

<p><strong>Memory Usage:</strong></p>

<ul>
  <li>Both: Minimal (&lt; 10MB for typical workloads)</li>
</ul>

<p><strong>HTTP Performance:</strong></p>

<ul>
  <li>Zig: Fast, but limited by <code class="language-plaintext highlighter-rouge">std.http</code> capabilities</li>
  <li>Rust: Fast with more features (connection pooling, better TLS)</li>
</ul>

<p>The performance differences are negligible for this use case. The real benefits are in functionality and maintainability.</p>

<h2 id="moving-forward">Moving Forward</h2>

<p>The Rust version of HTTP File Runner is now the primary implementation and receives all active development. Future enhancements include:</p>

<ul>
  <li><strong>Request timeout configuration</strong>: Per-request and global timeout settings</li>
  <li><strong>Response body filtering</strong>: JSONPath queries and XML parsing</li>
  <li><strong>Parallel execution</strong>: Concurrent processing of non-chained requests for faster test suites</li>
  <li><strong>Enhanced reporting</strong>: JSON, XML, and HTML output formats</li>
</ul>

<h2 id="conclusion">Conclusion</h2>

<p>Rewriting HTTP File Runner from Zig to Rust was driven by pragmatic necessity rather than preference. The inability to configure TLS certificate validation in Zig’s standard library was a blocking issue for a serious HTTP testing tool. While I enjoyed working with Zig and appreciated its design philosophy, the project needed the functionality and ecosystem maturity that Rust provides.</p>

<p>The migration maintained 100% feature parity while adding the critical capability to work with self-signed certificates in development environments. The Rust ecosystem’s mature libraries for HTTP, CLI parsing, and error handling made the rewrite straightforward and resulted in more maintainable code.</p>

<p>If you’re using the Zig version, I encourage you to try the Rust version:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cargo <span class="nb">install </span>httprunner
</code></pre></div></div>

<p>The tool remains fast, small, and cross-platform—but now it actually works in all the scenarios it needs to support. You can read more about the original Zig implementation in my <a href="/2025/06/http-file-runner">previous post</a>.</p>

<p>For more details on the migration, check out:</p>

<ul>
  <li><a href="https://github.com/christianhelle/httprunner/pull/43">Pull Request #43</a> - Complete migration details</li>
  <li><a href="https://github.com/christianhelle/httprunner">HTTP File Runner repository</a> - Rust implementation</li>
  <li><a href="https://github.com/christianhelle/httprunner-zig">HTTP File Runner (Zig)</a> - Original implementation</li>
  <li><a href="https://christianhelle.com/httprunner/">Documentation</a> - Full user guide</li>
</ul>

<p>The project continues to evolve, and I’m excited about the possibilities that Rust’s ecosystem enables for future enhancements.</p>]]></content><author><name>Christian Helle</name></author><category term="Rust" /><category term="Zig" /><category term="HTTP" /><category term="Migration" /><summary type="html"><![CDATA[A few months ago, I wrote about HTTP File Runner, a command-line tool I built in Zig to execute .http files from the terminal. The project was a successful learning exercise and a genuinely useful tool. However, I recently completed a full rewrite of the project from Zig to Rust. This wasn’t a decision made lightly or based on preferences—it was a technical necessity.]]></summary></entry><entry><title type="html">HttpTestGen - .http file testing framework for .NET</title><link href="https://christianhelle.com/2025/09/httptestgen-dotnet-testing-framework.html" rel="alternate" type="text/html" title="HttpTestGen - .http file testing framework for .NET" /><published>2025-09-18T00:00:00+00:00</published><updated>2025-09-18T00:00:00+00:00</updated><id>https://christianhelle.com/2025/09/httptestgen-dotnet-testing-framework</id><content type="html" xml:base="https://christianhelle.com/2025/09/httptestgen-dotnet-testing-framework.html"><![CDATA[<p>I’m excited to introduce <a href="https://github.com/christianhelle/httptestgen">HttpTestGen</a>, a powerful .NET source generator that automatically converts <code class="language-plaintext highlighter-rouge">.http</code> files into fully functional C# test code. This innovative tool bridges the gap between API testing in IDEs (like Visual Studio Code with the REST Client extension) and automated testing in your .NET projects.</p>

<p>If you’ve been using <code class="language-plaintext highlighter-rouge">.http</code> files to test your APIs manually in Visual Studio or other IDEs, HttpTestGen takes this workflow to the next level by automatically generating unit tests from those same files. This means you can design, test, and validate your APIs using the familiar <code class="language-plaintext highlighter-rouge">.http</code> syntax, then seamlessly integrate those tests into your automated test suite.</p>

<h2 id="what-is-httptestgen">What is HttpTestGen?</h2>

<p><a href="https://github.com/christianhelle/httptestgen">HttpTestGen</a> is a .NET source generator that reads <code class="language-plaintext highlighter-rouge">.http</code> files in your test projects and automatically generates corresponding xUnit or TUnit test methods at compile time. The tool supports a comprehensive range of HTTP operations and includes sophisticated assertion capabilities for validating API responses.</p>

<h3 id="key-features">Key Features</h3>

<p>HttpTestGen offers a rich set of features designed to make API testing both powerful and intuitive:</p>

<ul>
  <li><strong>Automatic Test Generation</strong>: Transform <code class="language-plaintext highlighter-rouge">.http</code> files into xUnit or TUnit test code at compile time</li>
  <li><strong>Rich HTTP Support</strong>: Parse GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, and TRACE methods</li>
  <li><strong>Header Processing</strong>: Full support for HTTP headers including custom headers</li>
  <li><strong>Request Bodies</strong>: Support for JSON, XML, and text request bodies</li>
  <li><strong>Response Assertions</strong>: Validate expected status codes and response headers</li>
  <li><strong>Multiple Test Frameworks</strong>: Generate tests for xUnit and TUnit</li>
  <li><strong>Source Generator</strong>: Zero-runtime overhead with compile-time code generation</li>
  <li><strong>IDE Integration</strong>: Works seamlessly with existing <code class="language-plaintext highlighter-rouge">.http</code> files in your IDE</li>
</ul>

<h2 id="installation">Installation</h2>

<p>Getting started with HttpTestGen is straightforward. The tool is available as NuGet packages for both xUnit and TUnit frameworks.</p>

<h3 id="xunit-generator">xUnit Generator</h3>

<p>For xUnit-based projects, add the following package reference to your test project:</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;PackageReference</span> <span class="na">Include=</span><span class="s">"HttpTestGen.XunitGenerator"</span> <span class="na">Version=</span><span class="s">"1.0.0"</span><span class="nt">&gt;</span>
  <span class="nt">&lt;PrivateAssets&gt;</span>all<span class="nt">&lt;/PrivateAssets&gt;</span>
  <span class="nt">&lt;IncludeAssets&gt;</span>runtime; build; native; contentfiles; analyzers<span class="nt">&lt;/IncludeAssets&gt;</span>
<span class="nt">&lt;/PackageReference&gt;</span>
</code></pre></div></div>

<h3 id="tunit-generator">TUnit Generator</h3>

<p>For TUnit-based projects, use this package reference:</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;PackageReference</span> <span class="na">Include=</span><span class="s">"HttpTestGen.TUnitGenerator"</span> <span class="na">Version=</span><span class="s">"1.0.0"</span><span class="nt">&gt;</span>
  <span class="nt">&lt;PrivateAssets&gt;</span>all<span class="nt">&lt;/PrivateAssets&gt;</span>
  <span class="nt">&lt;IncludeAssets&gt;</span>runtime; build; native; contentfiles; analyzers<span class="nt">&lt;/IncludeAssets&gt;</span>
<span class="nt">&lt;/PackageReference&gt;</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">PrivateAssets="all"</code> ensures that the source generator is only used at compile time and doesn’t become a runtime dependency of your application.</p>

<h2 id="creating-http-files-in-visual-studio">Creating .http Files in Visual Studio</h2>

<p>Visual Studio provides excellent support for <code class="language-plaintext highlighter-rouge">.http</code> files, making it easy to design and test your APIs interactively before converting them into automated tests.</p>

<h3 id="basic-http-file-syntax">Basic .http File Syntax</h3>

<p>Create a <code class="language-plaintext highlighter-rouge">.http</code> file in your test project with HTTP requests. Here’s an example covering common scenarios:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code># Simple GET request
GET https://api.example.com/users

# GET request with headers
GET https://api.example.com/users/123
Accept: application/json
Authorization: Bearer your-token-here

# POST request with JSON body
POST https://api.example.com/users
Content-Type: application/json

{
  "name": "John Doe",
  "email": "john@example.com"
}

# Request with expected status code
GET https://api.example.com/nonexistent
EXPECTED_RESPONSE_STATUS 404

# Request with expected response headers
GET https://api.example.com/data
EXPECTED_RESPONSE_HEADER content-type: application/json
EXPECTED_RESPONSE_HEADER x-custom-header: custom-value
</code></pre></div></div>

<h3 id="visual-studio-integration">Visual Studio Integration</h3>

<p>When working with <code class="language-plaintext highlighter-rouge">.http</code> files in Visual Studio, you can:</p>

<ol>
  <li><strong>Test manually</strong>: Use the “Send Request” button to execute requests directly from the editor</li>
  <li><strong>View responses</strong>: See formatted JSON, XML, and text responses inline</li>
  <li><strong>Debug requests</strong>: Inspect headers, status codes, and response times</li>
  <li><strong>Environment support</strong>: Use variables for different environments (development, staging, production)</li>
</ol>

<p><img src="/assets/images/httptestgen-vs.png" alt="Visual Studio Integration" /></p>

<h2 id="assertion-keywords">Assertion Keywords</h2>

<p>HttpTestGen implements powerful assertion keywords that allow you to validate API responses automatically. These assertions are embedded directly in your <code class="language-plaintext highlighter-rouge">.http</code> files and become part of the generated test code.</p>

<h3 id="status-code-assertions">Status Code Assertions</h3>

<p>Use <code class="language-plaintext highlighter-rouge">EXPECTED_RESPONSE_STATUS</code> to validate HTTP status codes:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code># Test successful response
GET https://api.example.com/users
EXPECTED_RESPONSE_STATUS 200

# Test not found scenario
GET https://api.example.com/notfound
EXPECTED_RESPONSE_STATUS 404

# Test authentication failure
GET https://api.example.com/protected
Authorization: Bearer invalid-token
EXPECTED_RESPONSE_STATUS 401
</code></pre></div></div>

<h3 id="header-assertions">Header Assertions</h3>

<p>Use <code class="language-plaintext highlighter-rouge">EXPECTED_RESPONSE_HEADER</code> to validate response headers:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code># Validate content type and caching headers
GET https://api.example.com/api/data
EXPECTED_RESPONSE_HEADER content-type: application/json
EXPECTED_RESPONSE_HEADER cache-control: no-cache

# Validate custom security headers
GET https://api.example.com/secure-endpoint
EXPECTED_RESPONSE_HEADER x-security-token: required
EXPECTED_RESPONSE_HEADER x-rate-limit-remaining: *
</code></pre></div></div>

<h2 id="generated-test-code">Generated Test Code</h2>

<p>The source generator automatically creates test methods from your <code class="language-plaintext highlighter-rouge">.http</code> files. The generated code is clean, readable, and follows testing best practices.</p>

<h3 id="xunit-output-example">xUnit Output Example</h3>

<p>For the <code class="language-plaintext highlighter-rouge">.http</code> file examples above, HttpTestGen would generate:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">class</span> <span class="nc">ApiTestsXunitTests</span>
<span class="p">{</span>
    <span class="p">[</span><span class="n">Xunit</span><span class="p">.</span><span class="n">Fact</span><span class="p">]</span>
    <span class="k">public</span> <span class="k">async</span> <span class="n">Task</span> <span class="nf">get_api_example_com_0</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="kt">var</span> <span class="n">sut</span> <span class="p">=</span> <span class="k">new</span> <span class="n">System</span><span class="p">.</span><span class="n">Net</span><span class="p">.</span><span class="n">Http</span><span class="p">.</span><span class="nf">HttpClient</span><span class="p">();</span>
        <span class="kt">var</span> <span class="n">response</span> <span class="p">=</span> <span class="k">await</span> <span class="n">sut</span><span class="p">.</span><span class="nf">GetAsync</span><span class="p">(</span><span class="s">"https://api.example.com/users"</span><span class="p">);</span>
        <span class="n">Xunit</span><span class="p">.</span><span class="n">Assert</span><span class="p">.</span><span class="nf">True</span><span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="n">IsSuccessStatusCode</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="p">[</span><span class="n">Xunit</span><span class="p">.</span><span class="n">Fact</span><span class="p">]</span>
    <span class="k">public</span> <span class="k">async</span> <span class="n">Task</span> <span class="nf">get_api_example_com_1</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="kt">var</span> <span class="n">sut</span> <span class="p">=</span> <span class="k">new</span> <span class="n">System</span><span class="p">.</span><span class="n">Net</span><span class="p">.</span><span class="n">Http</span><span class="p">.</span><span class="nf">HttpClient</span><span class="p">();</span>
        <span class="kt">var</span> <span class="n">request</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">HttpRequestMessage</span><span class="p">(</span><span class="n">HttpMethod</span><span class="p">.</span><span class="n">Get</span><span class="p">,</span> <span class="s">"https://api.example.com/users/123"</span><span class="p">);</span>
        <span class="n">request</span><span class="p">.</span><span class="n">Headers</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="s">"Accept"</span><span class="p">,</span> <span class="s">"application/json"</span><span class="p">);</span>
        <span class="n">request</span><span class="p">.</span><span class="n">Headers</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="s">"Authorization"</span><span class="p">,</span> <span class="s">"Bearer your-token-here"</span><span class="p">);</span>
        
        <span class="kt">var</span> <span class="n">response</span> <span class="p">=</span> <span class="k">await</span> <span class="n">sut</span><span class="p">.</span><span class="nf">SendAsync</span><span class="p">(</span><span class="n">request</span><span class="p">);</span>
        <span class="n">Xunit</span><span class="p">.</span><span class="n">Assert</span><span class="p">.</span><span class="nf">True</span><span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="n">IsSuccessStatusCode</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="p">[</span><span class="n">Xunit</span><span class="p">.</span><span class="n">Fact</span><span class="p">]</span>
    <span class="k">public</span> <span class="k">async</span> <span class="n">Task</span> <span class="nf">post_api_example_com_2</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="kt">var</span> <span class="n">sut</span> <span class="p">=</span> <span class="k">new</span> <span class="n">System</span><span class="p">.</span><span class="n">Net</span><span class="p">.</span><span class="n">Http</span><span class="p">.</span><span class="nf">HttpClient</span><span class="p">();</span>
        <span class="kt">var</span> <span class="n">content</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">StringContent</span><span class="p">(</span><span class="s">"{\"name\":\"John Doe\",\"email\":\"john@example.com\"}"</span><span class="p">,</span> 
            <span class="n">System</span><span class="p">.</span><span class="n">Text</span><span class="p">.</span><span class="n">Encoding</span><span class="p">.</span><span class="n">UTF8</span><span class="p">,</span> <span class="s">"application/json"</span><span class="p">);</span>
        
        <span class="kt">var</span> <span class="n">response</span> <span class="p">=</span> <span class="k">await</span> <span class="n">sut</span><span class="p">.</span><span class="nf">PostAsync</span><span class="p">(</span><span class="s">"https://api.example.com/users"</span><span class="p">,</span> <span class="n">content</span><span class="p">);</span>
        <span class="n">Xunit</span><span class="p">.</span><span class="n">Assert</span><span class="p">.</span><span class="nf">True</span><span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="n">IsSuccessStatusCode</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="p">[</span><span class="n">Xunit</span><span class="p">.</span><span class="n">Fact</span><span class="p">]</span>
    <span class="k">public</span> <span class="k">async</span> <span class="n">Task</span> <span class="nf">get_api_example_com_3</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="kt">var</span> <span class="n">sut</span> <span class="p">=</span> <span class="k">new</span> <span class="n">System</span><span class="p">.</span><span class="n">Net</span><span class="p">.</span><span class="n">Http</span><span class="p">.</span><span class="nf">HttpClient</span><span class="p">();</span>
        <span class="kt">var</span> <span class="n">response</span> <span class="p">=</span> <span class="k">await</span> <span class="n">sut</span><span class="p">.</span><span class="nf">GetAsync</span><span class="p">(</span><span class="s">"https://api.example.com/nonexistent"</span><span class="p">);</span>
        <span class="n">Xunit</span><span class="p">.</span><span class="n">Assert</span><span class="p">.</span><span class="nf">Equal</span><span class="p">(</span><span class="m">404</span><span class="p">,</span> <span class="p">(</span><span class="kt">int</span><span class="p">)</span><span class="n">response</span><span class="p">.</span><span class="n">StatusCode</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="p">[</span><span class="n">Xunit</span><span class="p">.</span><span class="n">Fact</span><span class="p">]</span>
    <span class="k">public</span> <span class="k">async</span> <span class="n">Task</span> <span class="nf">get_api_example_com_4</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="kt">var</span> <span class="n">sut</span> <span class="p">=</span> <span class="k">new</span> <span class="n">System</span><span class="p">.</span><span class="n">Net</span><span class="p">.</span><span class="n">Http</span><span class="p">.</span><span class="nf">HttpClient</span><span class="p">();</span>
        <span class="kt">var</span> <span class="n">response</span> <span class="p">=</span> <span class="k">await</span> <span class="n">sut</span><span class="p">.</span><span class="nf">GetAsync</span><span class="p">(</span><span class="s">"https://api.example.com/data"</span><span class="p">);</span>
        <span class="n">Xunit</span><span class="p">.</span><span class="n">Assert</span><span class="p">.</span><span class="nf">True</span><span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="n">IsSuccessStatusCode</span><span class="p">);</span>
        <span class="n">Xunit</span><span class="p">.</span><span class="n">Assert</span><span class="p">.</span><span class="nf">True</span><span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="n">Headers</span><span class="p">.</span><span class="nf">GetValues</span><span class="p">(</span><span class="s">"content-type"</span><span class="p">).</span><span class="nf">Contains</span><span class="p">(</span><span class="s">"application/json"</span><span class="p">));</span>
        <span class="n">Xunit</span><span class="p">.</span><span class="n">Assert</span><span class="p">.</span><span class="nf">True</span><span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="n">Headers</span><span class="p">.</span><span class="nf">GetValues</span><span class="p">(</span><span class="s">"x-custom-header"</span><span class="p">).</span><span class="nf">Contains</span><span class="p">(</span><span class="s">"custom-value"</span><span class="p">));</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="generated-test-features">Generated Test Features</h3>

<p>The generated tests include:</p>

<ul>
  <li><strong>Proper HTTP client usage</strong>: Each test method creates and uses an <code class="language-plaintext highlighter-rouge">HttpClient</code> instance</li>
  <li><strong>Header handling</strong>: Request headers are properly added to HTTP requests</li>
  <li><strong>Content management</strong>: Request bodies are converted to appropriate <code class="language-plaintext highlighter-rouge">HttpContent</code> types</li>
  <li><strong>Assertion integration</strong>: Expected status codes and headers become proper test assertions</li>
  <li><strong>Async/await patterns</strong>: All HTTP operations use proper async patterns</li>
</ul>

<h2 id="project-integration">Project Integration</h2>

<p>Integrating HttpTestGen into your project is seamless and requires minimal configuration.</p>

<h3 id="example-project-structure">Example Project Structure</h3>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>MyProject.Tests/
├── MyProject.Tests.csproj
├── api-tests.http
├── user-tests.http
└── integration-tests.http
</code></pre></div></div>

<p>The source generator will automatically create corresponding test classes:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">api-tests.http</code> → <code class="language-plaintext highlighter-rouge">ApiTestsXunitTests</code> or <code class="language-plaintext highlighter-rouge">ApiTestsTests</code> (TUnit)</li>
  <li><code class="language-plaintext highlighter-rouge">user-tests.http</code> → <code class="language-plaintext highlighter-rouge">UserTestsXunitTests</code> or <code class="language-plaintext highlighter-rouge">UserTestsTests</code> (TUnit)</li>
  <li><code class="language-plaintext highlighter-rouge">integration-tests.http</code> → <code class="language-plaintext highlighter-rouge">IntegrationTestsXunitTests</code> or <code class="language-plaintext highlighter-rouge">IntegrationTestsTests</code> (TUnit)</li>
</ul>

<h3 id="development-workflow">Development Workflow</h3>

<p>The typical development flow with HttpTestGen is:</p>

<ol>
  <li><strong>Design your API</strong> using <code class="language-plaintext highlighter-rouge">.http</code> files in your IDE</li>
  <li><strong>Test manually</strong> using REST Client extensions or Visual Studio’s built-in HTTP client</li>
  <li><strong>Add assertions</strong> for expected behavior using the assertion keywords</li>
  <li><strong>Build project</strong> to generate automated tests</li>
  <li><strong>Run tests</strong> in CI/CD pipeline</li>
</ol>

<h3 id="running-tests">Running Tests</h3>

<p>Once your project is built, you can run the generated tests using standard .NET testing tools:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Run all tests</span>
dotnet <span class="nb">test</span>

<span class="c"># Run tests with verbose output</span>
dotnet <span class="nb">test</span> <span class="nt">--logger</span>:console<span class="p">;</span><span class="nv">verbosity</span><span class="o">=</span>detailed

<span class="c"># Run specific test class</span>
dotnet <span class="nb">test</span> <span class="nt">--filter</span> <span class="s2">"ApiTestsXunitTests"</span>
</code></pre></div></div>

<p><img src="/assets/images/httptestgen-cli.png" alt="Running Tests in Terminal" /></p>

<h2 id="advanced-features">Advanced Features</h2>

<p>HttpTestGen supports several advanced features for complex testing scenarios.</p>

<h3 id="multiple-request-bodies">Multiple Request Bodies</h3>

<p>The tool supports various content types for request bodies:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code># JSON body
POST https://api.example.com/data
Content-Type: application/json

{
  "key": "value"
}

# XML body  
POST https://api.example.com/data
Content-Type: application/xml

&lt;root&gt;
  &lt;item&gt;value&lt;/item&gt;
&lt;/root&gt;

# Plain text body
POST https://api.example.com/data
Content-Type: text/plain

This is plain text content
</code></pre></div></div>

<h3 id="comments-and-documentation">Comments and Documentation</h3>

<p>Use <code class="language-plaintext highlighter-rouge">#</code> for comments in your <code class="language-plaintext highlighter-rouge">.http</code> files to document your tests:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code># This tests user authentication
POST https://api.example.com/auth/login
Content-Type: application/json

{
  "username": "testuser",
  "password": "testpass"
}

# Verify successful login returns token
EXPECTED_RESPONSE_STATUS 200
EXPECTED_RESPONSE_HEADER content-type: application/json
</code></pre></div></div>

<h3 id="multiple-requests">Multiple Requests</h3>

<p>Separate multiple requests with blank lines or comments:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>GET https://api.example.com/users

# Test user creation
POST https://api.example.com/users
Content-Type: application/json

{
  "name": "Test User"
}

# Test user deletion
DELETE https://api.example.com/users/123
</code></pre></div></div>

<h2 id="best-practices">Best Practices</h2>

<h3 id="organization-and-structure">Organization and Structure</h3>

<ul>
  <li><strong>Organize by feature</strong>: Create separate <code class="language-plaintext highlighter-rouge">.http</code> files for different API endpoints or features</li>
  <li><strong>Use descriptive comments</strong>: Document what each request tests</li>
  <li><strong>Add assertions</strong>: Always include expected status codes and important headers</li>
  <li><strong>Environment variables</strong>: Use your IDE’s environment variable support for different environments</li>
  <li><strong>Version control</strong>: Commit your <code class="language-plaintext highlighter-rouge">.http</code> files alongside your code</li>
</ul>

<h3 id="testing-strategies">Testing Strategies</h3>

<p><strong>Unit Tests vs Integration Tests</strong>:</p>

<ul>
  <li>Unit Tests: Test individual endpoints with mocked dependencies</li>
  <li>Integration Tests: Test complete API flows with real HTTP calls</li>
  <li>Contract Tests: Verify API contracts match expectations</li>
</ul>

<p><strong>Assertion Patterns</strong>:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code># Success scenarios
GET https://api.example.com/users
EXPECTED_RESPONSE_STATUS 200
EXPECTED_RESPONSE_HEADER content-type: application/json

# Error scenarios  
GET https://api.example.com/users/999999
EXPECTED_RESPONSE_STATUS 404

# Authentication scenarios
GET https://api.example.com/protected
Authorization: Bearer invalid-token
EXPECTED_RESPONSE_STATUS 401
</code></pre></div></div>

<h2 id="performance-considerations">Performance Considerations</h2>

<p>HttpTestGen is designed for optimal performance:</p>

<ul>
  <li><strong>Compile-time generation</strong>: Zero runtime overhead</li>
  <li><strong>Incremental builds</strong>: Only regenerates when <code class="language-plaintext highlighter-rouge">.http</code> files change</li>
  <li><strong>Parallel execution</strong>: Generated tests can run in parallel</li>
  <li><strong>Memory efficient</strong>: No reflection or dynamic compilation at runtime</li>
</ul>

<h2 id="integration-with-popular-tools">Integration with Popular Tools</h2>

<h3 id="visual-studio-code">Visual Studio Code</h3>

<p>HttpTestGen works seamlessly with the <a href="https://marketplace.visualstudio.com/items?itemName=humao.rest-client">REST Client extension</a> for Visual Studio Code, allowing you to design and test APIs interactively before generating automated tests.</p>

<h3 id="jetbrains-ides">JetBrains IDEs</h3>

<p>The tool is compatible with the built-in HTTP client in IntelliJ IDEA, WebStorm, and Rider, providing a consistent experience across different development environments.</p>

<h3 id="cicd-integration">CI/CD Integration</h3>

<p>Generated tests integrate naturally with CI/CD pipelines:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Example GitHub Actions workflow</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Run API Tests</span>
  <span class="na">run</span><span class="pi">:</span> <span class="s">dotnet test --filter "HttpTestGen" --logger trx</span>
</code></pre></div></div>

<h2 id="conclusion">Conclusion</h2>

<p><a href="https://github.com/christianhelle/httptestgen">HttpTestGen</a> represents a significant step forward in .NET API testing, bridging the gap between manual API testing and automated test suites. By leveraging the familiar <code class="language-plaintext highlighter-rouge">.http</code> file format that developers already use for interactive API testing, HttpTestGen eliminates the friction between designing APIs and testing them comprehensively.</p>

<p>The tool’s source generator approach ensures zero runtime overhead while providing powerful assertion capabilities that validate not just connectivity, but the correctness of API responses. Whether you’re building microservices, REST APIs, or integration tests, HttpTestGen fits naturally into your development workflow.</p>

<p>The combination of Visual Studio’s excellent <code class="language-plaintext highlighter-rouge">.http</code> file support and HttpTestGen’s automatic test generation creates a seamless experience from API design to automated testing. Your <code class="language-plaintext highlighter-rouge">.http</code> files become living documentation of your API contracts while simultaneously serving as comprehensive test suites.</p>

<p>Give HttpTestGen a try in your next .NET project and experience how it can streamline your API testing workflow. The tool is open source and available on <a href="https://github.com/christianhelle/httptestgen">GitHub</a>, where you can contribute, report issues, or explore the implementation details.</p>

<p>Whether you’re just getting started with API testing or looking to improve your existing testing strategy, HttpTestGen provides a powerful, lightweight solution that grows with your project’s needs.</p>]]></content><author><name>Christian Helle</name></author><category term=".NET" /><category term="HTTP" /><summary type="html"><![CDATA[I’m excited to introduce HttpTestGen, a powerful .NET source generator that automatically converts .http files into fully functional C# test code. This innovative tool bridges the gap between API testing in IDEs (like Visual Studio Code with the REST Client extension) and automated testing in your .NET projects.]]></summary></entry><entry><title type="html">Run .http files from the command line using a fast, small, single binary tool</title><link href="https://christianhelle.com/2025/06/http-file-runner-zig-tool.html" rel="alternate" type="text/html" title="Run .http files from the command line using a fast, small, single binary tool" /><published>2025-06-21T00:00:00+00:00</published><updated>2025-06-21T00:00:00+00:00</updated><id>https://christianhelle.com/2025/06/http-file-runner-zig-tool</id><content type="html" xml:base="https://christianhelle.com/2025/06/http-file-runner-zig-tool.html"><![CDATA[<p>I’m excited to share my latest project: <a href="https://github.com/christianhelle/httprunner">HTTP File Runner</a>, a command-line tool written in Zig that parses <code class="language-plaintext highlighter-rouge">.http</code> files and executes HTTP requests. This tool provides colored output to indicate success or failure, making it easy to test APIs and web services directly from your terminal.</p>

<p>This project started as a learning exercise to explore the Zig programming language’s capabilities. In a previous post, entitled <a href="/2023/11/http-file-generator.html">Generate .http files from OpenAPI specifications</a>, I wrote about <a href="https://github.com/christianhelle/httpgenerator">HTTP File Generator</a>, a tool that generates <code class="language-plaintext highlighter-rouge">.http</code> files from OpenAPI specifications. It felt natural to create a companion tool that could execute these generated files outside an IDE. The combination of these two tools creates a complete workflow: generate HTTP files from your API specifications, then run them from the command line for testing and validation.</p>

<h2 id="what-is-http-file-runner">What is HTTP File Runner?</h2>

<p><a href="https://github.com/christianhelle/httprunner">HTTP File Runner</a> is a simple yet powerful tool that reads <code class="language-plaintext highlighter-rouge">.http</code> files (the same format used by popular IDEs like Visual Studio Code, JetBrains IDEs, and Visual Studio 2022) and executes the HTTP requests defined within them. It’s designed to be fast, reliable, and developer-friendly.</p>

<p>The beauty of this approach lies in its simplicity. <code class="language-plaintext highlighter-rouge">.http</code> files are plain text files that can be generated, version-controlled, shared between team members, and executed across different environments. These files are human-readable and can be edited with any text editor. This makes them perfect for documentation, on-boarding new team members, and ensuring consistency across development environments.</p>

<h3 id="key-features">Key Features</h3>

<p>The tool comes packed with features that make API testing a breeze, each designed to address common pain points in API development and testing workflows:</p>

<ul>
  <li><strong>Parse and execute HTTP requests</strong> from <code class="language-plaintext highlighter-rouge">.http</code> files - The core functionality that reads the standard HTTP file format and executes requests sequentially</li>
  <li><strong>Support for multiple files</strong> - Run several <code class="language-plaintext highlighter-rouge">.http</code> files in a single command, perfect for organizing tests by feature or service</li>
  <li><strong>Discovery mode</strong> - Recursively find and run all <code class="language-plaintext highlighter-rouge">.http</code> files in a directory tree, ideal for comprehensive test suites</li>
  <li><strong>Verbose mode</strong> for detailed request and response information - See exactly what’s being sent and received, invaluable for debugging</li>
  <li><strong>Logging mode</strong> to save all output to a file for analysis and reporting - Essential for CI/CD pipelines and audit trails</li>
  <li><strong>Color-coded output</strong> (green for success, red for failure) - Immediate visual feedback on test results</li>
  <li><strong>Summary statistics</strong> showing success/failure counts per file and overall - Quick overview of test suite health</li>
  <li><strong>Support for various HTTP methods</strong> (GET, POST, PUT, DELETE, PATCH) - Covers all standard REST operations</li>
  <li><strong>Variables support</strong> with substitution in URLs, headers, and request bodies - Enables dynamic and reusable test scenarios</li>
  <li><strong>Response assertions</strong> for status codes, body content, and headers - Automated validation of API responses</li>
</ul>

<p>These features work together to create a comprehensive testing solution that scales from simple smoke tests to complex integration test suites. The combination of batch processing, detailed reporting, and assertion capabilities makes it suitable for both development-time testing and production monitoring.</p>

<h2 id="why-zig">Why Zig?</h2>

<p>Choosing Zig for this project was intentional. This started as a learning exercise to explore the Zig programming language. In fact, choosing Zig came before even deciding what to build. I primarily work with higher-level languages like C# in my day job, and I wanted to understand what Zig brings to the table. The decision was an excellent choice for several compelling reasons:</p>

<h3 id="performance">Performance</h3>

<p>Zig compiles to highly optimized native code without the overhead of a runtime or garbage collector. This means HTTP File Runner starts instantly and processes requests with minimal memory footprint.</p>

<p>The resulting binary is small (under 2MB), starts instantly, and handles network operations efficiently. The cross-platform support means teams can use the same tool regardless of their development environment, reducing friction and improving consistency.</p>

<h3 id="memory-management">Memory management</h3>

<p>Zig gives you explicit control over memory allocation and deallocation (similar to C/C++), but with helpful features to reduce errors. One standout is Zig’s <code class="language-plaintext highlighter-rouge">defer</code> statement, which ensures resources are released when a scope exits—making it easy to prevent memory leaks and resource leaks. While Zig does not provide full memory safety guarantees, its compile-time checks, clear ownership model, and <code class="language-plaintext highlighter-rouge">defer</code> help you write reliable low-level code more safely than traditional C.</p>

<h3 id="cross-platform">Cross-platform</h3>

<p>Zig’s excellent cross-compilation support made it trivial to build binaries for Linux, MacOS, and Windows from a single codebase. The build system handles platform-specific details seamlessly, which is essential for a tool that needs to work everywhere developers do.</p>

<h3 id="simple-syntax">Simple syntax</h3>

<p>Zig’s philosophy of “no hidden control flow” means the code does exactly what it appears to do. This makes the codebase easier to understand, debug, and maintain. Coming from higher-level languages, I appreciated how Zig forces you to be explicit about your intentions.</p>

<h3 id="no-runtime">No runtime</h3>

<p>Zero-cost abstractions and no garbage collector mean predictable performance characteristics. This is particularly important for a command-line tool that might be called thousands of times in automated testing scenarios.</p>

<h3 id="great-editor-support">Great editor support</h3>

<p>Zig’s Language Server Protocol (LSP) implementation provides excellent code completion, diagnostics, and navigation features in editors like Visual Studio Code or Neovim. This made development smooth and productive, with real-time feedback and powerful refactoring tools available out of the box.</p>

<h3 id="learning-value">Learning value</h3>

<p>As someone whose day job is working in managed languages, exploring manual memory management and system-level programming concepts in Zig provided valuable insights.</p>

<p>Compared to Rust, I found Zig’s learning curve to be less steep, and more fun to write. Zig offers a straightforward syntax which made it easier to grasp the core concepts quickly. While both languages provide low-level control and strong performance, Zig’s simplicity and explicitness helped me become productive faster, making it an appealing choice for systems programming projects like this one.</p>

<h3 id="the-zig-zen">The Zig Zen</h3>

<p>Zig’s development philosophy, known as “The Zen of Zig,” states the following:</p>

<ul>
  <li>Communicate intent precisely.</li>
  <li>Edge cases matter.</li>
  <li>Favor reading code over writing code.</li>
  <li>Only one obvious way to do things.</li>
  <li>Runtime crashes are better than bugs.</li>
  <li>Compile errors are better than runtime crashes.</li>
  <li>Incremental improvements.</li>
  <li>Avoid local maximums.</li>
  <li>Reduce the amount one must remember.</li>
  <li>Focus on code rather than style.</li>
  <li>Resource allocation may fail; resource deallocation must succeed.</li>
  <li>Memory is a resource.</li>
  <li>Together we serve the users.</li>
</ul>

<h2 id="installation">Installation</h2>

<p>Getting started is incredibly easy. The tool provides multiple installation options to suit different preferences and environments. I’ve focused on making the installation process as frictionless as possible, recognizing that developers often need to get tools up and running quickly.</p>

<h3 id="quick-install-recommended">Quick Install (Recommended)</h3>

<p>The fastest way to get started is using the automated installation scripts. These scripts handle platform detection, architecture identification, and PATH configuration automatically:</p>

<p><strong>Linux/MacOS:</strong></p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-fsSL</span> https://christianhelle.com/httprunner/install | bash
</code></pre></div></div>

<p><strong>Windows (PowerShell):</strong></p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">irm</span><span class="w"> </span><span class="nx">https://christianhelle.com/httprunner/install.ps1</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">iex</span><span class="w">
</span></code></pre></div></div>

<p>These scripts will automatically:</p>

<ul>
  <li>Detect your operating system and CPU architecture</li>
  <li>Download the appropriate binary from the latest GitHub release</li>
  <li>Install it to a standard location (<code class="language-plaintext highlighter-rouge">/usr/local/bin</code> on Unix-like systems, <code class="language-plaintext highlighter-rouge">$HOME/.local/bin</code> as fallback)</li>
  <li>Optionally add the installation directory to your PATH</li>
  <li>Verify the installation was successful</li>
</ul>

<h3 id="other-installation-methods">Other Installation Methods</h3>

<p>For users who prefer different installation approaches or have specific requirements:</p>

<ul>
  <li><strong>Snap Store</strong>: <code class="language-plaintext highlighter-rouge">snap install httprunner</code> - Great for Ubuntu and other snap-enabled distributions, provides automatic updates</li>
  <li><strong>Manual Download</strong>: Download from <a href="https://github.com/christianhelle/httprunner/releases/tag/0.3.33">GitHub Releases</a> - Full control over installation location and process</li>
  <li><strong>Docker</strong>: <code class="language-plaintext highlighter-rouge">docker pull christianhelle/httprunner</code> - Perfect for containerized environments or when you don’t want to install binaries locally</li>
  <li><strong>Build from source</strong>: Clone the repo and run <code class="language-plaintext highlighter-rouge">zig build</code> - For developers who want to customize the build or contribute to the project</li>
</ul>

<p>Each method has its advantages: Snap provides automatic updates, manual download gives you full control, Docker ensures isolation, and building from source allows customization. Choose the method that best fits your workflow and security requirements.</p>

<h2 id="usage-examples">Usage Examples</h2>

<p>Here are some common usage patterns that demonstrate the tool’s flexibility and power. These examples progress from simple single-file execution to complex batch processing scenarios:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Run a single .http file</span>
httprunner api-tests.http

<span class="c"># Run with verbose output</span>
httprunner api-tests.http <span class="nt">--verbose</span>

<span class="c"># Run multiple files</span>
httprunner auth.http users.http posts.http

<span class="c"># Discover and run all .http files recursively</span>
httprunner <span class="nt">--discover</span>

<span class="c"># Save output to a log file</span>
httprunner api-tests.http <span class="nt">--log</span> results.txt

<span class="c"># Combine verbose mode with logging</span>
httprunner <span class="nt">--discover</span> <span class="nt">--verbose</span> <span class="nt">--log</span> full-test-report.log
</code></pre></div></div>

<h3 id="real-world-scenarios">Real-World Scenarios</h3>

<p><strong>Development Workflow</strong>: During development, you might run <code class="language-plaintext highlighter-rouge">httprunner --discover --verbose</code> to execute all tests in your project and see detailed output for debugging.</p>

<p><strong>CI/CD Integration</strong>: In your build pipeline, use <code class="language-plaintext highlighter-rouge">httprunner --discover --log test-results.log</code> to run all tests and capture results for build reports.</p>

<p><strong>Environment Testing</strong>: When deploying to a new environment, run <code class="language-plaintext highlighter-rouge">httprunner health-checks.http --env production --log deployment-validation.log</code> to verify everything is working correctly.</p>

<p><strong>Performance Monitoring</strong>: Set up automated runs with <code class="language-plaintext highlighter-rouge">httprunner monitoring.http --log performance.log</code> to track API performance over time.</p>

<h2 id="http-file-format">HTTP File Format</h2>

<p>The tool supports the well known <code class="language-plaintext highlighter-rouge">.http</code> file format:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code># Comments start with #

# Basic GET request
GET https://api.github.com/users/octocat

# Request with headers
GET https://httpbin.org/headers
User-Agent: HttpRunner/1.0
Accept: application/json

# POST request with body
POST https://httpbin.org/post
Content-Type: application/json

{
  "name": "test",
  "value": 123
}
</code></pre></div></div>

<h2 id="variables-and-environment-support">Variables and Environment Support</h2>

<p>One of the most powerful features is the comprehensive variable support system, which enables you to create flexible, reusable test suites that work across different environments. This feature addresses one of the biggest pain points in API testing: managing different configurations for development, staging, and production environments.</p>

<h3 id="basic-variable-usage">Basic Variable Usage</h3>

<p>Variables are defined using the <code class="language-plaintext highlighter-rouge">@</code> syntax and referenced with double curly braces:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>@hostname=localhost
@port=8080
@baseUrl=https://:

GET /api/users
Authorization: Bearer 
</code></pre></div></div>

<h3 id="advanced-variable-composition">Advanced Variable Composition</h3>

<p>Variables can be composed of other variables, allowing you to build complex configurations incrementally:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>@protocol=https
@hostname=api.example.com
@port=443
@version=v1
@baseUrl=://:/

# Now you can use the composed URL
GET /users
GET /posts
GET /comments
</code></pre></div></div>

<h3 id="environment-configuration-files">Environment Configuration Files</h3>

<p>For managing different environments, you can create an <code class="language-plaintext highlighter-rouge">http-client.env.json</code> file with environment-specific values:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"dev"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"HostAddress"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://localhost:44320"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"ApiKey"</span><span class="p">:</span><span class="w"> </span><span class="s2">"dev-api-key-123"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"Environment"</span><span class="p">:</span><span class="w"> </span><span class="s2">"development"</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"staging"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"HostAddress"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://staging.example.com"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"ApiKey"</span><span class="p">:</span><span class="w"> </span><span class="s2">"staging-api-key-456"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"Environment"</span><span class="p">:</span><span class="w"> </span><span class="s2">"staging"</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"prod"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"HostAddress"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://api.example.com"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"ApiKey"</span><span class="p">:</span><span class="w"> </span><span class="s2">"prod-api-key-789"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"Environment"</span><span class="p">:</span><span class="w"> </span><span class="s2">"production"</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Then specify the environment when running tests:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>httprunner api-tests.http <span class="nt">--env</span> dev
</code></pre></div></div>

<p>This approach allows you to maintain a single set of test files while easily switching between different environments. The variable override behavior is intelligent: environment variables are loaded first, then any variables defined in the <code class="language-plaintext highlighter-rouge">.http</code> file override them, giving you both flexibility and control.</p>

<h2 id="response-assertions">Response Assertions</h2>

<p>The response assertion system is one of HTTP File Runner’s most powerful features, transforming it from a simple request executor into a comprehensive API testing framework. This system enables you to validate not just that your API responds, but that it responds correctly with the expected data, status codes, and headers. By incorporating assertions into your <code class="language-plaintext highlighter-rouge">.http</code> files, you can create robust test suites that catch regressions, validate API contracts, and ensure your services behave correctly across different environments.</p>

<h3 id="understanding-assertion-philosophy">Understanding Assertion Philosophy</h3>

<p>HTTP File Runner’s assertion system is designed around the principle of explicit validation. Rather than assuming that any response is acceptable, assertions force you to define what constitutes a successful interaction with your API. This approach catches subtle bugs that might otherwise go unnoticed, such as:</p>

<ul>
  <li>APIs returning 200 status codes with error messages in the body</li>
  <li>Correct data with unexpected content types or encoding</li>
  <li>Missing or incorrect security headers</li>
  <li>Performance regressions indicated by unexpected response patterns</li>
</ul>

<h3 id="types-of-assertions">Types of Assertions</h3>

<h4 id="status-code-assertions">Status Code Assertions</h4>

<p>Status code assertions are the foundation of API testing, ensuring your endpoints return the correct HTTP status codes for different scenarios:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code># Test successful resource retrieval
GET https://httpbin.org/status/200
EXPECTED_RESPONSE_STATUS 200

# Test resource not found scenario
GET https://httpbin.org/status/404
EXPECTED_RESPONSE_STATUS 404
</code></pre></div></div>

<p>Status code assertions are particularly valuable for testing error conditions and edge cases. You can verify that your API correctly returns 400 for bad requests, 401 for unauthorized access, 403 for forbidden operations, and 404 for missing resources.</p>

<h4 id="response-body-assertions">Response Body Assertions</h4>

<p>Response body assertions validate the actual content returned by your API. These assertions use substring matching, making them flexible enough to work with various response formats while being specific enough to catch content errors:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code># Test JSON response content
GET https://httpbin.org/json
EXPECTED_RESPONSE_STATUS 200
EXPECTED_RESPONSE_BODY "slideshow"
EXPECTED_RESPONSE_BODY "Sample Slide Show"

# Test API response structure
GET https://jsonplaceholder.typicode.com/users/1
EXPECTED_RESPONSE_STATUS 200
EXPECTED_RESPONSE_BODY "Leanne Graham"
EXPECTED_RESPONSE_BODY "@hildegard.org"
</code></pre></div></div>

<p>Body assertions are case-sensitive and look for exact substring matches. This approach allows you to validate specific field values in JSON responses, error messages, HTML content, XML elements, and plain text responses.</p>

<h4 id="response-header-assertions">Response Header Assertions</h4>

<p>Header assertions validate the metadata returned with your API responses. These are crucial for testing security headers, content types, caching directives, and custom application headers:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code># Test content type and security headers
GET https://httpbin.org/json
EXPECTED_RESPONSE_STATUS 200
EXPECTED_RESPONSE_HEADERS "Content-Type: application/json"

# Test custom application headers
POST https://httpbin.org/post
Content-Type: application/json

{"test": "data"}

EXPECTED_RESPONSE_STATUS 200
EXPECTED_RESPONSE_HEADERS "Content-Type: application/json"
EXPECTED_RESPONSE_HEADERS "Server: gunicorn"
</code></pre></div></div>

<p>Header assertions use substring matching on the full header line (including both name and value). This flexibility allows you to validate exact header values, check for header presence, test multiple values for the same header, and verify security and compliance headers.</p>

<h3 id="advanced-assertion-patterns">Advanced Assertion Patterns</h3>

<h4 id="comprehensive-api-endpoint-testing">Comprehensive API Endpoint Testing</h4>

<p>Here’s an example of thoroughly testing a user management API endpoint:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code># Test user creation with comprehensive validation
POST https://api.example.com/users
Content-Type: application/json
Authorization: Bearer 

{
  "name": "Christian Helle",
  "email": "christian.helle@example.com",
  "role": "user"
}

EXPECTED_RESPONSE_STATUS 201
EXPECTED_RESPONSE_BODY "Christian Helle"
EXPECTED_RESPONSE_BODY "christian.helle@example.com"
EXPECTED_RESPONSE_BODY "\"id\":"
EXPECTED_RESPONSE_HEADERS "Content-Type: application/json"
EXPECTED_RESPONSE_HEADERS "Location: /users/"

###

# Verify user is deleted properly
DELETE https://api.example.com/users/
Authorization: Bearer 

EXPECTED_RESPONSE_STATUS 204

###

# Confirm user no longer exists
GET https://api.example.com/users/
Authorization: Bearer 

EXPECTED_RESPONSE_STATUS 404
EXPECTED_RESPONSE_BODY "User not found"
</code></pre></div></div>

<h4 id="error-condition-testing">Error Condition Testing</h4>

<p>Testing error conditions is just as important as testing success scenarios:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code># Test validation errors
POST https://api.example.com/users
Content-Type: application/json

{
  "name": "",
  "email": "invalid-email"
}

EXPECTED_RESPONSE_STATUS 400
EXPECTED_RESPONSE_BODY "validation error"
EXPECTED_RESPONSE_BODY "name is required"
EXPECTED_RESPONSE_HEADERS "Content-Type: application/json"

###

# Test authentication errors
GET https://api.example.com/users
Authorization: Bearer invalid-token

EXPECTED_RESPONSE_STATUS 401
EXPECTED_RESPONSE_BODY "unauthorized"
EXPECTED_RESPONSE_HEADERS "WWW-Authenticate: Bearer"
</code></pre></div></div>

<h3 id="assertion-execution-and-behavior">Assertion Execution and Behavior</h3>

<h4 id="processing-order-and-logic">Processing Order and Logic</h4>

<p>When HTTP File Runner encounters assertions in a <code class="language-plaintext highlighter-rouge">.http</code> file, it follows a specific execution pattern:</p>

<ol>
  <li><strong>Request Execution</strong>: The HTTP request is sent normally, following all redirects and handling authentication</li>
  <li><strong>Response Capture</strong>: The complete response is captured, including status code, headers, and body</li>
  <li><strong>Assertion Evaluation</strong>: Each assertion is evaluated in the order it appears in the file</li>
  <li><strong>Result Aggregation</strong>: All assertion results are collected and reported</li>
  <li><strong>Request Status Determination</strong>: The request is marked as successful only if ALL assertions pass</li>
</ol>

<h4 id="enhanced-logging-and-reporting">Enhanced Logging and Reporting</h4>

<p>When assertions are present, HTTP File Runner automatically enables enhanced logging, even in non-verbose mode:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>🚀 HTTP File Runner - Processing file: .\tests\user-api.http
==================================================
Found 3 HTTP request(s)

✅ POST https://api.example.com/users - Status: 201 - 245ms
   ✅ Status assertion passed: 201
   ✅ Body assertion passed: "Christian Helle"
   ✅ Body assertion passed: "christian.helle@example.com"
   ✅ Header assertion passed: "Content-Type: application/json"
   ✅ Header assertion passed: "Location: /users/"

❌ GET https://api.example.com/users/invalid - Status: 404 - 123ms
   ✅ Status assertion passed: 404
   ❌ Body assertion failed: Expected "user not found" but got "Resource not found"
   ✅ Header assertion passed: "Content-Type: application/json"

✅ DELETE https://api.example.com/users/123 - Status: 204 - 156ms
   ✅ Status assertion passed: 204
   ✅ Header assertion passed: "X-Deleted-At:"

==================================================
File Summary: 2/3 requests succeeded
Total Assertions: 9 passed, 1 failed
</code></pre></div></div>

<h4 id="verbose-mode-enhancement">Verbose Mode Enhancement</h4>

<p>In verbose mode, HTTP File Runner provides even more detailed information about assertion evaluation:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>🚀 HTTP File Runner - Processing file: .\tests\detailed-test.http
==================================================

📤 Request: POST https://api.example.com/users
Headers:
  Content-Type: application/json
  Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...

Body:
{
  "name": "Christian Helle",
  "email": "christian.helle@example.com"
}

📥 Response: 201 Created (245ms)
Headers:
  Content-Type: application/json; charset=utf-8
  Location: /users/12345
  X-RateLimit-Remaining: 99
  Content-Length: 156

Body:
{
  "id": 12345,
  "name": "Christian Helle",
  "email": "christian.helle@example.com",
  "created_at": "2025-06-21T10:30:00Z",
  "role": "user"
}

🔍 Assertion Results:
   ✅ EXPECTED_RESPONSE_STATUS 201
      Expected: 201, Actual: 201 ✓

   ✅ EXPECTED_RESPONSE_BODY "Christian Helle"
      Found substring "Christian Helle" in response body ✓

   ✅ EXPECTED_RESPONSE_BODY "christian.helle@example.com"
      Found substring "christian.helle@example.com" in response body ✓

   ✅ EXPECTED_RESPONSE_HEADERS "Content-Type: application/json"
      Found header match "Content-Type: application/json" ✓

   ✅ EXPECTED_RESPONSE_HEADERS "Location: /users/"
      Found header match "Location: /users/" ✓

✅ Request completed successfully - All assertions passed
</code></pre></div></div>

<h3 id="best-practices-for-assertion">Best Practices for Assertion</h3>

<h4 id="start-simple-build-complex">Start Simple, Build Complex</h4>

<p>Begin with basic status code assertions and gradually add more specific validations:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code># Level 1: Basic connectivity
GET https://api.example.com/health
EXPECTED_RESPONSE_STATUS 200

# Level 2: Add content validation
GET https://api.example.com/health
EXPECTED_RESPONSE_STATUS 200
EXPECTED_RESPONSE_BODY "healthy"

# Level 3: Full contract validation
GET https://api.example.com/health
EXPECTED_RESPONSE_STATUS 200
EXPECTED_RESPONSE_BODY "healthy"
EXPECTED_RESPONSE_BODY "\"uptime\":"
EXPECTED_RESPONSE_BODY "\"version\":"
EXPECTED_RESPONSE_HEADERS "Content-Type: application/json"
</code></pre></div></div>

<h4 id="use-specific-but-resilient-assertions">Use Specific but Resilient Assertions</h4>

<p>Make your assertions specific enough to catch real issues, but resilient enough to not break on minor changes:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code># Good: Validates presence of key fields
EXPECTED_RESPONSE_BODY "\"id\":"
EXPECTED_RESPONSE_BODY "\"name\":"
EXPECTED_RESPONSE_BODY "\"email\":"

# Avoid: Too specific, breaks on formatting changes
EXPECTED_RESPONSE_BODY "  \"id\": 123,"
</code></pre></div></div>

<h4 id="test-both-success-and-failure-scenarios">Test Both Success and Failure Scenarios</h4>

<p>Comprehensive testing includes validating that your API fails correctly:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code># Test success case
POST https://api.example.com/login
Content-Type: application/json

{"username": "valid@example.com", "password": "correct"}

EXPECTED_RESPONSE_STATUS 200
EXPECTED_RESPONSE_BODY "\"token\":"

###

# Test failure case
POST https://api.example.com/login
Content-Type: application/json

{"username": "invalid@example.com", "password": "wrong"}

EXPECTED_RESPONSE_STATUS 401
EXPECTED_RESPONSE_BODY "invalid credentials"
</code></pre></div></div>

<h3 id="integration-with-development-workflows">Integration with Development Workflows</h3>

<h4 id="pre-commit-testing">Pre-commit Testing</h4>

<p>Use assertions in pre-commit hooks to catch API regressions before they reach version control:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># In your pre-commit script</span>
httprunner tests/api-contracts.http <span class="nt">--verbose</span>
<span class="k">if</span> <span class="o">[</span> <span class="nv">$?</span> <span class="nt">-ne</span> 0 <span class="o">]</span><span class="p">;</span> <span class="k">then
    </span><span class="nb">echo</span> <span class="s2">"API contract tests failed. Commit blocked."</span>
    <span class="nb">exit </span>1
<span class="k">fi</span>
</code></pre></div></div>

<h4 id="environment-validation">Environment Validation</h4>

<p>After deployments, run assertion-heavy test suites to validate the new environment:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Validate staging deployment</span>
httprunner tests/smoke-tests.http <span class="nt">--env</span> staging <span class="nt">--log</span> staging-validation.log

<span class="c"># Validate production deployment</span>
httprunner tests/critical-paths.http <span class="nt">--env</span> production <span class="nt">--log</span> prod-validation.log
</code></pre></div></div>

<h4 id="performance-and-regression-testing">Performance and Regression Testing</h4>

<p>Combine assertions with regular execution to catch both functional and performance regressions:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code># This request should complete quickly and return valid data
GET https://api.example.com/users?page=1&amp;limit=10
EXPECTED_RESPONSE_STATUS 200
EXPECTED_RESPONSE_BODY "\"users\":"
EXPECTED_RESPONSE_BODY "\"total\":"
EXPECTED_RESPONSE_HEADERS "Content-Type: application/json"

# The timing information in the output helps track performance trends
</code></pre></div></div>

<p>This comprehensive assertion system ensures that your API tests are thorough, reliable, and provide meaningful feedback when things go wrong. By incorporating detailed assertions into your HTTP files, you create living documentation of your API contracts while building robust test suites that catch issues early in the development cycle.</p>

<h2 id="logging-and-cicd-integration">Logging and CI/CD Integration</h2>

<p>The logging feature makes this tool perfect for CI/CD pipelines and automated testing scenarios. Modern software development relies heavily on automation, and HTTP File Runner’s logging capabilities are designed to integrate seamlessly into these workflows.</p>

<h3 id="logging-options">Logging Options</h3>

<p>The <code class="language-plaintext highlighter-rouge">--log</code> flag provides several options for capturing test results:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Generate test reports for build systems</span>
httprunner <span class="nt">--discover</span> <span class="nt">--log</span> test_report_<span class="si">$(</span><span class="nb">date</span> +%Y%m%d_%H%M%S<span class="si">)</span>.log

<span class="c"># Daily API health checks</span>
httprunner health-checks.http <span class="nt">--verbose</span> <span class="nt">--log</span> daily_health_check.log
</code></pre></div></div>

<h3 id="integration-scenarios">Integration Scenarios</h3>

<p><strong>Build Pipeline Integration</strong>: Configure your CI/CD system to run HTTP File Runner as part of the build process. The tool’s exit codes and log files provide everything needed for build status determination and result reporting.</p>

<p><strong>Deployment Validation</strong>: After deploying to a new environment, automatically run a suite of health check requests to verify that all services are responding correctly.</p>

<p><strong>Monitoring and Alerting</strong>: Set up scheduled runs of critical API tests, with log files feeding into monitoring systems that can alert on failures or performance degradation.</p>

<p><strong>Documentation and Reporting</strong>: The verbose logging mode captures complete request/response cycles, making it easy to generate API documentation or troubleshooting guides from actual test runs.</p>

<h3 id="log-file-benefits">Log File Benefits</h3>

<p>Log files preserve:</p>

<ul>
  <li>Complete terminal output including colors and emojis</li>
  <li>Detailed HTTP request and response information (when using <code class="language-plaintext highlighter-rouge">--verbose</code>)</li>
  <li>Success/failure indicators and summary statistics</li>
  <li>Error messages and network diagnostics</li>
  <li>Execution timestamps and duration metrics</li>
</ul>

<p>This comprehensive logging makes HTTP File Runner suitable not just for testing, but for API monitoring, documentation generation, and troubleshooting production issues.</p>

<h2 id="output-examples">Output Examples</h2>

<p>The tool provides beautiful, color-coded output:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>🚀 HTTP File Runner - Processing file: .\examples\simple.http
==================================================
Found 4 HTTP request(s)

✅ GET https://httpbin.org/status/200 - Status: 200 - 557ms
❌ GET https://httpbin.org/status/404 - Status: 404 - 542ms
✅ GET https://api.github.com/zen - Status: 200 - 85ms
✅ GET https://jsonplaceholder.typicode.com/users/1 - Status: 200 - 91ms

==================================================
File Summary: 3/4 requests succeeded
</code></pre></div></div>

<h2 id="whats-next">What’s Next?</h2>

<p>I’m continuously improving the tool based on user feedback and real-world usage patterns. The roadmap includes several exciting enhancements that will further enhance its capabilities:</p>

<h3 id="planned-enhancements">Planned Enhancements</h3>

<ul>
  <li>
    <p><strong>Request timeout configuration</strong> - Configurable timeouts per request or globally, essential for testing APIs with varying response times or unreliable networks.</p>
  </li>
  <li>
    <p><strong>JSON response formatting</strong> - Pretty-printing and syntax highlighting for JSON responses, making it easier to read and debug API responses.</p>
  </li>
  <li>
    <p><strong>Export results to different formats</strong> - JSON, XML, CSV, and HTML reports for integration with various reporting and analysis tools.</p>
  </li>
  <li>
    <p><strong>Parallel execution</strong> - Option to run multiple requests concurrently for faster test suite execution.</p>
  </li>
</ul>

<h2 id="conclusion">Conclusion</h2>

<p><a href="https://github.com/christianhelle/httprunner">HTTP File Runner</a> represents my exploration into newer programming languages with Zig while solving a real-world problem that affects developers daily. What started as a learning exercise to understand Zig evolved into a genuinely useful tool that complements my existing <a href="https://github.com/christianhelle/httpgenerator">HTTP File Generator</a> project.</p>

<p>Building <a href="https://github.com/christianhelle/httprunner">HTTP File Runner</a> in Zig has been an exercise in balancing performance with usability.</p>

<p>The modular code structure makes the project maintainable and extensible. Each component, from HTTP parsing to response validation, is cleanly separated, making it easy to add new features or modify existing behavior.</p>

<p>Give it a try and let me know what you think! If you find it useful, consider starring the repository or sharing it with fellow developers.</p>]]></content><author><name>Christian Helle</name></author><category term="Zig" /><category term="HTTP" /><summary type="html"><![CDATA[I’m excited to share my latest project: HTTP File Runner, a command-line tool written in Zig that parses .http files and executes HTTP requests. This tool provides colored output to indicate success or failure, making it easy to test APIs and web services directly from your terminal.]]></summary></entry><entry><title type="html">A faster Azure DevOps CLI written in Rust</title><link href="https://christianhelle.com/2025/06/azure-devops-cli.html" rel="alternate" type="text/html" title="A faster Azure DevOps CLI written in Rust" /><published>2025-06-05T00:00:00+00:00</published><updated>2025-06-05T00:00:00+00:00</updated><id>https://christianhelle.com/2025/06/azure-devops-cli</id><content type="html" xml:base="https://christianhelle.com/2025/06/azure-devops-cli.html"><![CDATA[<p>Imagine this: You get a brand new computer—exciting, right? But then reality hits. You need to restore your entire development environment, which means re-cloning hundreds of repositories from Azure DevOps. You launch the browser The process is slow, repetitive, and quickly turns your excitement into frustration. Obviously, any self respecting programmer would have found a way to automate this and script their way around the problem. And then there are types who go a step further and build a general purpose tool for the job. Introducing my latest project, the <a href="https://github.com/christianhelle/azdocli">Azure DevOps CLI</a> — a blazing fast Azure DevOps CLI tool written in Rust that can restore and clone hundreds of repositories in seconds. The tool currently only supports managing Boards, Repos, and Pipelines.</p>

<h2 id="why-rust">Why Rust?</h2>

<p>To be completely honest, this project started as a learning adventure. I’ve been wanting to dive deeper into Rust for a while now, attracted by all the buzz around its performance and memory safety guarantees. But I needed a real-world project that would push me beyond the typical “hello world” tutorials.</p>

<p>The turning point came when I discovered the <a href="https://github.com/microsoft/azure-devops-rust-api">Azure DevOps Rust API</a> - a beautifully crafted library that was auto-generated from OpenAPI specifications. It was like finding the perfect building blocks for my vision. Here was a chance to combine my frustration with existing tooling, my desire to learn Rust, and a solid foundation to build upon.</p>

<p>As I dove into development, I was amazed by Rust’s promises coming to life. What emerged was <a href="https://github.com/christianhelle/azdocli">azdocli</a> - a tool that delivers near-instant command execution, uses minimal memory, and compiles to a single static binary with zero dependencies. No more waiting for Python environments to initialize or dealing with complex dependency chains. Just download one file and you’re ready to go.</p>

<h2 id="key-features">Key Features</h2>

<ul>
  <li><strong>Lightning fast</strong>: Commands execute in milliseconds.</li>
  <li><strong>Cross-platform</strong>: Works on Windows, macOS, and Linux.</li>
  <li><strong>No dependencies</strong>: Just download the binary and run.</li>
  <li><strong>Simple authentication</strong>: Supports Personal Access Tokens (PAT) for secure authentication.</li>
  <li><strong>Default project management</strong>: Set a default project to avoid specifying <code class="language-plaintext highlighter-rouge">--project</code> for every command.</li>
  <li><strong>Repository Management</strong>: List, create, delete, clone, view, and manage pull requests in repositories.</li>
  <li><strong>Pipeline Management</strong>: List pipelines, view runs, show details, and trigger new builds.</li>
  <li><strong>Board Management</strong>: Create, read, update, and delete work items (bugs, tasks, user stories, features, epics).</li>
  <li><strong>Parallel operations</strong>: Clone multiple repositories simultaneously with configurable concurrency.</li>
  <li><strong>Easy scripting</strong>: Designed for automation and CI/CD workflows with <code class="language-plaintext highlighter-rouge">--yes</code> flags to skip confirmations.</li>
</ul>

<h2 id="getting-started">Getting Started</h2>

<h3 id="installation-options">Installation Options</h3>

<p>There are different ways to install azdocli:</p>

<h4 id="quick-install-scripts-recommended">Quick Install Scripts (Recommended)</h4>

<p><strong>Linux and macOS:</strong></p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-sSL</span> https://christianhelle.com/azdocli/install | bash
</code></pre></div></div>

<p><strong>Windows (PowerShell):</strong></p>

<pre><code class="language-pwsh">iwr -useb https://christianhelle.com/azdocli/install.ps1 | iex
</code></pre>

<p>These one-liner commands will automatically download and install the latest release for your platform.</p>

<h4 id="from-cratesio-requires-rust">From crates.io (Requires <a href="https://rust-lang.org/learn/get-started/">Rust</a>)</h4>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cargo <span class="nb">install </span>azdocli
</code></pre></div></div>

<h4 id="from-github-releases">From GitHub Releases</h4>

<ol>
  <li>Download the latest release from the <a href="https://github.com/christianhelle/azdocli/releases">GitHub releases page</a>.
    <ul>
      <li>Windows: <code class="language-plaintext highlighter-rouge">windows-x64.zip</code> or <code class="language-plaintext highlighter-rouge">windows-arm64.zip</code></li>
      <li>macOS: <code class="language-plaintext highlighter-rouge">macos-x64.zip</code> or <code class="language-plaintext highlighter-rouge">macos-arm64.zip</code></li>
      <li>Linux: <code class="language-plaintext highlighter-rouge">linux-x64.zip</code> or <code class="language-plaintext highlighter-rouge">linux-arm64.zip</code></li>
    </ul>
  </li>
  <li>Extract the binary and add it to your PATH.</li>
  <li>Make the binary executable (<code class="language-plaintext highlighter-rouge">chmod +x ado</code> on Unix).</li>
</ol>

<h3 id="authentication-setup">Authentication Setup</h3>

<p>Before using the CLI, you need to create a Personal Access Token (PAT) in Azure DevOps:</p>

<ol>
  <li><strong>Navigate to Azure DevOps</strong>:
    <ul>
      <li>Sign in to your Azure DevOps organization (<code class="language-plaintext highlighter-rouge">https://dev.azure.com/{yourorganization}</code>)</li>
      <li>Click on your profile picture in the top right corner</li>
      <li>Select <strong>Personal Access Tokens</strong></li>
    </ul>
  </li>
  <li><strong>Create New Token</strong>:
    <ul>
      <li>Click <strong>+ New Token</strong></li>
      <li>Enter a descriptive name (e.g., “azdocli-token”)</li>
      <li>Select your organization</li>
      <li>Set expiration date (recommended: 90 days or less)</li>
    </ul>
  </li>
  <li><strong>Configure Required Scopes</strong>:
    <ul>
      <li><strong>Code</strong>: Read &amp; write (for repository operations)</li>
      <li><strong>Build</strong>: Read &amp; execute (for pipeline operations)</li>
      <li><strong>Work Items</strong>: Read &amp; write (for board operations)</li>
      <li><strong>Project and Team</strong>: Read (for project operations)</li>
    </ul>
  </li>
  <li><strong>Save Your Token</strong>: Copy the token immediately and store it securely - it won’t be shown again.</li>
</ol>

<h3 id="initial-setup">Initial Setup</h3>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Login with your Personal Access Token</span>
azdocli login
<span class="c"># You'll be prompted for:</span>
<span class="c"># - Organization name (e.g., "mycompany" from https://dev.azure.com/mycompany)</span>
<span class="c"># - Personal Access Token (the PAT you created above)</span>

<span class="c"># Set a default project (optional but recommended)</span>
azdocli project MyProject
</code></pre></div></div>

<h2 id="the-aha-moment-real-world-performance">The “Aha!” Moment: Real-World Performance</h2>

<p>Let me share a story that perfectly illustrates why I built this tool. A couple of months ago, I was working on a project that required cloning hundreds of repositories from different Azure DevOps projects. With the official CLI and some scripting, this was a painful process - each repository clone command took several seconds to even start, and I had to run them one by one.</p>

<p>With azdocli, I can now do this:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>azdocli repos clone <span class="nt">--parallel</span> <span class="nt">--concurrency</span> 32 <span class="nt">--yes</span>
</code></pre></div></div>

<p>What used to take me 20 minutes of building a script and babysitting individual commands now completes in a few seconds, running unattended in the background while I do something else. That’s the kind of time savings that actually changes how you work.</p>

<h2 id="behind-the-scenes-development-stories">Behind the Scenes: Development Stories</h2>

<h3 id="the-default-project-revelation">The Default Project Revelation</h3>

<p>One of my favorite features didn’t come from grand planning - it emerged from pure frustration. During development, I found myself constantly typing <code class="language-plaintext highlighter-rouge">--project MyProject</code> for every single command. After the hundredth time, I thought “there has to be a better way.”</p>

<p>That’s when I implemented the default project feature:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Set it once</span>
azdocli project MyDefaultProject

<span class="c"># Now all these commands "just work" without repetitive typing</span>
azdocli boards work-item list       <span class="c"># List work items assigned to me</span>
azdocli repos list                  <span class="c"># Uses default project</span>
azdocli pipelines list              <span class="c"># Uses default project</span>
azdocli repos list <span class="nt">--project</span> Other  <span class="c"># Override when needed</span>
</code></pre></div></div>

<p>This simple feature probably saves me dozens of keystrokes every day. It’s those small quality-of-life improvements that make tools truly enjoyable to use.</p>

<h3 id="the-parallel-cloning-challenge">The Parallel Cloning Challenge</h3>

<p>Implementing parallel repository cloning was one of the most rewarding technical challenges. The original approach was straightforward - clone repositories one by one. But watching paint dry would have been more exciting.</p>

<p>The breakthrough came when I realized that most of the time was spent waiting for network operations, not actual CPU work. This was also the perfect chance to learn Rust’s async capabilities:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># The magic command that changed everything</span>
azdocli repos clone <span class="nt">--parallel</span> <span class="nt">--concurrency</span> 32 <span class="nt">--yes</span>
</code></pre></div></div>

<p>Watching 32 repositories clone simultaneously while seeing real-time progress updates was genuinely exciting. It felt like upgrading from a bicycle to a car.</p>

<h2 id="daily-workflow">Daily Workflow</h2>

<h3 id="morning-standup-preparation">Morning Standup Preparation</h3>

<p>Every morning before our team standup, I used to manually check multiple pipelines and pull requests across different projects. It was a tedious ritual involving multiple browser tabs and lots of clicking.</p>

<p>Now my morning routine looks like this:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># See my work items</span>
azdocli boards work-item list

<span class="c"># Check my pull requests</span>
azdocli repos <span class="nb">pr </span>list <span class="nt">--repo</span> MyMainRepo

<span class="c"># See what work items need attention</span>
azdocli boards work-item show <span class="nt">--id</span> 123 <span class="nt">--web</span>
</code></pre></div></div>

<p>Three commands, executed in seconds, and I’m fully prepared for standup. My teammates often ask how I’m always so up-to-date on project status!</p>

<h3 id="the-emergency-response">The Emergency Response</h3>

<p>A critical production issue that required immediate attention. While others were still loading their browsers and navigating through Azure DevOps web interface, I had already:</p>

<ol>
  <li>Triggered the hotfix pipeline: <code class="language-plaintext highlighter-rouge">azdocli pipelines run --id 42</code></li>
  <li>Created an emergency work item: <code class="language-plaintext highlighter-rouge">azdocli boards work-item create bug --title "Critical login issue" --description "Users unable to authenticate"</code></li>
  <li>Opened the build status directly in my browser for monitoring</li>
</ol>

<p>The entire response took less than 20 seconds and I never left the context of my editor or terminal</p>

<h4 id="pull-request-management">Pull Request Management</h4>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># List all pull requests for a repository</span>
azdocli repos <span class="nb">pr </span>list <span class="nt">--repo</span> MyRepository

<span class="c"># Show details of a specific pull request</span>
azdocli repos <span class="nb">pr </span>show <span class="nt">--repo</span> MyRepository <span class="nt">--id</span> 123

<span class="c"># Create a new pull request</span>
azdocli repos <span class="nb">pr </span>create <span class="nt">--repo</span> MyRepository <span class="nt">--source</span> <span class="s2">"feature/my-feature"</span> <span class="nt">--target</span> <span class="s2">"main"</span> <span class="nt">--title</span> <span class="s2">"My Feature"</span> <span class="nt">--description</span> <span class="s2">"Description"</span>

<span class="c"># Create with minimal information - target defaults to 'main'</span>
azdocli repos <span class="nb">pr </span>create <span class="nt">--repo</span> MyRepository <span class="nt">--source</span> <span class="s2">"feature/my-feature"</span> <span class="nt">--title</span> <span class="s2">"My Feature"</span>
</code></pre></div></div>

<h3 id="pipeline-management">Pipeline Management</h3>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># List all pipelines</span>
azdocli pipelines list

<span class="c"># Show all runs for a pipeline</span>
azdocli pipelines runs <span class="nt">--id</span> 42

<span class="c"># Show details of a specific pipeline build</span>
azdocli pipelines show <span class="nt">--id</span> 42 <span class="nt">--build-id</span> 123

<span class="c"># Run a pipeline</span>
azdocli pipelines run <span class="nt">--id</span> 42
</code></pre></div></div>

<h3 id="board-management">Board Management</h3>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Show "my" work items</span>
azdocli boards work-item list

<span class="c"># Show details of a specific work item</span>
azdocli boards work-item show <span class="nt">--id</span> 123

<span class="c"># Open work item directly in web browser</span>
azdocli boards work-item show <span class="nt">--id</span> 123 <span class="nt">--web</span>

<span class="c"># Create a new work item (supported types: bug, task, user-story, feature, epic)</span>
azdocli boards work-item create bug <span class="nt">--title</span> <span class="s2">"Fix login issue"</span> <span class="nt">--description</span> <span class="s2">"Users cannot login after password change"</span>

<span class="c"># Update a work item</span>
azdocli boards work-item update <span class="nt">--id</span> 123 <span class="nt">--title</span> <span class="s2">"New title"</span> <span class="nt">--state</span> <span class="s2">"Active"</span> <span class="nt">--priority</span> 2

<span class="c"># Delete a work item permanently</span>
azdocli boards work-item delete <span class="nt">--id</span> 123

<span class="c"># Soft delete a work item by changing state to "Removed"</span>
azdocli boards work-item delete <span class="nt">--id</span> 123 <span class="nt">--soft-delete</span>
</code></pre></div></div>

<h2 id="community-and-lessons-learned">Community and Lessons Learned</h2>

<h3 id="the-open-source-journey">The Open Source Journey</h3>

<p>Building azdocli has been more than just solving my own problems - it’s been a learning journey about the Rust ecosystem. The Azure DevOps Rust API that I built upon is itself a testament to the power of code generation and the thoughtful design of the Azure DevOps team.</p>

<p>What started as a personal frustration project has now grown into something I genuinely believe can help other developers be more productive. The feedback from early users has been incredibly encouraging, with many sharing their own stories of how the tool has streamlined their workflows.</p>

<h3 id="performance-numbers-that-matter">Performance Numbers That Matter</h3>

<p>Here’s what really gets me excited: the difference isn’t just subjective. During development, I did some basic benchmarking:</p>

<ul>
  <li><strong>Official Azure DevOps CLI</strong>: Takes seconds per command</li>
  <li><strong>azdocli</strong>: 50-200 milliseconds per command</li>
</ul>

<p>That’s not just a performance improvement - it’s a fundamentally different user experience. When commands execute faster than you can blink, it changes how you think about automation and scripting.</p>

<h3 id="advanced-features-born-from-real-needs">Advanced Features Born from Real Needs</h3>

<h3 id="parallel-repository-cloning">Parallel Repository Cloning</h3>

<p>One of the standout features is the ability to clone multiple repositories in parallel:</p>

<ul>
  <li><strong>Bulk cloning</strong>: Clone all repositories from a project with a single command</li>
  <li><strong>Target directory</strong>: Specify where to clone repositories (defaults to current directory)</li>
  <li><strong>Confirmation prompts</strong>: Interactive confirmation with repository listing before cloning</li>
  <li><strong>Automation support</strong>: Skip prompts with <code class="language-plaintext highlighter-rouge">--yes</code> flag for CI/CD scenarios</li>
  <li><strong>Parallel execution</strong>: Use <code class="language-plaintext highlighter-rouge">--parallel</code> flag to clone multiple repositories simultaneously</li>
  <li><strong>Concurrency control</strong>: Adjust the number of concurrent operations with <code class="language-plaintext highlighter-rouge">--concurrency</code> (1-8)</li>
</ul>

<h3 id="security-best-practices">Security Best Practices</h3>

<p>When working with Personal Access Tokens:</p>

<ul>
  <li>Never commit your PAT to version control</li>
  <li>Use environment variables or secure storage for automation</li>
  <li>Regularly rotate your tokens</li>
  <li>Use the minimum required permissions</li>
  <li>Store tokens securely and never share them</li>
</ul>

<h3 id="error-handling-learning-from-mistakes">Error Handling: Learning from Mistakes</h3>

<p>The CLI provides comprehensive error handling with:</p>

<ul>
  <li>Clear feedback when repositories, pipelines, or work items are not found</li>
  <li>Helpful suggestions when authentication fails</li>
  <li>Validation of required parameters before making API calls</li>
  <li>User-friendly formatting with emoji icons for better readability</li>
</ul>

<p>Every error message in azdocli has a story behind it - usually me running into that exact problem during development and thinking “how can I make this less confusing for the next person?”</p>

<h2 id="real-world-impact-the-numbers">Real-World Impact: The Numbers</h2>

<p>Since releasing azdocli, I’ve tracked some interesting metrics about my own usage:</p>

<ul>
  <li><strong>Daily time saved</strong>: Approximately 15-20 minutes (mostly from faster command execution and parallel operations)</li>
  <li><strong>Reduced context switching</strong>: 60% fewer browser tab switches during development work</li>
  <li><strong>Automation adoption</strong>: Increased script usage by 3x due to reliable <code class="language-plaintext highlighter-rouge">--yes</code> flags and fast execution</li>
</ul>

<p>But the real impact isn’t in the numbers - it’s in the reduced friction. When tools get out of your way, you can focus on what really matters: building great software.</p>

<h2 id="example-use-cases">Example Use Cases</h2>

<ul>
  <li><strong>Development Workflow</strong>: Quickly list and trigger pipelines, create pull requests, and manage work items</li>
  <li><strong>Repository Operations</strong>: Clone entire project repositories in parallel, view repository details, and manage pull requests</li>
  <li><strong>CI/CD Automation</strong>: Integrate with build scripts using <code class="language-plaintext highlighter-rouge">--yes</code> flags to skip confirmations</li>
  <li><strong>Work Item Management</strong>: Create, update, and track bugs, tasks, user stories, features, and epics</li>
  <li><strong>Team Collaboration</strong>: Open work items and pull requests directly in the browser for quick access</li>
</ul>

<h2 id="building-from-source">Building from Source</h2>

<p>If you prefer to build from source:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Clone the repository</span>
git clone https://github.com/christianhelle/azdocli.git
<span class="nb">cd </span>azdocli

<span class="c"># Build the project</span>
cargo build

<span class="c"># Run tests</span>
cargo <span class="nb">test</span>

<span class="c"># Run the CLI</span>
cargo run <span class="nt">--</span> &lt;<span class="nb">command</span><span class="o">&gt;</span>
</code></pre></div></div>

<h2 id="testing">Testing</h2>

<p>The project includes comprehensive integration tests that verify functionality against real Azure DevOps instances. Tests cover repository operations including create, show, clone, and delete operations.</p>

<h2 id="looking-forward">Looking Forward</h2>

<p>Building <a href="https://github.com/christianhelle/azdocli">azdocli</a> was a learning exercise. I have built the features that I personally need and use and I don’t have plans for newer features. If you have any ideas and feature requests then feel free to create an issue on the Github repository and I’ll see what I can do. Or even better, implement the feature yourself and I’ll make sure we merge your pull request in and get it released</p>]]></content><author><name>Christian Helle</name></author><category term="Azure DevOps" /><category term="Rust" /><summary type="html"><![CDATA[Imagine this: You get a brand new computer—exciting, right? But then reality hits. You need to restore your entire development environment, which means re-cloning hundreds of repositories from Azure DevOps. You launch the browser The process is slow, repetitive, and quickly turns your excitement into frustration. Obviously, any self respecting programmer would have found a way to automate this and script their way around the problem. And then there are types who go a step further and build a general purpose tool for the job. Introducing my latest project, the Azure DevOps CLI — a blazing fast Azure DevOps CLI tool written in Rust that can restore and clone hundreds of repositories in seconds. The tool currently only supports managing Boards, Repos, and Pipelines.]]></summary></entry><entry><title type="html">SQLite Query Analyzer - A Decade-Long Journey with C++ and Qt</title><link href="https://christianhelle.com/2025/05/sqlite-query-analyzer-revisited.html" rel="alternate" type="text/html" title="SQLite Query Analyzer - A Decade-Long Journey with C++ and Qt" /><published>2025-05-13T00:00:00+00:00</published><updated>2025-05-13T00:00:00+00:00</updated><id>https://christianhelle.com/2025/05/sqlite-query-analyzer-revisited</id><content type="html" xml:base="https://christianhelle.com/2025/05/sqlite-query-analyzer-revisited.html"><![CDATA[<p>There’s something deeply satisfying about revisiting a project you created over a decade ago and breathing new life into it. Today, I want to share the story of <a href="https://github.com/christianhelle/sqlitequery">SQLite Query Analyzer</a>, a cross-platform database management tool that began as a personal necessity in 2011 and has recently undergone a complete modernization to embrace contemporary C++ practices and the latest Qt framework features.</p>

<p>Back in 2011, I found myself in a predicament that many developers face. I was working extensively with mobile applications that relied heavily on SQLite databases for local data persistence. While there were several database management tools available at the time, most were either too heavyweight for my needs, lacked cross-platform compatibility, or simply didn’t provide the streamlined workflow I required for rapid development and debugging. I needed a tool that could do what none of the existing tools could do.</p>

<p>The mobile development landscape in 2011 was quite different from today. Most of the work I did ran on even older Windows CE based handheld devices. These industrial devices had a 15-year lifetime, and cost a fortune. I was dealing with resource-constrained devices, and every byte mattered. SQLite was (and still is) the go-to solution for local data storage, but debugging and managing these databases during development was often cumbersome. I needed something fast, lightweight, and intuitive – a tool that could keep up with my development workflow without getting in the way. It also needed to work on MacOS, Windows, and Linux</p>

<p>That’s when I decided to create SQLite Query Analyzer. The goal was simple: build a no-nonsense, efficient database management tool that would allow me to quickly inspect, query, and modify SQLite databases during mobile app development. I chose C++ for its performance characteristics and Qt for its excellent cross-platform capabilities and mature UI framework.</p>

<h3 id="the-technical-foundation">The Technical Foundation</h3>

<p>From the beginning, <a href="https://github.com/christianhelle/sqlitequery">SQLite Query Analyzer</a> was designed with several core principles in mind:</p>

<p><strong>Performance First</strong>: Working with mobile development taught me to respect the value of speed and efficiency. The application needed to start instantly, load databases instantly, and execute queries without delay.</p>

<p><strong>Cross-Platform Compatibility</strong>: Developing for Windows, macOS, and Linux required a tool that could run reliably on all major operating systems. Qt’s robust cross-platform support enabled me to maintain a single codebase while delivering native experiences across different environments.</p>

<p><strong>Simplicity and Usability</strong>: The interface needed to be intuitive enough for quick database inspections during debugging sessions, yet powerful enough for more complex database management tasks.</p>

<p><strong>Modern UI Paradigms</strong>: Even in 2011, I wanted the application to feel contemporary and integrate well with the native look and feel of each operating system.</p>

<h3 id="features-that-matter">Features That Matter</h3>

<p>Over the years, <a href="https://github.com/christianhelle/sqlitequery">SQLite Query Analyzer</a> has evolved to include features that directly address real-world database management needs:</p>

<p><strong>Theme Awareness</strong>: In our modern development environments, dark mode isn’t just a preference – it’s often a necessity for long coding sessions. The application automatically detects and adapts to your system’s color theme, providing a comfortable viewing experience in both light and dark modes.</p>

<p><strong>Intelligent Query Interface</strong>: The application provides a clean, syntax-highlighted SQL editor that makes writing and executing queries a pleasant experience. Whether you’re running a simple SELECT statement or a complex JOIN operation, the interface stays out of your way and lets you focus on the data.</p>

<p><img src="/assets/images/sqlitequery-windows-dark-query-select.png" alt="Large databases" /></p>

<p><strong>Speed</strong>: Loading a database over 100GB takes microseconds. Executing queries are equally fast (of course given that the query is also fast)</p>

<p><img src="/assets/images/sqlitequery-windows-dark-query-insert.png" alt="Large databases" /></p>

<p><strong>Direct Table Editing</strong>: One of the features I’m most proud of is the ability to edit table data directly within the interface. This eliminates the need to write UPDATE statements for simple data modifications, significantly speeding up the development and testing process.</p>

<p><img src="/assets/images/sqlitequery-windows-table-data.png" alt="Table Editor" /></p>

<p><strong>Session Persistence</strong>: The application remembers your last session, including open databases, recent queries, and window positioning. This might seem like a small detail, but it makes a huge difference in daily workflow efficiency.</p>

<p><img src="/assets/images/sqlitequery-windows-dark-recent-files.png" alt="Recent Files" /></p>

<p><strong>Data Export Capabilities</strong>: Need to share your database structure or data? The tool can export schemas as CREATE TABLE statements and data as SQL INSERT scripts or CSV files. This has proven invaluable for documentation, migration scenarios, and sharing sample data with team members.</p>

<p><img src="/assets/images/sqlitequery-windows-dark-export-schema.png" alt="Export Schema" /></p>

<p><img src="/assets/images/sqlitequery-macos-export-data.png" alt="Export Data" /></p>

<h4 id="modernization">Modernization</h4>

<p>Recently, I decided it was time to give this decade-old project the attention it deserved. The C++ landscape has evolved significantly since 2011, with C++17 and C++20 introducing numerous improvements that make code more expressive, safer, and more maintainable. Similarly, Qt has continued to innovate, with Qt 6 bringing substantial improvements in performance, API design, and cross-platform consistency.</p>

<p>The modernization effort focused on several key areas:</p>

<p><strong>Modern C++ Standards</strong>: The codebase has been updated to leverage C++17 and C++20 features, including structured bindings, constexpr enhancements, and improved type deduction. This makes the code more readable and allows the compiler to perform better optimizations.</p>

<p><strong>Qt 6 Migration</strong>: Moving to Qt 6 brought numerous benefits, including better high-DPI support, system-aware themes for light and dark mode, improved rendering performance, and more consistent behavior across platforms.</p>

<p><strong>Enhanced Build System</strong>: The project now uses CMake with modern practices, making it easier to build across different platforms and integrate with various development environments. The build system now properly handles dependencies and provides clear configuration options for different deployment scenarios.</p>

<p><strong>Improved Code Architecture</strong>: Years of experience have taught me better patterns for organizing C++ code. The modernized version features cleaner separation of concerns, better error handling, and more robust memory management practices.</p>

<p><strong>CI/CD Integration</strong>: The project now includes comprehensive continuous integration pipelines for Windows, macOS, and Linux, ensuring that changes don’t break compatibility across platforms. This automation gives me confidence when making changes and helps maintain the quality standards the project deserves.</p>

<h3 id="working-with-qt-and-sqlite">Working with Qt and SQLite</h3>

<p>One of the most interesting aspects of this project has been the intersection of Qt’s powerful database abstraction layer with SQLite’s unique characteristics. Qt’s QSqlDatabase and related classes provide an excellent foundation for database operations, but SQLite’s specific features and behavior patterns require careful consideration.</p>

<p>The cross-platform nature of both Qt and SQLite creates some fascinating technical challenges. File path handling, for example, needs to account for different path separators and naming conventions across operating systems. Similarly, font rendering and UI scaling behaviors vary significantly between platforms, requiring careful testing and adjustment to ensure a consistent user experience.</p>

<p>The project has also been a testament to the longevity of well-designed technologies. Both C++ and Qt have proven to be incredibly stable platforms for building desktop applications. Code written in 2011 still compiles and runs today with minimal modifications, which speaks to the maturity and backward compatibility of these technologies.</p>

<p>Perhaps most importantly, this project has reinforced my belief in the value of creating tools that enhance developer productivity. In an industry that’s constantly evolving and introducing new frameworks, platforms, and paradigms, having reliable, efficient tools for fundamental tasks like database management remains as important as ever.</p>

<h3 id="looking-forward">Looking Forward</h3>

<p>As we move further into 2025, <a href="https://github.com/christianhelle/sqlitequery">SQLite Query Analyzer</a> continues to serve its original purpose while adapting to modern development needs. The recent modernization ensures that it will continue to be a valuable tool for developers working with SQLite databases, whether they’re building mobile applications, desktop software, or embedded systems.</p>

<p>The open-source nature of the project means that it can continue to evolve with community input and contributions. I’ve always believed that the best tools are those that grow organically based on real-world usage and feedback from the people who rely on them daily.</p>

<p>For developers interested in C++ desktop application development, the project also serves as a practical example of how to build cross-platform applications using Qt. The source code demonstrates real-world patterns for database integration, UI design, and cross-platform deployment – knowledge that’s valuable regardless of the specific domain you’re working in.</p>

<h3 id="try-it-yourself">Try It Yourself</h3>

<p><a href="https://github.com/christianhelle/sqlitequery">SQLite Query Analyzer</a> is available as an open-source project on GitHub, complete with installation and build instructions for Windows, MacOS, and Linux. Whether you’re a mobile developer working with SQLite databases, a desktop application developer looking for a lightweight database tool, or simply someone interested in seeing how modern C++ and Qt can be used together, I encourage you to give it a try.</p>

<p>The project includes comprehensive build documentation and automated builds for all major platforms, making it easy to get started regardless of your development environment. And if you find it useful or have suggestions for improvements, I’d love to hear from you – that’s the beauty of open-source development.</p>]]></content><author><name>Christian Helle</name></author><category term="SQLite" /><category term="C++" /><category term="Qt" /><summary type="html"><![CDATA[There’s something deeply satisfying about revisiting a project you created over a decade ago and breathing new life into it. Today, I want to share the story of SQLite Query Analyzer, a cross-platform database management tool that began as a personal necessity in 2011 and has recently undergone a complete modernization to embrace contemporary C++ practices and the latest Qt framework features.]]></summary></entry><entry><title type="html">Azure Entra ID Authentication with Scalar and .NET 9.0</title><link href="https://christianhelle.com/2025/01/scalar-azure-authentication.html" rel="alternate" type="text/html" title="Azure Entra ID Authentication with Scalar and .NET 9.0" /><published>2025-01-20T00:00:00+00:00</published><updated>2025-01-20T00:00:00+00:00</updated><id>https://christianhelle.com/2025/01/scalar-azure-authentication</id><content type="html" xml:base="https://christianhelle.com/2025/01/scalar-azure-authentication.html"><![CDATA[<p><a href="https://swagger.io/tools/swagger-ui/">Swagger UI</a> and
<a href="https://github.com/domaindrivendev/Swashbuckle.AspNetCore">Swashbuckle.AspNetCore</a>
are two popular tools for working with OpenAPI for ASP.NET Core Web APIs.
Both tools have come with benefits and problems,
but that said, most .NET developers have gotten used to it.
Unfortunately, in May 2024, Microsoft
<a href="https://github.com/dotnet/aspnetcore/issues/54599">announced that Swashbuckle.AspNetCore will be removed from .NET 9.0</a>.
For the past year or so, I started using .http files in favor of Swagger UI.
Using .http files immediately lead me to develop <a href="https://github.com/christianhelle/httpgenerator">HTTP File Generator</a>,
a tool that can <a href="/2023/11/http-file-generator.html">generate a suite of .http files from OpenAPI specifications</a></p>

<p>The use of .http files were not immediately adopted by my teams so I started looking
at other alternatives, particularly, <a href="https://scalar.com">Scalar</a>.
Other teams I work with have built a work flow based on sharing
<a href="https://www.postman.com/product/collections/">Postman Collections</a> configured
with Authentication and multiple environments, like Dev, Test, and Production.
<a href="https://scalar.com">Scalar</a> feels a lot like <a href="https://www.postman.com">Postman</a>
which caught the interest of teams around me</p>

<p>You can try out Scalar <a href="https://docs.scalar.com/swagger-editor">here</a>.
It looks like this:</p>

<p><img src="/assets/images/scalar.png" alt="Scalar demo" /></p>

<p>In any project I’m involved in, one of the first thing I do,
even before setting up a CI/CD pipeline, is to configure security.
This usually uses <a href="https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-implicit-grant-flow?WT.mc_id=DT-MVP-5004822">OAuth2 with the implicit (or authorization code) flow</a>
and Azure Entra ID as a Secure Token Service (STS) for authentication.</p>

<p>This post will show you how to setup a .NET 9.0 project that produces
an OpenAPI docment and will demonstrate how to use
<a href="https://scalar.com">Scalar</a> instead of Swagger UI.
We will configure Scalar to authenticate against Azure Entra ID.</p>

<p>Let’s start by creating a simple API with .NET 9.0 (AOT)</p>

<p>By default, the <code class="language-plaintext highlighter-rouge">.csproj</code> file looks something like this:</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;Project</span> <span class="na">Sdk=</span><span class="s">"Microsoft.NET.Sdk.Web"</span><span class="nt">&gt;</span>
  <span class="nt">&lt;PropertyGroup&gt;</span>
    <span class="nt">&lt;TargetFramework&gt;</span>net9.0<span class="nt">&lt;/TargetFramework&gt;</span>
    <span class="nt">&lt;Nullable&gt;</span>enable<span class="nt">&lt;/Nullable&gt;</span>
    <span class="nt">&lt;ImplicitUsings&gt;</span>enable<span class="nt">&lt;/ImplicitUsings&gt;</span>
    <span class="nt">&lt;InvariantGlobalization&gt;</span>true<span class="nt">&lt;/InvariantGlobalization&gt;</span>
    <span class="nt">&lt;PublishAot&gt;</span>true<span class="nt">&lt;/PublishAot&gt;</span>
  <span class="nt">&lt;/PropertyGroup&gt;</span>
<span class="nt">&lt;/Project&gt;</span>
</code></pre></div></div>

<p>And the <code class="language-plaintext highlighter-rouge">Program.cs</code></p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">builder</span> <span class="p">=</span> <span class="n">WebApplication</span><span class="p">.</span><span class="nf">CreateSlimBuilder</span><span class="p">(</span><span class="n">args</span><span class="p">);</span>
<span class="n">builder</span><span class="p">.</span><span class="n">Services</span><span class="p">.</span><span class="nf">ConfigureHttpJsonOptions</span><span class="p">(</span>
    <span class="n">options</span> <span class="p">=&gt;</span>
    <span class="p">{</span>
        <span class="n">options</span><span class="p">.</span><span class="n">SerializerOptions</span><span class="p">.</span><span class="n">TypeInfoResolverChain</span><span class="p">.</span><span class="nf">Insert</span><span class="p">(</span>
            <span class="m">0</span><span class="p">,</span>
            <span class="n">AppJsonSerializerContext</span><span class="p">.</span><span class="n">Default</span><span class="p">);</span>
    <span class="p">})</span>

<span class="kt">var</span> <span class="n">app</span> <span class="p">=</span> <span class="n">builder</span><span class="p">.</span><span class="nf">Build</span><span class="p">();</span>
<span class="kt">var</span> <span class="n">todosApi</span> <span class="p">=</span> <span class="n">app</span><span class="p">.</span><span class="nf">MapGroup</span><span class="p">(</span><span class="s">"/todos"</span><span class="p">);</span>

<span class="kt">var</span> <span class="n">sampleTodos</span> <span class="p">=</span> <span class="k">new</span> <span class="n">Todo</span><span class="p">[]</span>
<span class="p">{</span>
    <span class="k">new</span><span class="p">(</span><span class="m">1</span><span class="p">,</span> <span class="s">"Walk the dog"</span><span class="p">),</span>
    <span class="k">new</span><span class="p">(</span><span class="m">2</span><span class="p">,</span> <span class="s">"Do the dishes"</span><span class="p">,</span> <span class="n">DateOnly</span><span class="p">.</span><span class="nf">FromDateTime</span><span class="p">(</span><span class="n">DateTime</span><span class="p">.</span><span class="n">Now</span><span class="p">)),</span>
    <span class="k">new</span><span class="p">(</span><span class="m">3</span><span class="p">,</span> <span class="s">"Do the laundry"</span><span class="p">,</span> <span class="n">DateOnly</span><span class="p">.</span><span class="nf">FromDateTime</span><span class="p">(</span><span class="n">DateTime</span><span class="p">.</span><span class="n">Now</span><span class="p">.</span><span class="nf">AddDays</span><span class="p">(</span><span class="m">1</span><span class="p">))),</span>
    <span class="k">new</span><span class="p">(</span><span class="m">4</span><span class="p">,</span> <span class="s">"Clean the bathroom"</span><span class="p">),</span>
    <span class="k">new</span><span class="p">(</span><span class="m">5</span><span class="p">,</span> <span class="s">"Clean the car"</span><span class="p">,</span> <span class="n">DateOnly</span><span class="p">.</span><span class="nf">FromDateTime</span><span class="p">(</span><span class="n">DateTime</span><span class="p">.</span><span class="n">Now</span><span class="p">.</span><span class="nf">AddDays</span><span class="p">(</span><span class="m">2</span><span class="p">)))</span>
<span class="p">};</span>

<span class="n">todosApi</span><span class="p">.</span><span class="nf">MapGet</span><span class="p">(</span><span class="s">"/"</span><span class="p">,</span> <span class="p">()</span> <span class="p">=&gt;</span> <span class="n">sampleTodos</span><span class="p">);</span>
<span class="n">todosApi</span><span class="p">.</span><span class="nf">MapGet</span><span class="p">(</span>
    <span class="s">"/{id}"</span><span class="p">,</span>
    <span class="p">(</span><span class="kt">int</span> <span class="n">id</span><span class="p">)</span> <span class="p">=&gt;</span>
        <span class="n">sampleTodos</span><span class="p">.</span><span class="nf">FirstOrDefault</span><span class="p">(</span><span class="n">a</span> <span class="p">=&gt;</span> <span class="n">a</span><span class="p">.</span><span class="n">Id</span> <span class="p">==</span> <span class="n">id</span><span class="p">)</span> <span class="k">is</span> <span class="p">{</span> <span class="p">}</span> <span class="n">todo</span>
            <span class="p">?</span> <span class="n">Results</span><span class="p">.</span><span class="nf">Ok</span><span class="p">(</span><span class="n">todo</span><span class="p">)</span>
            <span class="p">:</span> <span class="n">Results</span><span class="p">.</span><span class="nf">NotFound</span><span class="p">());</span>

<span class="n">app</span><span class="p">.</span><span class="nf">Run</span><span class="p">();</span>

<span class="k">public</span> <span class="n">record</span> <span class="nf">Todo</span><span class="p">(</span>
    <span class="kt">int</span> <span class="n">Id</span><span class="p">,</span>
    <span class="kt">string</span><span class="p">?</span> <span class="n">Title</span><span class="p">,</span>
    <span class="n">DateOnly</span><span class="p">?</span> <span class="n">DueBy</span> <span class="p">=</span> <span class="k">null</span><span class="p">,</span>
    <span class="kt">bool</span> <span class="n">IsComplete</span> <span class="p">=</span> <span class="k">false</span><span class="p">);</span>

<span class="p">[</span><span class="nf">JsonSerializable</span><span class="p">(</span><span class="k">typeof</span><span class="p">(</span><span class="n">Todo</span><span class="p">[]))]</span>
<span class="k">internal</span> <span class="k">partial</span> <span class="k">class</span> <span class="nc">AppJsonSerializerContext</span> <span class="p">:</span> <span class="n">JsonSerializerContext</span>
<span class="p">{</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Next, we need to install a couple of NuGet packages</p>

<ul>
  <li><a href="https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.JwtBearer">Microsoft.AspNetCore.Authentication.JwtBearer</a> - as of writing v9.0.1</li>
  <li><a href="https://www.nuget.org/packages/Microsoft.AspNetCore.OpenApi">Microsoft.AspNetCore.OpenApi</a> - as of writing v9.0.1</li>
  <li><a href="https://www.nuget.org/packages/Scalar.AspNetCore">Scalar.AspNetCore</a> - as of writing v1.2.*</li>
</ul>

<p>The <code class="language-plaintext highlighter-rouge">.csproj</code> file should now look like this:</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;Project</span> <span class="na">Sdk=</span><span class="s">"Microsoft.NET.Sdk.Web"</span><span class="nt">&gt;</span>

  <span class="nt">&lt;PropertyGroup&gt;</span>
    <span class="nt">&lt;TargetFramework&gt;</span>net9.0<span class="nt">&lt;/TargetFramework&gt;</span>
    <span class="nt">&lt;Nullable&gt;</span>enable<span class="nt">&lt;/Nullable&gt;</span>
    <span class="nt">&lt;ImplicitUsings&gt;</span>enable<span class="nt">&lt;/ImplicitUsings&gt;</span>
    <span class="nt">&lt;InvariantGlobalization&gt;</span>true<span class="nt">&lt;/InvariantGlobalization&gt;</span>
    <span class="nt">&lt;PublishAot&gt;</span>true<span class="nt">&lt;/PublishAot&gt;</span>
  <span class="nt">&lt;/PropertyGroup&gt;</span>

  <span class="nt">&lt;ItemGroup&gt;</span>
    <span class="nt">&lt;PackageReference</span> <span class="na">Include=</span><span class="s">"Microsoft.AspNetCore.Authentication.JwtBearer"</span> <span class="na">Version=</span><span class="s">"9.0.1"</span> <span class="nt">/&gt;</span>
    <span class="nt">&lt;PackageReference</span> <span class="na">Include=</span><span class="s">"Microsoft.AspNetCore.OpenApi"</span> <span class="na">Version=</span><span class="s">"9.0.1"</span> <span class="nt">/&gt;</span>
    <span class="nt">&lt;PackageReference</span> <span class="na">Include=</span><span class="s">"Scalar.AspNetCore"</span> <span class="na">Version=</span><span class="s">"1.2.*"</span> <span class="nt">/&gt;</span>
  <span class="nt">&lt;/ItemGroup&gt;</span>

<span class="nt">&lt;/Project&gt;</span>
</code></pre></div></div>

<p>Next, we need to configure the API to expose OpenAPI specifications
using the Microsoft OpenAPI toolset. We need to register the
Microsoft OpenAPI dependencies using the <code class="language-plaintext highlighter-rouge">AddOpenApi()</code>
extension method to <code class="language-plaintext highlighter-rouge">IServiceCollection</code> and configure the middleware
using the <code class="language-plaintext highlighter-rouge">UseOpenApi()</code> on the <code class="language-plaintext highlighter-rouge">WebApplication</code></p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">builder</span> <span class="p">=</span> <span class="n">WebApplication</span><span class="p">.</span><span class="nf">CreateSlimBuilder</span><span class="p">(</span><span class="n">args</span><span class="p">);</span>
<span class="n">builder</span><span class="p">.</span><span class="n">Services</span><span class="p">.</span><span class="nf">ConfigureHttpJsonOptions</span><span class="p">(</span>
    <span class="n">options</span> <span class="p">=&gt;</span>
    <span class="p">{</span>
        <span class="n">options</span><span class="p">.</span><span class="n">SerializerOptions</span><span class="p">.</span><span class="n">TypeInfoResolverChain</span><span class="p">.</span><span class="nf">Insert</span><span class="p">(</span>
            <span class="m">0</span><span class="p">,</span>
            <span class="n">AppJsonSerializerContext</span><span class="p">.</span><span class="n">Default</span><span class="p">);</span>
    <span class="p">})</span>
    <span class="p">.</span><span class="nf">AddOpenApi</span><span class="p">();</span>

<span class="p">...</span>

<span class="kt">var</span> <span class="n">app</span> <span class="p">=</span> <span class="n">builder</span><span class="p">.</span><span class="nf">Build</span><span class="p">();</span>
<span class="n">app</span><span class="p">.</span><span class="nf">UseOpenApi</span><span class="p">();</span>

<span class="p">...</span>
</code></pre></div></div>

<p>Next, we configure Scalar. This is done setting up the Scalar middleware
calling <code class="language-plaintext highlighter-rouge">MapScalarApiReference()</code> on the <code class="language-plaintext highlighter-rouge">WebApplication</code>.
You will need to import the <code class="language-plaintext highlighter-rouge">Scalar.AspNetCore</code> namespace</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">Scalar.AspNetCore</span><span class="p">;</span>

<span class="p">...</span>

<span class="kt">var</span> <span class="n">app</span> <span class="p">=</span> <span class="n">builder</span><span class="p">.</span><span class="nf">Build</span><span class="p">();</span>
<span class="n">app</span><span class="p">.</span><span class="nf">UseOpenApi</span><span class="p">();</span>
<span class="n">app</span><span class="p">.</span><span class="nf">MapScalarApiReference</span><span class="p">();</span>

<span class="p">...</span>
</code></pre></div></div>

<p>With this, we should be able to see the Scalar page on <code class="language-plaintext highlighter-rouge">/scalar/v1</code>.
It looks something like this:</p>

<p><img src="/assets/images/scalar-default.png" alt="scalar default" /></p>

<p><img src="/assets/images/scalar-get-todos-preview.png" alt="scalar get todos" /></p>

<p>Clicking on the <strong>Test Request</strong> button from the <code class="language-plaintext highlighter-rouge">GET /todos</code> section will allow
you to perform tests against the endpoint. With the current setup,
it would look something like this:</p>

<p><img src="/assets/images/scalar-get-todos-results.png" alt="scalar get todos result" /></p>

<p>By now, we have a fully functional API without any security.</p>

<p>Next, let’s secure the API. For this example we use JWT Bearer tokens
containing the audience and role claims. To do this,
we use <code class="language-plaintext highlighter-rouge">AddAuthentication()</code> and <code class="language-plaintext highlighter-rouge">AddAuthorization()</code> on the <code class="language-plaintext highlighter-rouge">IServiceCollection</code>
and enable middleware using <code class="language-plaintext highlighter-rouge">UseAuthentication()</code> and <code class="language-plaintext highlighter-rouge">UseAuthorization()</code>
on the <code class="language-plaintext highlighter-rouge">WebApplication</code>.</p>

<p>We need to configure the API require the JWT Bearer token to have the following claims</p>

<ul>
  <li>
    <p><code class="language-plaintext highlighter-rouge">aud</code> - Identifies the intended recipient of the token.
In access_tokens and id_tokens, the audience is the
App Registration Application URI ID,
specified under the <strong>Expose an API</strong> section of your App Registration,
or if not specified it is the App Registration Application ID
assigned to your app in the Azure portal. Your app should validate this value,
and reject the token if the value does not match.</p>
  </li>
  <li>
    <p><code class="language-plaintext highlighter-rouge">iss</code>- Identifies the security token service (STS) that constructs
and returns the token, and the Azure Entra ID tenant in which the
user was authenticated. If the token was issued by the v2.0 endpoint,
the URI will end in /v2.0. The app should use the GUID portion of the
claim to restrict the set of tenants that can sign in to the app, if applicable.</p>
  </li>
  <li>
    <p><code class="language-plaintext highlighter-rouge">role</code> - The set of permissions exposed by your application that the
requesting application has been given permission to call.
This is used during the client-credentials flow in place of user scopes,
and is only present in applications tokens.</p>
  </li>
</ul>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">builder</span> <span class="p">=</span> <span class="n">WebApplication</span><span class="p">.</span><span class="nf">CreateSlimBuilder</span><span class="p">(</span><span class="n">args</span><span class="p">);</span>
<span class="n">builder</span><span class="p">.</span><span class="n">Services</span><span class="p">.</span><span class="nf">ConfigureHttpJsonOptions</span><span class="p">(</span>
    <span class="n">options</span> <span class="p">=&gt;</span>
    <span class="p">{</span>
        <span class="n">options</span><span class="p">.</span><span class="n">SerializerOptions</span><span class="p">.</span><span class="n">TypeInfoResolverChain</span><span class="p">.</span><span class="nf">Insert</span><span class="p">(</span>
            <span class="m">0</span><span class="p">,</span>
            <span class="n">AppJsonSerializerContext</span><span class="p">.</span><span class="n">Default</span><span class="p">);</span>
    <span class="p">})</span>
    <span class="p">.</span><span class="nf">AddOpenApi</span><span class="p">()</span>
    <span class="p">.</span><span class="nf">AddAuthorization</span><span class="p">()</span>
    <span class="p">.</span><span class="nf">AddAuthentication</span><span class="p">()</span>
    <span class="p">.</span><span class="nf">AddJwtBearer</span><span class="p">(</span><span class="n">o</span> <span class="p">=&gt;</span>
        <span class="p">{</span>
            <span class="n">o</span><span class="p">.</span><span class="n">Audience</span> <span class="p">=</span> <span class="s">"api://[app registration client id]"</span><span class="p">;</span>
            <span class="n">o</span><span class="p">.</span><span class="n">Authority</span> <span class="p">=</span> <span class="s">"https://login.microsoftonline.com/[tenant id]"</span><span class="p">;</span>
        <span class="p">});</span>

<span class="p">...</span>

<span class="kt">var</span> <span class="n">app</span> <span class="p">=</span> <span class="n">builder</span><span class="p">.</span><span class="nf">Build</span><span class="p">();</span>
<span class="n">app</span><span class="p">.</span><span class="nf">UseOpenApi</span><span class="p">();</span>
<span class="n">app</span><span class="p">.</span><span class="nf">UseAuthorization</span><span class="p">();</span>
<span class="n">app</span><span class="p">.</span><span class="nf">UseAuthentication</span><span class="p">();</span>
<span class="n">app</span><span class="p">.</span><span class="nf">MapScalarApiReference</span><span class="p">();</span>

<span class="p">...</span>
</code></pre></div></div>

<p>The Audience and Authority will be used in multiple places so it’s best to extract a constants class for this. While we’re at it, let’s add some other constants that will be used later</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">internal</span> <span class="k">class</span> <span class="nc">Constants</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="k">const</span> <span class="kt">string</span> <span class="n">Authority</span> <span class="p">=</span> <span class="s">"https://login.microsoftonline.com/[tenant id]"</span><span class="p">;</span>
    <span class="k">public</span> <span class="k">const</span> <span class="kt">string</span> <span class="n">ClientId</span> <span class="p">=</span> <span class="s">"[Scalar App Registration Application ID]"</span><span class="p">;</span>
    <span class="k">public</span> <span class="k">const</span> <span class="kt">string</span> <span class="n">Audience</span> <span class="p">=</span> <span class="s">"api://[app registration client id]"</span><span class="p">;</span>
    <span class="k">public</span> <span class="k">const</span> <span class="kt">string</span> <span class="n">DefaultScope</span> <span class="p">=</span> <span class="s">$"</span><span class="p">{</span><span class="n">Audience</span><span class="p">}</span><span class="s">/.default"</span><span class="p">;</span>
    <span class="k">public</span> <span class="k">const</span> <span class="kt">string</span> <span class="n">Scheme</span> <span class="p">=</span> <span class="s">"Bearer"</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Now we need to require roles to access our <code class="language-plaintext highlighter-rouge">todos</code> endpoints. Let’s keep it simple, and use a single role called <code class="language-plaintext highlighter-rouge">todo.read</code>. To setup this up we the <code class="language-plaintext highlighter-rouge">RequireAuthorization()</code> extension method and configure it’s options to use <code class="language-plaintext highlighter-rouge">RequireRole("todo.read")</code></p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">...</span>

<span class="kt">var</span> <span class="n">todosApi</span> <span class="p">=</span> <span class="n">app</span><span class="p">.</span><span class="nf">MapGroup</span><span class="p">(</span><span class="s">"/todos"</span><span class="p">);</span>
<span class="n">todosApi</span>
    <span class="p">.</span><span class="nf">MapGet</span><span class="p">(</span><span class="s">"/"</span><span class="p">,</span> <span class="p">()</span> <span class="p">=&gt;</span> <span class="n">sampleTodos</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">RequireAuthorization</span><span class="p">(</span><span class="n">o</span> <span class="p">=&gt;</span> <span class="n">o</span><span class="p">.</span><span class="nf">RequireRole</span><span class="p">(</span><span class="s">"todo.read"</span><span class="p">));</span>

<span class="n">todosApi</span>
    <span class="p">.</span><span class="nf">MapGet</span><span class="p">(</span><span class="s">"/{id}"</span><span class="p">,</span> <span class="p">(</span><span class="kt">int</span> <span class="n">id</span><span class="p">)</span> <span class="p">=&gt;</span>
        <span class="n">sampleTodos</span><span class="p">.</span><span class="nf">FirstOrDefault</span><span class="p">(</span><span class="n">a</span> <span class="p">=&gt;</span> <span class="n">a</span><span class="p">.</span><span class="n">Id</span> <span class="p">==</span> <span class="n">id</span><span class="p">)</span> <span class="k">is</span> <span class="p">{</span> <span class="p">}</span> <span class="n">todo</span>
            <span class="p">?</span> <span class="n">Results</span><span class="p">.</span><span class="nf">Ok</span><span class="p">(</span><span class="n">todo</span><span class="p">)</span>
            <span class="p">:</span> <span class="n">Results</span><span class="p">.</span><span class="nf">NotFound</span><span class="p">())</span>
    <span class="p">.</span><span class="nf">RequireAuthorization</span><span class="p">(</span><span class="n">o</span> <span class="p">=&gt;</span> <span class="n">o</span><span class="p">.</span><span class="nf">RequireRole</span><span class="p">(</span><span class="s">"todo.read"</span><span class="p">));</span>

<span class="p">...</span>
</code></pre></div></div>

<p>Right now, we have no way of retrieving an JWT Bearer token from Scalar itself. But you can always acquire an access token yourself, one way to do this is to use <a href="https://learn.microsoft.com/en-us/cli/azure/account?view=azure-cli-latest#az-account-get-access-token?WT.mc_id=DT-MVP-5004822">Azure CLI</a> with the following command, assuming that you are logged in to the same tenant and have been granted the <code class="language-plaintext highlighter-rouge">todo.read</code> role on the Azure Entra ID Enterprise Application associated to the App Registration.</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">az</span><span class="w"> </span><span class="nx">account</span><span class="w"> </span><span class="nx">get-access-token</span><span class="w"> </span><span class="nt">--scope</span><span class="w"> </span><span class="p">[</span><span class="n">Some</span><span class="w"> </span><span class="n">Application</span><span class="w"> </span><span class="n">ID</span><span class="w"> </span><span class="n">URI</span><span class="p">]</span><span class="nx">/.default</span><span class="w">
</span></code></pre></div></div>

<p>To enable OAuth2 authentication on Scalar, we need to ensure that the OpenAPI document exposed by the API defines this <a href="https://learn.openapis.org/specification/security.html">securitySchemes</a>.
To do so, we need to setup a <a href="https://learn.microsoft.com/en-us/aspnet/core/fundamentals/openapi/customize-openapi?view=aspnetcore-10.0&amp;WT.mc_id=DT-MVP-5004822">Document Transformer</a> things in <code class="language-plaintext highlighter-rouge">AddOpenApi()</code>. A Document Transformer implements the <code class="language-plaintext highlighter-rouge">IOpenApiDocumentTransformer</code> interface. If you have previously worked with <a href="https://github.com/domaindrivendev/Swashbuckle.AspNetCore">Swashbuckle.AspNetCore</a>, then this should feel familiar to you.</p>

<p>The following code confgures a security scheme that enables the OAuth2 Implicit grant flow</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">Microsoft.AspNetCore.OpenApi</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">Microsoft.OpenApi.Models</span><span class="p">;</span>

<span class="k">public</span> <span class="k">class</span> <span class="nc">OpenApiSecuritySchemeTransformer</span>
    <span class="p">:</span> <span class="n">IOpenApiDocumentTransformer</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="n">Task</span> <span class="nf">TransformAsync</span><span class="p">(</span>
        <span class="n">OpenApiDocument</span> <span class="n">document</span><span class="p">,</span> 
        <span class="n">OpenApiDocumentTransformerContext</span> <span class="n">context</span><span class="p">,</span>
        <span class="n">CancellationToken</span> <span class="n">cancellationToken</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="kt">var</span> <span class="n">securitySchema</span> <span class="p">=</span>
            <span class="k">new</span> <span class="n">OpenApiSecurityScheme</span>
            <span class="p">{</span>
                <span class="n">Type</span> <span class="p">=</span> <span class="n">SecuritySchemeType</span><span class="p">.</span><span class="n">OAuth2</span><span class="p">,</span>
                <span class="n">Scheme</span> <span class="p">=</span> <span class="s">"Bearer"</span><span class="p">,</span>
                <span class="n">BearerFormat</span> <span class="p">=</span> <span class="s">"JWT"</span><span class="p">,</span>
                <span class="n">Flows</span> <span class="p">=</span> <span class="k">new</span> <span class="n">OpenApiOAuthFlows</span>
                <span class="p">{</span>
                    <span class="n">Implicit</span> <span class="p">=</span> <span class="k">new</span> <span class="n">OpenApiOAuthFlow</span>
                    <span class="p">{</span>
                        <span class="n">AuthorizationUrl</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">Uri</span><span class="p">(</span><span class="s">$"</span><span class="p">{</span><span class="n">Constants</span><span class="p">.</span><span class="n">Authority</span><span class="p">}</span><span class="s">/oauth2/v2.0/authorize"</span><span class="p">),</span>
                        <span class="n">TokenUrl</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">Uri</span><span class="p">(</span><span class="s">$"</span><span class="p">{</span><span class="n">Constants</span><span class="p">.</span><span class="n">Authority</span><span class="p">}</span><span class="s">/oauth2/v2.0/token"</span><span class="p">),</span>
                        <span class="n">Scopes</span> <span class="p">=</span> <span class="k">new</span> <span class="n">Dictionary</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">,</span> <span class="kt">string</span><span class="p">&gt;</span>
                        <span class="p">{</span>
                            <span class="p">{</span> <span class="n">Constants</span><span class="p">.</span><span class="n">DefaultScope</span><span class="p">,</span> <span class="s">"Access the API"</span> <span class="p">}</span>
                        <span class="p">}</span>
                    <span class="p">}</span>
                <span class="p">}</span>
            <span class="p">};</span>

        <span class="kt">var</span> <span class="n">securityRequirement</span> <span class="p">=</span>
            <span class="k">new</span> <span class="n">OpenApiSecurityRequirement</span>
            <span class="p">{</span>
                <span class="p">{</span>
                    <span class="k">new</span> <span class="n">OpenApiSecurityScheme</span>
                    <span class="p">{</span>
                        <span class="n">Reference</span> <span class="p">=</span> <span class="k">new</span> <span class="n">OpenApiReference</span>
                        <span class="p">{</span>
                            <span class="n">Id</span> <span class="p">=</span> <span class="s">"Bearer"</span><span class="p">,</span>
                            <span class="n">Type</span> <span class="p">=</span> <span class="n">ReferenceType</span><span class="p">.</span><span class="n">SecurityScheme</span><span class="p">,</span>
                        <span class="p">},</span>
                    <span class="p">},</span>
                    <span class="p">[]</span>
                <span class="p">}</span>
            <span class="p">};</span>

        <span class="n">document</span><span class="p">.</span><span class="n">SecurityRequirements</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="n">securityRequirement</span><span class="p">);</span>
        <span class="n">document</span><span class="p">.</span><span class="n">Components</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">OpenApiComponents</span><span class="p">()</span>
        <span class="p">{</span>
            <span class="n">SecuritySchemes</span> <span class="p">=</span> <span class="k">new</span> <span class="n">Dictionary</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">,</span> <span class="n">OpenApiSecurityScheme</span><span class="p">&gt;()</span>
            <span class="p">{</span>
                <span class="p">{</span> <span class="s">"Bearer"</span><span class="p">,</span> <span class="n">securitySchema</span> <span class="p">}</span>
            <span class="p">}</span>
        <span class="p">};</span>

        <span class="k">return</span> <span class="n">Task</span><span class="p">.</span><span class="n">CompletedTask</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Now that we have a Document Transformer, we need to use it by calling <code class="language-plaintext highlighter-rouge">AddDocumentTransformer&lt;OpenApiSecuritySchemeTransformer&gt;()</code> in the <code class="language-plaintext highlighter-rouge">OpenApiOptions</code> provided upon <code class="language-plaintext highlighter-rouge">AddOpenApi()</code></p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">builder</span> <span class="p">=</span> <span class="n">WebApplication</span><span class="p">.</span><span class="nf">CreateSlimBuilder</span><span class="p">(</span><span class="n">args</span><span class="p">);</span>
<span class="n">builder</span><span class="p">.</span><span class="n">Services</span><span class="p">.</span><span class="nf">ConfigureHttpJsonOptions</span><span class="p">(</span>
    <span class="n">options</span> <span class="p">=&gt;</span>
    <span class="p">{</span>
        <span class="n">options</span><span class="p">.</span><span class="n">SerializerOptions</span><span class="p">.</span><span class="n">TypeInfoResolverChain</span><span class="p">.</span><span class="nf">Insert</span><span class="p">(</span>
            <span class="m">0</span><span class="p">,</span>
            <span class="n">AppJsonSerializerContext</span><span class="p">.</span><span class="n">Default</span><span class="p">);</span>
    <span class="p">})</span>
    <span class="p">.</span><span class="nf">AddOpenApi</span><span class="p">(</span><span class="n">o</span> <span class="p">=&gt;</span> <span class="n">o</span><span class="p">.</span><span class="n">AddDocumentTransformer</span><span class="p">&lt;</span><span class="n">OpenApiSecuritySchemeTransformer</span><span class="p">&gt;())</span>
    <span class="p">.</span><span class="nf">AddAuthorization</span><span class="p">()</span>
    <span class="p">.</span><span class="nf">AddAuthentication</span><span class="p">()</span>
    <span class="p">.</span><span class="nf">AddJwtBearer</span><span class="p">(</span><span class="n">o</span> <span class="p">=&gt;</span>
        <span class="p">{</span>
            <span class="n">o</span><span class="p">.</span><span class="n">Audience</span> <span class="p">=</span> <span class="s">"api://[app registration client id]"</span><span class="p">;</span>
            <span class="n">o</span><span class="p">.</span><span class="n">Authority</span> <span class="p">=</span> <span class="s">"https://login.microsoftonline.com/[tenant id]"</span><span class="p">;</span>
        <span class="p">});</span>

<span class="p">...</span>

<span class="kt">var</span> <span class="n">app</span> <span class="p">=</span> <span class="n">builder</span><span class="p">.</span><span class="nf">Build</span><span class="p">();</span>
<span class="n">app</span><span class="p">.</span><span class="nf">UseOpenApi</span><span class="p">();</span>
<span class="n">app</span><span class="p">.</span><span class="nf">UseAuthorization</span><span class="p">();</span>
<span class="n">app</span><span class="p">.</span><span class="nf">UseAuthentication</span><span class="p">();</span>
<span class="n">app</span><span class="p">.</span><span class="nf">MapScalarApiReference</span><span class="p">();</span>

<span class="p">...</span>
</code></pre></div></div>

<p>This should add the <code class="language-plaintext highlighter-rouge">securityScheme</code> to the OpenAPI document <code class="language-plaintext highlighter-rouge">components</code> when retrieving it from the <code class="language-plaintext highlighter-rouge">/openapi/v1.json</code> URL</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nl">"components"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="err">...</span><span class="w">
  </span><span class="nl">"securitySchemes"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"Bearer"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"oauth2"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"flows"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"implicit"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
          </span><span class="nl">"authorizationUrl"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://login.microsoftonline.com/[tenant id]/oauth2/v2.0authorize"</span><span class="p">,</span><span class="w">
          </span><span class="nl">"tokenUrl"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://login.microsoftonline.com/[tenant id]/oauth2/v2.0/token"</span><span class="p">,</span><span class="w">
          </span><span class="nl">"scopes"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="nl">"api://[app registration client id]/.default"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Access the API"</span><span class="w">
          </span><span class="p">}</span><span class="w">
        </span><span class="p">}</span><span class="w">
      </span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>When we run the API and browser to the Scalar page then we should now see that the <strong>Auth Type</strong> dropdown now has a <strong>Bearer</strong> option</p>

<p><img src="/assets/images/scalar-auth-bearer.png" alt="Scalar bearer auth type option" /></p>

<p>and when selecting <strong>Bearer</strong> you will see pre-populated options for OAuth2 and a pre-checked scope</p>

<p><img src="/assets/images/scalar-authorize.png" alt="Scalar implicit grant flow" /></p>

<p>Clicking on <strong>Authorize</strong> should open a window prompting for user credentials</p>

<p><img src="/assets/images/scalar-azure.png" alt="Azure login" /></p>

<p>and upon successful authentication, the access token is copied over to Scalar</p>

<p><img src="/assets/images/scalar-token.png" alt="Scalar bearer token" /></p>

<p>and is automatically used when sending API requests from Scalar</p>

<p><img src="/assets/images/scalar-use-token.png" alt="Scalar use bearer token" /></p>

<p>I published an example project to <a href="https://github.com/christianhelle/ScalarAzureAuthentication">Github</a> if you want to try it out yourself.</p>]]></content><author><name>Christian Helle</name></author><category term="Scalar" /><category term="OpenAPI" /><category term=".NET 9.0" /><summary type="html"><![CDATA[Swagger UI and Swashbuckle.AspNetCore are two popular tools for working with OpenAPI for ASP.NET Core Web APIs. Both tools have come with benefits and problems, but that said, most .NET developers have gotten used to it. Unfortunately, in May 2024, Microsoft announced that Swashbuckle.AspNetCore will be removed from .NET 9.0. For the past year or so, I started using .http files in favor of Swagger UI. Using .http files immediately lead me to develop HTTP File Generator, a tool that can generate a suite of .http files from OpenAPI specifications]]></summary></entry></feed>