Name and Version
aidial-client-python 0.7.1
What steps will reproduce the bug?
- In a project that depends on
aidial-client and a DIAL Core instance with at least one file uploaded under an appdata folder (so the folder physically exists).
- Configure an
AsyncDial client against that core, e.g. dial = AsyncDial(base_url="http://localhost:8090", api_key="...").
- Call the folder metadata endpoint with a trailing slash, as required by DIAL Core to distinguish a folder listing from a file lookup:
await dial.metadata.get("files", "files/<bucket>/appdata/<app>/")
- Observe the resulting outbound HTTP request and the raised exception.
The same effect occurs for any relative URL passed to the SDK whose trailing / is semantically meaningful — folder listings under /v1/metadata/files/... are the most visible case. The bug affects both Dial and AsyncDial since it lives in the shared BaseHTTPClient.
What is the expected behavior?
The trailing slash supplied by the caller should be preserved on the wire. The above call should reach DIAL Core as:
GET /v1/metadata/files/<bucket>/appdata/<app>/ -> 200 OK
and return the folder's FileMetadata (the same response that a plain curl against the URL with trailing slash returns).
What do you see instead?
BaseHTTPClient._prepare_url in aidial_client/_http_client/_base.py strips the trailing / from the merged path:
def _prepare_url(self, url: str) -> httpx.URL:
parsed_url = httpx.URL(url)
if parsed_url.is_relative_url:
merge_raw_path = self.base_url.raw_path + parsed_url.raw_path.lstrip(b"/")
return self.base_url.copy_with(raw_path=merge_raw_path.rstrip(b"/")) # ← drops the trailing slash
return parsed_url
so the outgoing request becomes:
GET /v1/metadata/files/<bucket>/appdata/<app> -> 404 Not Found
and the SDK raises:
aidial_client._exception.DialException(message='', status_code=404, type='runtime_error', ...)
Verified independently with curl against the same DIAL Core:
curl …/v1/metadata/files/<bucket>/appdata/<app> -> 404
curl …/v1/metadata/files/<bucket>/appdata/<app>/ -> 200
Absolute URLs are unaffected because they bypass the is_relative_url branch — the bug only hits relative URLs joined against base_url.
Additional information
The rstrip(b"/") looks like a guard against double slashes at the join boundary, but it also removes the significant trailing slash on the user-supplied relative path. Because base_url is already normalized via enforce_trailing_slash and the relative part is lstrip-ed, no rstrip is needed:
def _prepare_url(self, url: str) -> httpx.URL:
parsed_url = httpx.URL(url)
if parsed_url.is_relative_url:
base = self.base_url.raw_path # already ends with b"/"
tail = parsed_url.raw_path.lstrip(b"/")
return self.base_url.copy_with(raw_path=base + tail)
return parsed_url
Additional edge cases the proposed fix also addresses:
- Empty relative URL
"" — current code strips the base's own trailing / (e.g. /v1/ → /v1); proposed fix preserves it.
- Relative URL equal to
"/" — same as above.
- Multiple meaningful trailing slashes (
files/x//) — current rstrip(b"/") collapses all of them; proposed fix preserves caller intent.
- Relative URL containing a query string (
files/x/?foo=1) — not broken today, since httpx.URL.raw_path includes the query string so / is no longer the last byte; the fix doesn't regress it.
Environment: aidial-client-python 0.7.1, Python 3.13, httpx (transitive).
Name and Version
aidial-client-python 0.7.1
What steps will reproduce the bug?
aidial-clientand a DIAL Core instance with at least one file uploaded under anappdatafolder (so the folder physically exists).AsyncDialclient against that core, e.g.dial = AsyncDial(base_url="http://localhost:8090", api_key="...").The same effect occurs for any relative URL passed to the SDK whose trailing
/is semantically meaningful — folder listings under/v1/metadata/files/...are the most visible case. The bug affects bothDialandAsyncDialsince it lives in the sharedBaseHTTPClient.What is the expected behavior?
The trailing slash supplied by the caller should be preserved on the wire. The above call should reach DIAL Core as:
and return the folder's
FileMetadata(the same response that a plaincurlagainst the URL with trailing slash returns).What do you see instead?
BaseHTTPClient._prepare_urlinaidial_client/_http_client/_base.pystrips the trailing/from the merged path:so the outgoing request becomes:
and the SDK raises:
Verified independently with
curlagainst the same DIAL Core:Absolute URLs are unaffected because they bypass the
is_relative_urlbranch — the bug only hits relative URLs joined againstbase_url.Additional information
The
rstrip(b"/")looks like a guard against double slashes at the join boundary, but it also removes the significant trailing slash on the user-supplied relative path. Becausebase_urlis already normalized viaenforce_trailing_slashand the relative part islstrip-ed, norstripis needed:Additional edge cases the proposed fix also addresses:
""— current code strips the base's own trailing/(e.g./v1/→/v1); proposed fix preserves it."/"— same as above.files/x//) — currentrstrip(b"/")collapses all of them; proposed fix preserves caller intent.files/x/?foo=1) — not broken today, sincehttpx.URL.raw_pathincludes the query string so/is no longer the last byte; the fix doesn't regress it.Environment: aidial-client-python 0.7.1, Python 3.13, httpx (transitive).