DEV Community

Cover image for We killed nock with 500 lines of Python stdlib — and still recorded the OpenAI API
aman kumar chaursiya
aman kumar chaursiya

Posted on

We killed nock with 500 lines of Python stdlib — and still recorded the OpenAI API

We killed nock with 500 lines of stdlib — and still recorded the OpenAI API

A write-up for Hackathon Raptors' Zero Dependency hackathon.

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


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

The answer turned out to be less "HTTP client" and more "identity
problem." Forwarding a request is the easy 20%. The hard 80% is deciding
whether a request you're seeing right now is the same request you
recorded five minutes — or five days — ago.

The naive version breaks immediately

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

  • Date headers change every second.
  • X-Request-Id / X-Trace-Id headers are randomized per call by design.
  • JSON bodies carry timestamp, nonce, and idempotencyKey fields that are, again, deliberately different every time.
  • Multipart form bodies embed a randomized boundary string that every real HTTP client generates fresh. Stripping the known 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 key name gave no hint that it was volatile. A field like capturedAt 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 meta object three levels deep.

The fix: match on value shape, not just key name

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

So the normalizer does two passes over every JSON body, recursively:

  1. If the key matches a volatile-name pattern (timestamp, nonce, .*id$, session[_-]?id, ...) → replace the value with a fixed placeholder.
  2. Independently, if the value itself 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.

We wrote a test that encodes exactly this bug —
test_iso8601_values_are_normalized_regardless_of_key_name — using a
capturedAt field specifically because it's a realistic key name that a
naive keyword-matching regex would miss.

The second problem: what if the API you need to record is HTTPS-only?

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

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

client = OpenAI(base_url="http://localhost:8888/v1", api_key="sk-...")
Enter fullscreen mode Exit fullscreen mode

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

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

The part that actually worried us: secrets ending up in git

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

The fix is two-layered, on purpose:

  1. Secret-bearing headers (Authorization, X-Api-Key, Cookie, and a short explicit list of others) are stripped from the fingerprint, 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.
  2. Those same headers are separately redacted to <redacted> in what gets written to disk, after 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 different 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."

What we didn't build, on purpose

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

The packages we're honestly comparing against, not beating

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

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

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

Try it

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

https://github.com/amankumarchaursiya/vcrproxy

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

Top comments (0)