{{ message }}
Repository navigation
Do not modify the headers dict the caller passed in - #3530
Merged
Merged
Conversation
RestMixin.rest() and FastHttpSession.request() wrote their default headers (Content-Type, Accept, Accept-Encoding, Authorization) into the dict they were given, so a shared headers dict silently gained JSON headers and changed unrelated later requests that reused it. Build a copy instead.
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.

What
RestMixin.rest()(locust/user/users.py) andFastHttpSession.request()(locust/contrib/fasthttp.py) both open withheaders = headers or {}and then write their own default headers into that object. That only protects a falsy argument, so a non-empty dict the caller keeps a reference to — typically a module levelHEADERSshared by every user — is modified in place and stays modified for the rest of the run.Each line now copies first:
headers = dict(headers) if headers else {}. The falsy case behaves exactly as before (a fresh empty dict), so callers that pass{}or nothing see no change.Fixes #3529
Why
In the reproduction from #3529 a single
rest()call is enough to break a later, unrelated request: the shared dict now claimsContent-Type: application/json, so the form body of the next request is no longer parsed by the server (form round trip: ''instead of'hello'). WithFastHttpUserboth sites are on the path at once, so the caller's dict also growsAccept-Encoding, andAuthorizationwheneverauth/auth_headeris used — a value that no line of the user's code ever assigned.headers=should be an input, not an in/out parameter.How it was checked
Three regression tests, all RED on
555ad9e5and GREEN with this change:locust/test/test_http.py::TestHttpUserRest::test_rest_does_not_modify_the_given_headerslocust/test/test_fasthttp.py::TestFastHttpUserClass::test_client_request_does_not_modify_the_given_headerslocust/test/test_fasthttp.py::TestFastHttpCatchResponse::test_rest_does_not_modify_the_given_headersEach one asserts both halves of the contract: the request still carries the defaults (
Content-Type, and the caller's ownX-Header-Test), and the caller's dict still carries only what the caller put in it.The two whole files, plus
test_dispatch.py:The 3 remaining failures are the same 3 on both sides, and none of them is a behaviour change caused by this diff:
TestHttpSession::test_event_measureasserts1 <= response_time <= 1.5and measured 14.7 s on this machine;TestHttpSession::test_verify_true_fails_with_bad_certgetsSSLCertVerificationError: certificate has expiredfrom the locally generated test certificate, which this urllib3 version does not wrap intourllib3.exceptions.SSLError;TestFastHttpSsl::test_ssl_request_insecurefails already insetUp, withPermissionError: [Errno 13] Permission deniedwhile writing the temporary certificate file on Windows.The 4th base failure,
TestRampUpUsersFromZero::test_ramp_up_users_to_4_workers_with_spawn_rate_of_1, is timing-sensitive and passed on this run; nothing in this diff touches dispatch.uv run ruff check,uv run ruff format --checkanduv run mypy locust/user/users.py locust/contrib/fasthttp.pyare clean.One trade-off worth stating
dict(headers)turns a case-insensitive mapping (sayrequests'CaseInsensitiveDict) into a plain dict. Every header name that these two functions add is looked up under both spellings ("Content-Type" ... or "content-type" ...), so the headers actually sent are unchanged for the usual casing.AI assistance disclosure
This change was produced by an AI coding assistant in a scripted session (locust lane). The RED/GREEN counts, the pytest command lines and the failure names above were copied from the actual local command output rather than estimated, and the diff was reviewed against the reported defect before pushing. No human has reviewed it yet — the maintainer review below is pending.