undefined behaviour when used in a parallel environment
moon-bits opened this issue · 8 comments
Hi @lundberg,
First of all, thank you very much for your effort! I really enjoy working with respx
!
We are currently using respx
to mock the behaviour of a real system in our staging system.
Yet, we experience some undefined behaviour in our application and question whether respx
is intended to be used within a concurrent/parallel environment. The only hint I could see was the following pull request, but there is no test case for the parallel usage, as far as I can see.
The problem is that module A
is being mocked via a respx
context manager, which works as intended. But module B
should query the server directly (i.e. must not being mocked). Yet, when the application gets hit with many requests, the httpx.Client
requests within module B
start to fail as they receive empty response bodys as it seems that respx
did not revert the patch.
We already investigated whether the server within module B
is returning malformed responses, but the logs via Wireshark state something else.
Unfortunately, I have no clue how the patching actually works behind the scenes. But since we make use of respx
within a context manager, my understanding was that the patching is also being done locally.
So our questions are:
- Is
respx
thread-safe or allowed to be used in a parallel environment? - If so, what would be the correct usage?
Thanks in advance!
The stacktrace of the failing requests within module B
also shows that respx
was trying to mock a request that was not intended to be mocked:
File "httpx/_client.py", line 1025, in get
return self.request(
File "httpx/_client.py", line 802, in request
return self.send(request, auth=auth, follow_redirects=follow_redirects)
File "httpx/_client.py", line 889, in send
response = self._send_handling_auth(
File "httpx/_client.py", line 917, in _send_handling_auth
response = self._send_handling_redirects(
File "httpx/_client.py", line 954, in _send_handling_redirects
response = self._send_single_request(request)
File "httpx/_client.py", line 990, in _send_single_request
response = transport.handle_request(request)
File "httpx/_transports/default.py", line 187, in handle_request
resp = self._pool.handle_request(req)
File "respx/mocks.py", line 179, in mock
response = cls._send_sync_request(
File "respx/mocks.py", line 214, in _send_sync_request
response = cls.from_sync_httpx_response(httpx_response, instance, **kwargs)
File "respx/mocks.py", line 318, in from_sync_httpx_response
status=httpx_response.status_code,
Hi, I don't really get the full picture, but think I understand the question anyways 😉 .
Since RESPX patches and mocks the actual HTTPX inners, it's patched "globally" once RESPX is started, i.e. entering the respx.mock
context manager or decorated function.
This means that if RESPX in module A
starts patching HTTPX, it probably will be patched in module B
at the same time.
Questions:
- Does
module A
andmodule B
request different APIs, i.e. different remote endpoints? - Do you need both
module A
andmodule B
to be mocked, or is it only one of them and the other always hit real remote?
About #39, it's to prevent multiple respx.mock
contexts to not patch HTTPX multiple times, i.e. not patch the patch 😉 .
Hi @lundberg,
Thanks for your quick response!
OK, now the errors make sense. So it's a global patch, which creates the race-condition. Regarding your questions:
Does module A and module B request different APIs, i.e. different remote endpoints?
Yes, module A
sends request to example.org
and module B
sends requests to github.com
Do you need both module A and module B to be mocked, or is it only one of them and the other always hit real remote?
Exactly, only module A
would need to be mocked and the other module B
must always hit the real remote endpoint (github.com
).
Do you see a solution for my use case?
Do you see a solution for my use case?
Yes, you can use pass through for this.
Create a single route for github.com
and mark it for pass through....
respx.route(host="github.com").pass_through()
I see, why is it problematic if I define a Router
like that:
router = respx.mock(base_url="http://example.org/api")
Because that's how we specify the mocking for example.org
. From our understanding it should only be valid for example.org/api/*
requests.
Or is there a difference between your provided solution and ours?
I see, why is it problematic if I define a Router like that
It's not a problem, you need both...
router = respx.mock(base_url="http://example.org/api")
router.post("/some_endpoint").mock(...) # mocks posts to http://example.org/api/some_endpoint
# This route will catch all github requests and pass them through
router.route(host="github.com").pass_through()
@router
def some_test():
...
Closing this, please re-open if you still have any problems @moon-bits.