aio-libs/aiohttp · error · ValueError

base_url must have a trailing '/'

Error message

base_url must have a trailing '/'

What it means

Raised in ClientSession.__init__ (client.py:337) as a ValueError when a base_url is supplied whose path does not end with '/'. aiohttp concatenates base_url with per-request relative URLs and relies on RFC 3986 path resolution, which only behaves predictably when the base path ends in '/'. This is a fail-fast configuration check at construction time.

Source

Thrown at aiohttp/client.py:337

        max_line_size: int = 8190,
        max_field_size: int = 8190,
        max_headers: int = 128,
        fallback_charset_resolver: _CharsetResolver = lambda r, b: "utf-8",
        middlewares: Sequence[ClientMiddlewareType] = (),
        ssl_shutdown_timeout: _SENTINEL | None | float = sentinel,
    ) -> None:
        # We initialise _connector to None immediately, as it's referenced in __del__()
        # and could cause issues if an exception occurs during initialisation.
        self._connector: BaseConnector | None = None
        if base_url is None or isinstance(base_url, URL):
            self._base_url: URL | None = base_url
            self._base_url_origin = None if base_url is None else base_url.origin()
        else:
            self._base_url = URL(base_url)
            self._base_url_origin = self._base_url.origin()
            assert self._base_url.absolute, "Only absolute URLs are supported"
        if self._base_url is not None and not self._base_url.path.endswith("/"):
            raise ValueError("base_url must have a trailing '/'")

        if not isinstance(ssl, SSL_ALLOWED_TYPES):
            raise TypeError(
                "ssl should be SSLContext, Fingerprint, or bool, "
                f"got {ssl!r} instead."
            )

        loop = asyncio.get_running_loop()

        if timeout is sentinel or timeout is None:
            timeout = ClientTimeout()
        if not isinstance(timeout, ClientTimeout):
            raise ValueError(
                f"timeout parameter cannot be of {type(timeout)} type, "
                "please use 'timeout=ClientTimeout(...)'",
            )
        self._timeout = timeout

View on GitHub (pinned to c0ef574e29)

Solutions

  1. Add a trailing '/' to base_url: 'https://api.example.com/v1/'.
  2. Normalize dynamically-built base URLs with yarl: URL(s).with_path(s.path.rstrip('/') + '/').
  3. If you need the origin only, pass base_url without a path or leave it None.

Example fix

# before
session = aiohttp.ClientSession(base_url='https://api.example.com/v1')

# after
session = aiohttp.ClientSession(base_url='https://api.example.com/v1/')
Defensive patterns

Strategy: validation

Validate before calling

from yarl import URL
base = URL('https://api.example.com/v1')
if not base.path.endswith('/'):
    base = base.with_path(base.path + '/')
session = aiohttp.ClientSession(base_url=base)

Type guard

def valid_base_url(u: str) -> bool:
    return u.endswith('/') or URL(u).path.endswith('/')

Try / catch

try:
    session = aiohttp.ClientSession(base_url=base)
except ValueError:
    base = base.rstrip('/') + '/'
    session = aiohttp.ClientSession(base_url=base)

Prevention

When it happens

Trigger: ClientSession(base_url='https://api.example.com/v1') (no trailing slash) — or any base_url string/URL whose .path does not terminate with '/'.

Common situations: Copying an API base URL straight from a provider's docs (which usually omit the trailing slash), or constructing base_url dynamically without normalizing.

Related errors


AI-assisted analysis of aio-libs/aiohttp@c0ef574e29 (2026-08-04). Data as JSON: /data/errors/15fb1e350e1b4caf.json. Report an issue: GitHub.