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
- Add a trailing '/' to base_url: 'https://api.example.com/v1/'.
- Normalize dynamically-built base URLs with yarl: URL(s).with_path(s.path.rstrip('/') + '/').
- 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
- Always end base_url with '/'
- Normalize dynamically-built base URLs before passing them
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
- ssl should be SSLContext, Fingerprint, or bool, got {ssl!r}
- {url}
- Connection timeout to host {url}
- timeout parameter cannot be of {type} type, please use 'time
- Session and connector have to use same event loop
AI-assisted analysis of aio-libs/aiohttp@c0ef574e29 (2026-08-04).
Data as JSON: /data/errors/15fb1e350e1b4caf.json.
Report an issue: GitHub.