<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Amit</title>
    <description>The latest articles on DEV Community by Amit (@xgadhia).</description>
    <link>https://dev.to/xgadhia</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4088361%2Fd3f0a0d0-1784-40c7-8d44-d4985d6c395c.jpg</url>
      <title>DEV Community: Amit</title>
      <link>https://dev.to/xgadhia</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/xgadhia"/>
    <language>en</language>
    <item>
      <title>ReverseProxy panics on client disconnect and your cleanup code never runs</title>
      <dc:creator>Amit</dc:creator>
      <pubDate>Fri, 21 Aug 2026 15:32:27 +0000</pubDate>
      <link>https://dev.to/xgadhia/reverseproxy-panics-on-client-disconnect-and-your-cleanup-code-never-runs-1ldh</link>
      <guid>https://dev.to/xgadhia/reverseproxy-panics-on-client-disconnect-and-your-cleanup-code-never-runs-1ldh</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F02li3k1s7jwmq530rhao.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F02li3k1s7jwmq530rhao.png" alt=" " width="800" height="336"&gt;&lt;/a&gt;I maintain a proxy that tracks how much each caller spends on LLM API calls and cuts them off at a limit. Before adding anything else to it, I wanted to know whether the numbers it reports are actually right — not "does it compile" right, but right after a restart and right when two hundred requests arrive at once.&lt;/p&gt;

&lt;p&gt;I went through the issue tracker of a much larger project doing a similar job, looking for cases where someone's spend numbers had come out wrong in production, and wrote a test for each failure mode against my own code.&lt;/p&gt;

&lt;p&gt;Three of them failed the first time I ran them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The cancelled stream&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The proxy uses &lt;code&gt;httputil.ReverseProxy&lt;/code&gt;. My bookkeeping ran right after:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;go&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Proxy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;ServeHTTP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ResponseWriter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ServeHTTP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c"&gt;// record usage&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which is fine until a client hangs up mid-response. &lt;code&gt;ReverseProxy&lt;/code&gt; doesn't return normally in that case. It panics with &lt;code&gt;http.ErrAbortHandler&lt;/code&gt;, a value &lt;code&gt;net/http&lt;/code&gt; recognizes and treats as a closed connection rather than a crash. The panic unwinds straight past everything after &lt;code&gt;ServeHTTP&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So a request could be served, cost real money, and leave no record at all. Nothing logged, nothing crashed, nothing counted.&lt;/p&gt;

&lt;p&gt;The fix is a &lt;code&gt;defer&lt;/code&gt;, because deferred calls still run while a panic unwinds, before it reaches the stdlib's own per-request recovery:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;go&lt;/span&gt;
&lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;finish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ServeHTTP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I didn't recover the panic. &lt;code&gt;net/http&lt;/code&gt; already handles &lt;code&gt;ErrAbortHandler&lt;/code&gt; correctly and there was no reason to reimplement that.&lt;/p&gt;

&lt;p&gt;The connection pool&lt;/p&gt;

&lt;p&gt;Separate test, worse number. I fired two hundred concurrent requests at a fresh SQLite database and counted the rows. About 29% weren't there.&lt;/p&gt;

&lt;p&gt;SQLite allows one writer. Go's &lt;code&gt;database/sql&lt;/code&gt; opens a pool by default, which is the right choice for nearly every database and the wrong one here — a pool of writers is just more connections fighting over a single lock. Past &lt;code&gt;busy_timeout&lt;/code&gt; they fail with &lt;code&gt;SQLITE_BUSY&lt;/code&gt;, and the calling code could only log it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;go&lt;/span&gt;
&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SetMaxOpenConns&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Writes queue instead of colliding. It also made those inserts about 30x faster, since a pool blocked on one lock does a lot of expensive waiting that a queue skips.&lt;/p&gt;

&lt;p&gt;This isn't specific to what I'm building. Any Go service writing to SQLite under concurrency has this by default, and it fails quietly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The third one&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Smaller. A stored row showing $0 could mean the request was free, or the response didn't parse, or the model wasn't in the pricing table. All identical on disk. A dashboard reading that couldn't tell "nothing happened" from "something happened and I don't know what it cost," which is worse than being wrong because it doesn't look like a problem.&lt;/p&gt;

&lt;p&gt;Added a column recording which case applied.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One I left alone&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Budget checks happen before a request, costs get recorded after, and a round trip to the provider sits in between. Enough concurrent requests arriving at the boundary all see the old total and all get through. Worst case I measured: $2.99 recorded against a $1.00 limit at a hundred concurrent.&lt;/p&gt;

&lt;p&gt;Fixing it properly means reserving an estimate on the way in, truing it up on the way out, and releasing the reservation on every path that doesn't finish — disconnects, upstream errors, the process dying. Bigger and riskier than anything else here, so I wrote the bound down instead and opened an issue.&lt;/p&gt;

</description>
      <category>go</category>
      <category>testing</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
