lundberg/respx

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 and module B request different APIs, i.e. different remote endpoints?
  • 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?

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.