respx icon indicating copy to clipboard operation
respx copied to clipboard

`respx_mock` doesn't handle `//` in the path-section of URLs.

Open Skeen opened this issue 1 year ago • 3 comments
trafficstars

Hi,

I just stumbled upon the issue described in the title, which can be reproduced with the following pytest file.

import httpx
import pytest
from pydantic import AnyHttpUrl
from respx import MockRouter

@pytest.mark.parametrize(
    "url",
    [
        "http://localhost",  # OK
        "http://localhost/",  # OK
        "http://localhost//",  # Fails
        "http://localhost///",  # Fails
        "http://localhost/%2F",  # Fails
        "http://localhost/%2F/",  # Fails
        "http://localhost/%2F%2F",  # Fails
    ],
)
async def test_respx_targeting(respx_mock: MockRouter, url: AnyHttpUrl) -> None:
    route = respx_mock.get(url=url).respond(status_code=200)
    result = httpx.get(url)
    assert result.status_code == 200
    assert route.called

The fails all take the form of:

respx.models.AllMockedAssertionError: RESPX: <Request('GET', 'http://localhost//')> not mocked!

Having // in the path sections of URLs is valid according to RFC 3986 (see section '3: Syntax Components' and section '3.3. Path'), Stack Overflow seems to concur.

Skeen avatar Aug 02 '24 19:08 Skeen

I noticed the issue as I am using hypothesis to run property-based tests on my code, in particular using their provisional.urls strategy.

The above parametrized tests can instead be run under hypothesis using the following code:

import httpx
from hypothesis import HealthCheck
from hypothesis import given
from hypothesis import settings
from hypothesis.provisional import urls
from hypothesis.strategies import register_type_strategy
from pydantic import AnyHttpUrl
from respx import MockRouter

register_type_strategy(AnyHttpUrl, urls())


@given(...)
@settings(suppress_health_check=[HealthCheck.function_scoped_fixture])
async def test_respx_targeting(respx_mock: MockRouter, url: AnyHttpUrl) -> None:
    # Reset the function scoped fixture as it is used with hypothesis
    respx_mock.reset()

    route = respx_mock.get(url=url).respond(status_code=200)
    result = httpx.get(url)
    assert result.status_code == 200
    assert route.called

To ensure that the property holds for "all" urls.

Skeen avatar Aug 02 '24 20:08 Skeen

Thanks for reporting @Skeen. I've narrowed down the problem to the Path pattern here.

Both urljoin("/", "///") and httpx.URL("///") turns e.g. /// into a single / path 🤔

lundberg avatar Sep 02 '24 09:09 lundberg

Looked a bit more, and regarding urljoin, we'll need to change to a simple conditional slash prepend.

Regarding httpx.URL("///"), this unfortunately is treated as first two first // as scheme/host delimiter, and a single / path 😅 . Could be fixed by simply adding that, e.g. httpx.URL("scheme://host" + path).path

lundberg avatar Sep 02 '24 09:09 lundberg