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:
parts = urlsplit(container_url)
if parts.path == "/":
payload["containerUrl"] = urlunsplit(parts._replace(path=""))
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 URLthat 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
containerUrlashttps://<id>.runs.apify.net, with no trailing slash.Reading the same field through
client.run(...).get().container_urlin v3 giveshttps://<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, andmodel_dump_json().This is a change from v2
v2 returned plain
dicts (ActorClient.call()andRunClient.get()areannotated
dict | None) and did not depend on pydantic at all, so the valuepassed through untouched. Code that upgrades to v3 silently starts seeing a
different string for the same run.
Scope
18
AnyUrl-typed fields acrossapify_client._modelsare affected, includingRun.container_url,Task.standby_url,Dataset.console_url,KeyValueStore.records_public_url,RequestQueue.console_url, andWebhook.request_url.Why this is a problem
I realize
https://example.comandhttps://example.com/are equivalent perRFC 3986 section 6.2.3, so this may look harmless. Two reasons why it still matters:
under v2 and compares it against a freshly fetched one under v3 breaks on
upgrade, with nothing in the changelog to explain it.
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: