You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
URL fields get an extra trailing slash in v3, changing the value the API sent #999
Title: URL fields get an extra trailing slash in v3, changing the value the API sent
Issue description
In v3, URL fields are typed as pydantic.AnyUrl. Pydantic appends / to a URL
that has no path, so the string the client gives back is not the string the API
sent.
Fetching the same actor run two ways shows the difference. The raw API response
returns containerUrl as https://<id>.runs.apify.net, with no trailing slash.
Reading the same field through client.run(...).get().container_url in v3 gives https://<id>.runs.apify.net/, with one added.
The change happens during validation, not serialization, so every access path
carries it - attribute access, model_dump() in either mode, and model_dump_json().
This is a change from v2
v2 returned plain dicts (ActorClient.call() and RunClient.get() are
annotated dict | None) and did not depend on pydantic at all, so the value
passed through untouched. Code that upgrades to v3 silently starts seeing a
different string for the same run.
Scope
18 AnyUrl-typed fields across apify_client._models are affected, including Run.container_url, Task.standby_url, Dataset.console_url, KeyValueStore.records_public_url, RequestQueue.console_url, and Webhook.request_url.
Why this is a problem
I realize https://example.com and https://example.com/ are equivalent per
RFC 3986 section 6.2.3, so this may look harmless. Two reasons why it still matters:
It changes silently across a major version. Any code that stored a URL
under v2 and compares it against a freshly fetched one under v3 breaks on
upgrade, with nothing in the changelog to explain it.
A client shouldn't alter values the server sent. Even where the two forms
are equivalent as URLs, they are not equal as strings, and callers reasonably
expect the field to match the API response.
Anywhere the value is used as a base to build longer URLs, the added slash also
produces a doubled separator (...net//path), which is not equivalent.
We hit this in the Apify Dify plugin and work around it like so:
Update the change log and explicitly mention this in the upgrade guide:
You should not compare the string saved URL with the string representation of AnyUrl. The correct approach is to convert any string-based URL to AnyUrl and only then compare it.
So, for example, if you already have a string-based URL:
url_as_string = "https://www.žluty.cz"
url_normalized = AnyUrl("https://www.žluty.cz")
# DO NOT COMPARE LIKE THIS:
url_as_string == str(url_normalized)
# COMPARE LIKE THIS:
AnyUrl(url_as_string) == url_normalized
But to be honest, I am not sure if we consistently stick to this advice in the SDK or if we do this somewhere by mistake as well...
After the internal discussion, we consider this behavior correct and will document it rather than change it. The best practise should be what @Pijukatel suggested - comparing URLs in their normalized form.
Activity
vdusek commentedon Aug 10, 2026
Hi, thanks for the report.
The
datamodel-codegengenerates theAnyUrlfield type from theformat: urispecification. That's where it comes from.An extra trailing slash is not the only thing; see:
This is unfortunate; avoiding normalization of URL fields would require changing their type from
AnyUrltostr, which would be a breaking change.cc @Pijukatel @janbuchar, I'd like to hear your thoughts
Pijukatel commentedon Aug 10, 2026
Not sure, but I would probably lean towards this:
Update the change log and explicitly mention this in the upgrade guide:
You should not compare the string saved URL with the string representation of AnyUrl. The correct approach is to convert any string-based URL to AnyUrl and only then compare it.
So, for example, if you already have a string-based URL:
But to be honest, I am not sure if we consistently stick to this advice in the SDK or if we do this somewhere by mistake as well...
vdusek commentedon Aug 12, 2026
After the internal discussion, we consider this behavior correct and will document it rather than change it. The best practise should be what @Pijukatel suggested - comparing URLs in their normalized form.