Skip to content

Commit

Permalink
Raise TimeoutError on request timeout, instead of RequestError
Browse files Browse the repository at this point in the history
  • Loading branch information
dvolodin7 committed Mar 22, 2024
1 parent 231d15d commit a0d4447
Show file tree
Hide file tree
Showing 6 changed files with 112 additions and 4 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ To see unreleased changes, please see the [CHANGELOG on the main branch guide](h

### Fixed

* Raise `TimeoutError` on request timeout, instead of `RequestError`.
* Exception names shown without _Py_ prefix.

### Changed
Expand Down
7 changes: 6 additions & 1 deletion src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
// ------------------------------------------------------------------------
use pyo3::{
create_exception,
exceptions::{PyConnectionError, PyException, PyValueError},
exceptions::{PyConnectionError, PyException, PyTimeoutError, PyValueError},
PyErr,
};

Expand All @@ -18,6 +18,7 @@ pub enum GufoHttpError {
Redirect,
Connect(String),
ValueError(String),
Timeout,
}

create_exception!(
Expand All @@ -38,6 +39,7 @@ impl From<GufoHttpError> for PyErr {
GufoHttpError::Redirect => RedirectError::new_err("redirects limit exceeded"),
GufoHttpError::Connect(x) => PyConnectionError::new_err(x),
GufoHttpError::ValueError(x) => PyValueError::new_err(x),
GufoHttpError::Timeout => PyTimeoutError::new_err("timed out"),
}
}
}
Expand All @@ -47,6 +49,9 @@ impl From<reqwest::Error> for GufoHttpError {
if value.is_connect() {
return GufoHttpError::Connect(value.to_string());
}
if value.is_timeout() {
return GufoHttpError::Timeout;
}
if value.is_redirect() {
return GufoHttpError::Redirect;
}
Expand Down
10 changes: 10 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@

from .util import (
HTTPD_ADDRESS,
HTTPD_BLACKHOLE_PORT,
HTTPD_HOST,
HTTPD_PATH,
HTTPD_PORT,
HTTPD_TLS_PORT,
BlackholeHttpd,
)


Expand Down Expand Up @@ -49,3 +51,11 @@ def httpd_tls() -> Iterator[Httpd]:
mode=HttpdMode.HTTPS,
) as httpd:
yield httpd


@pytest.fixture(scope="session")
def httpd_blackhole() -> Iterator[BlackholeHttpd]:
logger = logging.getLogger("gufo.http.httpd")
logger.setLevel(logging.DEBUG)
with BlackholeHttpd(port=HTTPD_BLACKHOLE_PORT) as httpd:
yield httpd
11 changes: 10 additions & 1 deletion tests/test_async_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
from gufo.http.async_client import HttpClient
from gufo.http.httpd import Httpd

from .util import UNROUTABLE_PROXY, UNROUTABLE_URL, with_env
from .util import UNROUTABLE_PROXY, UNROUTABLE_URL, BlackholeHttpd, with_env


def test_get(httpd: Httpd) -> None:
Expand Down Expand Up @@ -456,6 +456,15 @@ async def inner() -> None:
asyncio.run(inner())


def test_request_timeout(httpd_blackhole: BlackholeHttpd) -> None:
async def inner() -> None:
async with HttpClient(timeout=1.0) as client:
with pytest.raises(TimeoutError):
await client.get(f"{httpd_blackhole.prefix}/")

asyncio.run(inner())


def test_default_user_agent(httpd: Httpd) -> None:
async def inner() -> None:
async with HttpClient() as client:
Expand Down
7 changes: 6 additions & 1 deletion tests/test_sync_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
from gufo.http.httpd import Httpd
from gufo.http.sync_client import HttpClient

from .util import UNROUTABLE_PROXY, UNROUTABLE_URL, with_env
from .util import UNROUTABLE_PROXY, UNROUTABLE_URL, BlackholeHttpd, with_env


def test_get(httpd: Httpd) -> None:
Expand Down Expand Up @@ -357,6 +357,11 @@ def test_connect_timeout(httpd: Httpd) -> None:
client.get(UNROUTABLE_URL)


def test_request_timeout(httpd_blackhole: BlackholeHttpd) -> None:
with HttpClient(timeout=1.0) as client, pytest.raises(TimeoutError):
client.get(f"{httpd_blackhole.prefix}/")


def test_default_user_agent(httpd: Httpd) -> None:
with HttpClient() as client:
resp = client.get(f"{httpd.prefix}/ua/default")
Expand Down
80 changes: 79 additions & 1 deletion tests/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,25 @@
import contextlib
import os
import random
from typing import Dict
import select
import socket
from logging import getLogger
from threading import Thread
from types import TracebackType
from typing import Dict, Optional, Type

HTTPD_PATH = "/usr/sbin/nginx"
HTTPD_HOST = "local.gufolabs.com"
HTTPD_ADDRESS = "127.0.0.1"
HTTPD_PORT = random.randint(52000, 53999)
HTTPD_TLS_PORT = random.randint(52000, 53999)
HTTPD_BLACKHOLE_PORT = random.randint(52000, 53999)
UNROUTABLE_URL = "http://192.0.2.1/"
UNROUTABLE_PROXY = "http://192.0.2.1:3128/"
TEXT_PLAIN = "text/plain"

logger = getLogger("gufo.httpd.httpd")


@contextlib.contextmanager
def with_env(env: Dict[str, str]) -> None:
Expand All @@ -42,3 +50,73 @@ def with_env(env: Dict[str, str]) -> None:
del os.environ[k]
elif v is not None:
os.environ[k] = v


class BlackholeHttpd(object):
"""Blackhole server to test request timeouts."""

def __init__(
self: "BlackholeHttpd",
address: str = "127.0.0.1",
port: int = 10000,
host: str = "local.gufolabs.com",
) -> None:
self._address = address
self._port = port
self._host = host
self.prefix = f"http://{self._host}:{self._port}"
self._thread: Optional[Thread] = None
self._to_shutdown = False

def __enter__(self: "BlackholeHttpd") -> "BlackholeHttpd":
"""Context manager entry."""
self.start()
return self

def __exit__(
self: "BlackholeHttpd",
exc_type: Optional[Type[BaseException]],
exc_val: Optional[BaseException],
exc_tb: Optional[TracebackType],
) -> None:
"""Context manager exit."""
self.stop()

def start(self: "BlackholeHttpd") -> None:
"""Start server."""
self._thread = Thread(name=f"blackhole-{self._port}", target=self._run)
self._thread.daemon = True
self._thread.start()

def stop(self: "BlackholeHttpd") -> None:
"""Stop server."""
self._to_shutdown = True
if self._thread:
self._thread.join(3.0)
self._thread = None

def _run(self: "BlackholeHttpd") -> None:
"""Server implementation."""
listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
readers = [listener]
listener.bind((self._address, self._port))
listener.listen(5)
logger.info("Listeninng %s:%s", self._address, self._port)
while not self._to_shutdown:
readers, _, _ = select.select(readers, [], [], 1.0)
for sock in readers:
if sock == listener:
# New connection
new_client, remote_addr = listener.accept()
logger.info("Connect from %s", remote_addr)
readers.append(new_client)
else:
# Incoming data
data = sock.recv(1024)
if data:
logger.info("Received: %s", data)
else:
# Connection closed
logger.info("Connnnection closed")
sock.close()
readers.remove(sock)

0 comments on commit a0d4447

Please sign in to comment.