aiohttp icon indicating copy to clipboard operation
aiohttp copied to clipboard

`BaseTestServer.start_server()` changes `host` value to an IP address

Open ppena-LiveData opened this issue 3 years ago • 2 comments

Describe the bug

For some reason, BaseTestServer.start_server() changes self.host to an IP address, which is a problem if someone is expecting a host name, e.g. localhost. I am using aioresponses in addition to pytest-aiohttp, the latter of which creates a aiohttp.test_utils.TestServer. The problem is that if I pass http://localhost:{port} to aioresponses(passthrough=[...]) and then try to do a relative client.get('/'), it uses 127.0.0.1 instead of localhost, so aioresponses does not try to pass that through to aiohttp to handle it.

I see that @webknjaz made the commit that added code to start_server() to change self.host to an IP address, but the commit message doesn't say why that is being done.

To Reproduce

This pytest reproduction of the problem is just a slight variation of the example code from https://pypi.org/project/pytest-aiohttp/:

from aiohttp import web
from aioresponses import aioresponses


async def hello(request):
    return web.Response(body=b'Hello, world')


def create_app():
    app = web.Application()
    app.router.add_route('GET', '/', hello)
    return app


async def test_hello(aiohttp_client):
    host, port = 'localhost', 54321
    with aioresponses(passthrough=[f'http://{host}:{port}']):
        opts = {'host': host, 'port': port}
        client = await aiohttp_client(create_app(), server_kwargs=opts)
        resp = await client.get('/')
        assert resp.status == 200
        text = await resp.text()
        assert 'Hello, world' in text

Expected behavior

Relative paths should work when using an aiohttp_client with a host value of localhost, even when using aioresponses.

Logs/tracebacks

The pytest reproduction above causes `aioresponses/core.py` to throw this exception:

aiohttp.client_exceptions.ClientConnectionError: Connection refused: GET http://127.0.0.1:54321/

Python Version

$ python --version
Python 3.9.13

aiohttp Version

$ python -m pip show aiohttp
Name: aiohttp
Version: 3.8.3
Summary: Async http client/server framework (asyncio)
Home-page: https://github.com/aio-libs/aiohttp
...

multidict Version

$ python -m pip show multidict
Name: multidict
Version: 6.0.4
Summary: multidict implementation
Home-page: https://github.com/aio-libs/multidict
...

yarl Version

$ python -m pip show yarl
Name: yarl
Version: 1.8.2
Summary: Yet another URL library
Home-page: https://github.com/aio-libs/yarl/

OS

Windows 11 but also in Linux:

$ uname -a
Linux hippy 5.15.79.1-microsoft-standard-WSL2 #1 SMP Wed Nov 23 01:01:46 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux

Related component

Server, Client

Additional context

No response

Code of Conduct

  • [X] I agree to follow the aio-libs Code of Conduct

ppena-LiveData avatar Jan 12 '23 20:01 ppena-LiveData

NOTE: to workaround the issue, you can replace the _root URL of the TestServer, like this:

client._server._root = URL(f'http://{host}:{port}')

ppena-LiveData avatar Jan 12 '23 22:01 ppena-LiveData

` class BaseTestServer(ABC): __test__ = False

def __init__(
    self,
    *,
    scheme: Union[str, object] = sentinel,
    loop: Optional[asyncio.AbstractEventLoop] = None,
    host: str = "127.0.0.1",
    port: Optional[int] = None,
    skip_url_asserts: bool = False,
    socket_factory: Callable[
        [str, int, socket.AddressFamily], socket.socket
    ] = get_port_socket,
    **kwargs: Any,
) -> None:
    self._loop = loop
    self.runner: Optional[BaseRunner] = None
    self._root: Optional[URL] = None
    self.host = host
    self.port = port
    self._closed = False
    self.scheme = scheme
    self.skip_url_asserts = skip_url_asserts
    self.socket_factory = socket_factory

async def start_server(
    self, loop: Optional[asyncio.AbstractEventLoop] = None, **kwargs: Any
) -> None:
    if self.runner:
        return
    self._loop = loop
    self._ssl = kwargs.pop("ssl", None)
    self.runner = await self._make_runner(handler_cancellation=True, **kwargs)
    await self.runner.setup()
    if not self.port:
        self.port = 0
    try:
        version = ipaddress.ip_address(self.host).version
    except ValueError:
        version = 4
    family = socket.AF_INET6 if version == 6 else socket.AF_INET
    _sock = self.socket_factory(self.host, self.port, family)
    self.host, self.port = _sock.getsockname()[:2]
    site = SockSite(self.runner, sock=_sock, ssl_context=self._ssl)
    await site.start()
    server = site._server
    assert server is not None
    sockets = server.sockets  # type: ignore[attr-defined]
    assert sockets is not None
    self.port = sockets[0].getsockname()[1]
    if self.scheme is sentinel:
        if self._ssl:
            scheme = "https"
        else:
            scheme = "http"
        self.scheme = scheme
    self._root = URL(f"{self.scheme}://{self.host}:{self.port}")`
  

This happened because kwargs into BaseTestServer just remain forgotten into __init__ method and not transmitted into start_server when aiohttp_client making TestServer instance

a-shahov avatar Jan 23 '24 13:01 a-shahov