<?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: aman kumar chaursiya</title>
    <description>The latest articles on DEV Community by aman kumar chaursiya (@amanchaursiya).</description>
    <link>https://dev.to/amanchaursiya</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%2F3423732%2F69f16ea0-319b-40c1-8df8-2ccc5ced35ce.png</url>
      <title>DEV Community: aman kumar chaursiya</title>
      <link>https://dev.to/amanchaursiya</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/amanchaursiya"/>
    <language>en</language>
    <item>
      <title>We killed nock with 500 lines of Python stdlib — and still recorded the OpenAI API</title>
      <dc:creator>aman kumar chaursiya</dc:creator>
      <pubDate>Sat, 05 Sep 2026 05:02:31 +0000</pubDate>
      <link>https://dev.to/amanchaursiya/we-killed-nock-with-500-lines-of-python-stdlib-and-still-recorded-the-openai-api-4afo</link>
      <guid>https://dev.to/amanchaursiya/we-killed-nock-with-500-lines-of-python-stdlib-and-still-recorded-the-openai-api-4afo</guid>
      <description>&lt;h2&gt;
  
  
  We killed &lt;code&gt;nock&lt;/code&gt; with 500 lines of stdlib — and still recorded the OpenAI API
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;A write-up for Hackathon Raptors' Zero Dependency hackathon.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; We built a zero-dependency HTTP record/replay proxy in pure&lt;br&gt;
Python — no &lt;code&gt;pip install&lt;/code&gt; beyond CPython itself. Along the way we hit&lt;br&gt;
three real problems that don't show up until you actually try to ship&lt;br&gt;
this kind of tool: request matching breaks the moment real APIs put&lt;br&gt;
timestamps and UUIDs in unpredictable places, HTTPS APIs like OpenAI's&lt;br&gt;
looked impossible to record without a crypto library, and the moment&lt;br&gt;
that got solved, we nearly wrote API keys straight into a file meant for&lt;br&gt;
git. This is the write-up of how each of those actually got fixed.&lt;/p&gt;



&lt;p&gt;Every project that talks to an external API eventually needs its tests&lt;br&gt;
to stop talking to that API. It's slow, it costs money, and it breaks in&lt;br&gt;
CI the moment the vendor has a bad day. The standard fix is a mocking&lt;br&gt;
library — &lt;code&gt;nock&lt;/code&gt; if you're in Node, &lt;code&gt;VCR.py&lt;/code&gt;/&lt;code&gt;responses&lt;/code&gt; if you're in&lt;br&gt;
Python, &lt;code&gt;WireMock&lt;/code&gt; if you're in Java. We wanted to know: what does that&lt;br&gt;
library actually &lt;em&gt;do&lt;/em&gt;, once you strip away the package boundary?&lt;/p&gt;

&lt;p&gt;The answer turned out to be less "HTTP client" and more "identity&lt;br&gt;
problem." Forwarding a request is the easy 20%. The hard 80% is deciding&lt;br&gt;
whether a request you're seeing right now is &lt;em&gt;the same request&lt;/em&gt; you&lt;br&gt;
recorded five minutes — or five days — ago.&lt;/p&gt;
&lt;h2&gt;
  
  
  The naive version breaks immediately
&lt;/h2&gt;

&lt;p&gt;Our first pass hashed the raw request bytes: method, URL, headers, body,&lt;br&gt;
sha256, done. It worked for exactly one test run. The second run failed&lt;br&gt;
every single match, because nothing about a real HTTP request is&lt;br&gt;
actually stable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Date&lt;/code&gt; headers change every second.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;X-Request-Id&lt;/code&gt; / &lt;code&gt;X-Trace-Id&lt;/code&gt; headers are randomized per call by design.&lt;/li&gt;
&lt;li&gt;JSON bodies carry &lt;code&gt;timestamp&lt;/code&gt;, &lt;code&gt;nonce&lt;/code&gt;, and &lt;code&gt;idempotencyKey&lt;/code&gt; fields that
are, again, deliberately different every time.&lt;/li&gt;
&lt;li&gt;Multipart form bodies embed a randomized boundary string that every
real HTTP client generates fresh.
Stripping the &lt;em&gt;known&lt;/em&gt; volatile headers fixed most of it quickly. What
didn't get fixed — and what became the actual afternoon-eater — was JSON
bodies where the volatile field's &lt;em&gt;key name&lt;/em&gt; gave no hint that it was
volatile. A field like &lt;code&gt;capturedAt&lt;/code&gt; doesn't match any reasonable
"this looks like a timestamp field" regex on the key alone. Neither does
a UUID sitting inside a nested, arbitrarily-named &lt;code&gt;meta&lt;/code&gt; object three
levels deep.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  The fix: match on value shape, not just key name
&lt;/h2&gt;

