{"record":{"id":"15fb1e350e1b4caf","repo":"aio-libs/aiohttp","slug":"base-url-must-have-a-trailing","errorCode":null,"errorMessage":"base_url must have a trailing '/'","messagePattern":"base_url must have a trailing '/'","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"aiohttp/client.py","lineNumber":337,"sourceCode":"        max_line_size: int = 8190,\n        max_field_size: int = 8190,\n        max_headers: int = 128,\n        fallback_charset_resolver: _CharsetResolver = lambda r, b: \"utf-8\",\n        middlewares: Sequence[ClientMiddlewareType] = (),\n        ssl_shutdown_timeout: _SENTINEL | None | float = sentinel,\n    ) -> None:\n        # We initialise _connector to None immediately, as it's referenced in __del__()\n        # and could cause issues if an exception occurs during initialisation.\n        self._connector: BaseConnector | None = None\n        if base_url is None or isinstance(base_url, URL):\n            self._base_url: URL | None = base_url\n            self._base_url_origin = None if base_url is None else base_url.origin()\n        else:\n            self._base_url = URL(base_url)\n            self._base_url_origin = self._base_url.origin()\n            assert self._base_url.absolute, \"Only absolute URLs are supported\"\n        if self._base_url is not None and not self._base_url.path.endswith(\"/\"):\n            raise ValueError(\"base_url must have a trailing '/'\")\n\n        if not isinstance(ssl, SSL_ALLOWED_TYPES):\n            raise TypeError(\n                \"ssl should be SSLContext, Fingerprint, or bool, \"\n                f\"got {ssl!r} instead.\"\n            )\n\n        loop = asyncio.get_running_loop()\n\n        if timeout is sentinel or timeout is None:\n            timeout = ClientTimeout()\n        if not isinstance(timeout, ClientTimeout):\n            raise ValueError(\n                f\"timeout parameter cannot be of {type(timeout)} type, \"\n                \"please use 'timeout=ClientTimeout(...)'\",\n            )\n        self._timeout = timeout\n","sourceCodeStart":319,"sourceCodeEnd":355,"githubUrl":"https://github.com/aio-libs/aiohttp/blob/d041d4d0fd48c3f0832084d33be16cf1c4835f85/aiohttp/client.py#L319-L355","documentation":"Raised by ClientSession.__init__ when base_url's path does not end with '/'. aiohttp resolves relative request URLs against base_url, so a missing trailing slash would silently drop the last path segment (e.g. 'http://x/api' + 'foo' resolves to 'http://x/foo', not 'http://x/api/foo'). The check is intentional to prevent wrong request URLs.","triggerScenarios":"Calling ClientSession(base_url='http://example.com/api') (no trailing '/'). Passing base_url as a string without a trailing slash; the URL is parsed and self._base_url.path.endswith('/') fails.","commonSituations":"Copying an API base URL from a browser bar or docs that omits the trailing slash. Migrating from requests/httpx where trailing slashes are not enforced. Reading base_url from an env var that was trimmed.","solutions":["Add a trailing '/' to base_url: ClientSession(base_url='http://example.com/api/').","Construct with an aiohttp.URL: ClientSession(base_url=URL('http://example.com/api/')).","If the value comes from config, normalize at load time: base_url = cfg.rstrip('/') + '/'."],"exampleFix":"// before\nsession = ClientSession(base_url='http://example.com/api')\n// after\nsession = ClientSession(base_url='http://example.com/api/')","handlingStrategy":"validation","validationCode":"from aiohttp import URL\n\ndef normalize_base_url(raw: str) -> URL:\n    u = URL(raw)\n    if not u.path.endswith('/'):\n        u = u.with_path(u.path + '/')\n    return u\n\n# at session creation\nClientSession(base_url=normalize_base_url(cfg['base_url']))","typeGuard":"from aiohttp import URL\n\ndef has_trailing_slash(u: URL) -> bool:\n    return u.path.endswith('/')","tryCatchPattern":"try:\n    session = ClientSession(base_url=raw)\nexcept ValueError as e:\n    if 'trailing' in str(e):\n        raw = raw.rstrip('/') + '/'\n        session = ClientSession(base_url=raw)\n    else:\n        raise","preventionTips":["Centralize base_url construction in one helper that always appends '/'.","Read base_url from config typed as a yarl.URL, not a raw string.","Add a unit test asserting base_url.path.endswith('/')."],"tags":["client-session","configuration","url","validation"],"backgroundTag":null,"analyzedSha":"d041d4d0fd48c3f0832084d33be16cf1c4835f85","analyzedAt":"2026-08-11T20:44:15.550Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}