fix: __get_headers mutates caller's config dict on multipart requests#83
Open
SiddarthAA wants to merge 1 commit intoJigsawStack:mainfrom
Open
fix: __get_headers mutates caller's config dict on multipart requests#83SiddarthAA wants to merge 1 commit intoJigsawStack:mainfrom
SiddarthAA wants to merge 1 commit intoJigsawStack:mainfrom
Conversation
Both Request and AsyncRequest stored a direct reference to the headers dict
from the caller's config TypedDict:
self.headers = config.get("headers", None) or {...}
During multipart file-upload requests __get_headers() then called:
self.headers.pop("Content-Type")
Because self.headers was the same object as config["headers"], this pop
permanently removed Content-Type from the shared config. Every subsequent
request reusing the same client config (which is what all service classes do)
would silently go out without Content-Type.
Fix:
- __init__ now copies the incoming headers with dict() so self.headers is
always an owned instance, never an alias into the config.
- __get_headers() makes a second local copy (caller_headers) before calling
.pop(), so neither self.headers nor the config are ever touched.
Adds tests/test_headers_mutation.py with 12 focused unit tests covering both
the sync and async clients. No API key or network access is required to run
them.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
So every service class (
Audio,Translate,Vision, etc.) creates a singleRequestConfig/AsyncRequestConfigin its__init__and reuses it for every method call on that instance.The bug was in how
Request.__init__(and the async version) was setting headers:This isn't copying anything — it's just storing a reference to the same dict object that the service class holds onto. So when
__get_headers()does:...during a multipart upload, it's actually wiping
Content-Typeout of the shared config for good. Every call after that — JSON POSTs, GETs, whatever — goes out withoutContent-Type: application/jsonand you start getting silent 400/422s from the server.The really annoying part is it only breaks on the second call, so it's easy to miss in basic tests and a pain to track down.
Same issue in both
request.pyandasync_request.py.Fix
I went with a two-level defence in both files so there's no way either the config or
self.headersgets touched:__init__— own the dictNow
self.headersis its own copy — whatever happens to it later, the caller's config is safe.__get_headers()— throwaway per-call copy.pop()only touches a local copy made fresh each call, soself.headersstays intact.Also cleaned up the leftover
headers.pop("Content-Type", None)inmake_request()in both files —__get_headers()already handles that, so it was just dead code sitting there.Tests
Added
test_headers_mutation.py— 12 tests, no API key or network needed:test_multipart_does_not_mutate_config_headerstest_repeated_multipart_calls_do_not_degrade__get_headers()twice returns identical resultstest_multipart_output_omits_content_typeContent-Typeso the HTTP library can set the boundarytest_json_request_includes_content_typeContent-Type: application/jsontest_custom_headers_propagatedtest_second_request_on_same_config_unaffectedRequestbuilt from the same config after a multipart call has correct headers — the original regression scenarioAll 6 run for both
Request(sync) andAsyncRequest(async) — 12/12 pass.Files changed
request.pyasync_request.pytest_headers_mutation.py(new)Contact
Thanks for reviewing — happy to fix something / reiterate if needed :D
siddartha_ay@protonmail.com | (everything about me at siddartha-ay.xyz )