&lt;p&gt;The insight that unblocked us: you don't need to guess every possible&lt;br&gt;
volatile field &lt;em&gt;name&lt;/em&gt; an API author might choose. You need to recognize&lt;br&gt;
volatile field &lt;em&gt;values&lt;/em&gt; — an ISO-8601 timestamp and a UUID have&lt;br&gt;
extremely recognizable shapes regardless of what key they're filed under.&lt;/p&gt;

&lt;p&gt;So the normalizer does two passes over every JSON body, recursively:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;If the &lt;em&gt;key&lt;/em&gt; matches a volatile-name pattern (&lt;code&gt;timestamp&lt;/code&gt;, &lt;code&gt;nonce&lt;/code&gt;,
&lt;code&gt;.*id$&lt;/code&gt;, &lt;code&gt;session[_-]?id&lt;/code&gt;, ...) → replace the value with a fixed
placeholder.&lt;/li&gt;
&lt;li&gt;Independently, if the &lt;em&gt;value itself&lt;/em&gt; matches an ISO-8601 timestamp
regex or a UUID regex, regardless of key name → replace it too.
That second pass is the whole fix. It's about fifteen lines of code. It
took an afternoon to notice it was missing, because the failures it
prevents don't show up as crashes — they show up as replay silently
returning "no cassette match" for a request that a human would call
obviously identical to the one that was recorded.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We wrote a test that encodes exactly this bug —&lt;br&gt;
&lt;code&gt;test_iso8601_values_are_normalized_regardless_of_key_name&lt;/code&gt; — using a&lt;br&gt;
&lt;code&gt;capturedAt&lt;/code&gt; field specifically because it's a realistic key name that a&lt;br&gt;
naive keyword-matching regex would miss.&lt;/p&gt;
&lt;h2&gt;
  
  
  The second problem: what if the API you need to record is HTTPS-only?
&lt;/h2&gt;

&lt;p&gt;Almost every real API worth testing against — OpenAI, Stripe, GitHub —&lt;br&gt;
is HTTPS-only. Our proxy design can't decrypt someone else's TLS tunnel&lt;br&gt;
without a certificate authority, which needs a crypto library we&lt;br&gt;
deliberately refused to add (more on that below). So on paper, this tool&lt;br&gt;
looked useless for the exact APIs people actually integrate with.&lt;/p&gt;

&lt;p&gt;The fix wasn't cryptographic, it was architectural: stop trying to&lt;br&gt;
&lt;em&gt;intercept&lt;/em&gt; the client's HTTPS connection, and instead &lt;em&gt;become&lt;/em&gt; the&lt;br&gt;
client. Most API SDKs — OpenAI's included — let you override the base&lt;br&gt;
URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:8888/v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sk-...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Point the SDK at vcrproxy directly instead of &lt;code&gt;HTTP_PROXY&lt;/code&gt;-style&lt;br&gt;
tunneling, and the SDK now speaks plain HTTP to a local process. vcrproxy&lt;br&gt;
then makes its &lt;em&gt;own&lt;/em&gt; real HTTPS call to &lt;code&gt;api.openai.com&lt;/code&gt; — using&lt;br&gt;
&lt;code&gt;http.client.HTTPSConnection&lt;/code&gt;, which was already sitting in the codebase&lt;br&gt;
for the record path — gets back the decrypted response, and hands it&lt;br&gt;
over. No tunnel to break into, because there isn't a tunnel between the&lt;br&gt;
two parties whose traffic actually needs recording.&lt;/p&gt;

&lt;p&gt;This turned "can't record HTTPS" into "can record any HTTPS API whose&lt;br&gt;
client supports a custom base URL" — which covers most of the SDKs&lt;br&gt;
you'd actually use in a chatbot backend, a payments integration, or&lt;br&gt;
anything else built on a modern API.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that actually worried us: secrets ending up in git
&lt;/h2&gt;

&lt;p&gt;The moment "record OpenAI traffic" became real, so did a new problem: an&lt;br&gt;
&lt;code&gt;Authorization: Bearer sk-...&lt;/code&gt; header on every single request, about to&lt;br&gt;
be written into a JSON file that's &lt;em&gt;meant&lt;/em&gt; to be committed to a repo so&lt;br&gt;
teammates can replay it. Shipping a tool that quietly writes API keys&lt;br&gt;
into version-controlled fixtures is a worse outcome than not building&lt;br&gt;
the feature at all.&lt;/p&gt;

