Do not modify the headers dict the caller passed in by Lesereingrape · Pull Request #3530 · locustio/locust · GitHub
Skip to content

Do not modify the headers dict the caller passed in - #3530

Merged
cyberw merged 1 commit into
locustio:masterfrom
Lesereingrape:fix/rest-headers-copy
Oct 4, 2026
Merged

cyberw merged 1 commit into
locustio:masterfrom
Lesereingrape:fix/rest-headers-copy

Conversation

@Lesereingrape

Copy link
Copy Markdown
Contributor

What

RestMixin.rest() (locust/user/users.py) and FastHttpSession.request() (locust/contrib/fasthttp.py) both open with headers = 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 level HEADERS shared 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 claims Content-Type: application/json, so the form body of the next request is no longer parsed by the server (form round trip: '' instead of 'hello'). With FastHttpUser both sites are on the path at once, so the caller's dict also grows Accept-Encoding, and Authorization whenever auth/auth_header is 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 555ad9e5 and GREEN with this change:

  • locust/test/test_http.py::TestHttpUserRest::test_rest_does_not_modify_the_given_headers
  • locust/test/test_fasthttp.py::TestFastHttpUserClass::test_client_request_does_not_modify_the_given_headers
  • locust/test/test_fasthttp.py::TestFastHttpCatchResponse::test_rest_does_not_modify_the_given_headers

Each one asserts both halves of the contract: the request still carries the defaults (Content-Type, and the caller's own X-Header-Test), and the caller's dict still carries only what the caller put in it.

uv run pytest locust/test/test_http.py locust/test/test_fasthttp.py -k not_modify
before: 3 failed, 106 deselected
after : 3 passed, 106 deselected

The two whole files, plus test_dispatch.py:

uv run pytest locust/test/test_http.py locust/test/test_fasthttp.py locust/test/test_dispatch.py
base  (555ad9e5): 4 failed, 160 passed, 1 skipped
this branch     : 3 failed, 164 passed, 1 skipped

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_measure asserts 1 <= response_time <= 1.5 and measured 14.7 s on this machine;
  • TestHttpSession::test_verify_true_fails_with_bad_cert gets SSLCertVerificationError: certificate has expired from the locally generated test certificate, which this urllib3 version does not wrap into urllib3.exceptions.SSLError;
  • TestFastHttpSsl::test_ssl_request_insecure fails already in setUp, with PermissionError: [Errno 13] Permission denied while 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 --check and uv run mypy locust/user/users.py locust/contrib/fasthttp.py are clean.

One trade-off worth stating

dict(headers) turns a case-insensitive mapping (say requests' 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.

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.
@cyberw
cyberw merged commit 1fdcfd1 into locustio:master Oct 4, 2026
21 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

rest() and FastHttpSession.request() modify the headers dict passed by the caller

2 participants