&lt;p&gt;The fix is two-layered, on purpose:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Secret-bearing headers (&lt;code&gt;Authorization&lt;/code&gt;, &lt;code&gt;X-Api-Key&lt;/code&gt;, &lt;code&gt;Cookie&lt;/code&gt;, and a
short explicit list of others) are stripped from the &lt;em&gt;fingerprint&lt;/em&gt;, so
a request still matches on replay even when it was recorded with one
API key and replayed with a completely different one — a teammate's
key, or one rotated since.&lt;/li&gt;
&lt;li&gt;Those same headers are separately redacted to &lt;code&gt;&amp;lt;redacted&amp;gt;&lt;/code&gt; in what
gets written to disk, &lt;em&gt;after&lt;/em&gt; the real value was already used to make
the actual outbound call. The live request goes out with your real
key; the cassette file never sees it.
We wrote a test that records a request with a fake key, greps the saved
cassette file for that key, and asserts it isn't there — and a second
test that replays with a &lt;em&gt;different&lt;/em&gt; key than what was recorded, to
prove the matching logic never depended on the secret's value in the
first place. Both were added specifically because "it worked when I
tried it" isn't the same guarantee as "it's asserted in a test that runs
every time."&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What we didn't build, on purpose
&lt;/h2&gt;

&lt;p&gt;Target mode covers HTTPS APIs whose client lets you point it somewhere&lt;br&gt;
else. It doesn't cover traffic from a client that hard-codes its host —&lt;br&gt;
a mobile app, a third-party binary, a browser hitting a real site — where&lt;br&gt;
the only way to see the request at all is a genuine man-in-the-middle:&lt;br&gt;
intercepting the connection and presenting a certificate for a domain you&lt;br&gt;
don't control. That requires generating and signing an X.509 certificate,&lt;br&gt;
which Python's &lt;code&gt;ssl&lt;/code&gt; module deliberately doesn't do — it can &lt;em&gt;load&lt;/em&gt; a&lt;br&gt;
cert, not &lt;em&gt;create and sign&lt;/em&gt; one. Doing that needs a real crypto library,&lt;br&gt;
or shelling out to an &lt;code&gt;openssl&lt;/code&gt; binary, which is a dependency wearing a&lt;br&gt;
disguise. We left that case unhandled and said so in the README, rather&lt;br&gt;
than adding an undisclosed external call to make the demo look more&lt;br&gt;
capable than it is.&lt;/p&gt;

&lt;h2&gt;
  
  
  The packages we're honestly comparing against, not beating
&lt;/h2&gt;

&lt;p&gt;Python already has good tools for this: &lt;code&gt;vcrpy&lt;/code&gt; (Production/Stable,&lt;br&gt;
six maintainers) and &lt;code&gt;responses&lt;/code&gt; (maintained by Sentry, huge install&lt;br&gt;
base). Both hook into Python's HTTP client internals to fake responses&lt;br&gt;
in-process. &lt;code&gt;vcrproxy&lt;/code&gt; takes a different architectural bet — run as an&lt;br&gt;
actual proxy server, so it works with any HTTP client in any language,&lt;br&gt;
not just the Python ones a package can patch — and that bet pays off in&lt;br&gt;
three concrete ways: an empty dependency manifest, language-agnostic&lt;br&gt;
capture, and a &lt;code&gt;diff&lt;/code&gt; mode that neither package has (&lt;code&gt;vcrpy&lt;/code&gt;'s own docs&lt;br&gt;
say the fix for a stale cassette is "delete it and re-record," which is&lt;br&gt;
a manual, after-the-fact discovery instead of a proactive check).&lt;/p&gt;

&lt;p&gt;It also costs something real, and we're not going to pretend otherwise:&lt;br&gt;
because a proxy sits outside the TLS handshake, it can't see decrypted&lt;br&gt;
HTTPS traffic the way an in-process patch can. &lt;code&gt;vcrpy&lt;/code&gt; and &lt;code&gt;responses&lt;/code&gt;&lt;br&gt;
both capture HTTPS content natively. &lt;code&gt;vcrproxy&lt;/code&gt; tunnels it and gives up&lt;br&gt;
on recording it, for the reasons in the HTTPS section above. Neither&lt;br&gt;
package has to make that trade because neither one gives up its runtime&lt;br&gt;
dependencies to get there.&lt;/p&gt;

&lt;p&gt;That, not a "we beat the incumbent" headline, is the actual story:&lt;br&gt;
a specific, disclosed trade of maturity and HTTPS coverage for an empty&lt;br&gt;
manifest, cross-language reach, and one feature (drift detection) the&lt;br&gt;
incumbents don't have at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Full source, 21 passing tests, an automated zero-dependency proof, and a&lt;br&gt;
reproducible build (two independent builds, byte-identical hashes) are&lt;br&gt;
all in the repo:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/amankumarchaursiya/vcrproxy" rel="noopener noreferrer"&gt;https://github.com/amankumarchaursiya/vcrproxy&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you're building something similar — or you've hit a worse version of&lt;br&gt;
the timestamp-matching problem than we did — I'd genuinely like to hear&lt;br&gt;
about it. Comments open below.&lt;/p&gt;

</description>
      <category>python</category>
      <category>testing</category>
      <category>hackathon</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
