Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,49 @@ All notable changes to the Rust Apify API client are documented here. The format
based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project adheres
to [Semantic Versioning](https://semver.org/).

## [0.16.0] - 2026-11-06

Ports the `apify-client-js` Server Actor support and the `count` of dataset item pages.

### Added
- `ActorClient::server`, which returns an `ActorServerClient` for a Server Actor (formerly Actor
Standby), and its deprecated alias `ActorClient::standby`.
- `ActorServerClient::request` and `request_stream`, which send an HTTP request to the Actor's
server with the token in `X-Apify-Authorization`, without following redirects, retrying only `429`
and `503`, and returning error statuses as responses. Their options are
`ActorServerRequestOptions`, `ActorServerBody` and `ActorServerCompression`.
- `ActorServerClient::get_url` and `get_open_api_spec`.
- `Actor::standby_url`, `actor_standby` (`ActorStandby`, with the `tenancy` values `SINGLE_TENANT`
and `MULTI_TENANT`) and `tagged_builds` (`TaggedBuildInfo`).
- `ActorVersion::actor_path`, the Actor's directory inside an uploaded source.
- `common::MAX_EXPIRES_IN_SECS`, the longest `expires_in_secs` of a signed public URL.
- `HttpMethod::Patch`, `Options` and `Trace`, `HttpRequest::follow_redirects`,
`ReqwestBackend::with_no_redirect_client`, and `HttpResponse::is_success`, `text` and `json`.

### Changed
- Synced with Apify OpenAPI spec `v2-2026-11-04T151257Z` (`API_SPEC_VERSION`).
- **Breaking:** `PaginationList::count` of `DatasetClient::list_items` is the number of rows the API
scanned, or the number of items returned when that is larger, so filters can leave it above
`items.len()`. The iterators step by it, capped at the page size.
- **Breaking:** `HttpRequest` has the new field `follow_redirects`, which a custom `HttpBackend`
must honor, and `HttpMethod` has new variants.
- **Breaking:** `Actor` and `ActorVersion` have new fields, and `standbyUrl`, `actorStandby` and
`taggedBuilds` are no longer in `Actor::extra`.
- Documented that iterating `iterate_items` without a page size loads the whole dataset in one
response.

### Fixed
- The streaming calls (`LogClient::stream`, `DatasetClient::download_items_stream` and
`ActorServerClient::request_stream`) retry an error status or a transport error until the
response starts, like the other calls, instead of failing on the first one.
- `create_items_public_url`, `create_items_public_url_with_format` and `create_keys_public_url` fail
with `InvalidArgument` for an `expires_in_secs` that is `0` or negative, before any request is
sent. Such a value used to sign a URL that had already expired. An `expires_in_secs` above the new
`common::MAX_EXPIRES_IN_SECS` fails with `InvalidArgument` too, instead of panicking or wrapping
around.
- A `ListIterator` no longer ends in front of rows that match when a filter leaves a page with no
items.

## [0.15.0] - 2026-10-29

Ports the `apify-client-js` v3 changes.
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "apify-client"
version = "0.15.0"
version = "0.16.0"
authors = ["Apify Technologies <[email protected]>"]
description = "Rust client for the Apify API (https://apify.com)."
license = "Apache-2.0"
Expand Down
27 changes: 24 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,10 @@ Every request carries a `User-Agent` of the form
| `client.me()` / `client.user(id)` | single | User accounts |
| `client.log(build_or_run_id)` | single | Build/run logs |

`client.actor(id).server()` returns a client for the server interface of a Server Actor (formerly
Actor Standby): it sends HTTP requests to the Actor's web server with the client's token, and gets
the server's URL and OpenAPI specification. See [Actors](docs/actors.md#server-actors).

`client.api_base_url()` returns the API base URL the client targets, including `/v2` and any
`base_url` override. The iterators returned by `iterate(..)` (and by `RunClient::iterate_dataset_items`)
yield one item per `next().await?`; `collect_all().await?` drains the remaining items into a `Vec`.
Expand Down Expand Up @@ -392,7 +396,9 @@ pool, …), as shown below; to route requests through an entirely different HTTP
`log().stream()` and `download_items_stream()`) has a default implementation that buffers the
whole response via `send`, so it is optional unless you want true incremental streaming. A request
whose `timeout` is `timeouts::NO_TIMEOUT` (see [Timeouts](#timeouts)) may run for as long as it
takes, so a backend must check for it before it applies a timeout of its own:
takes, so a backend must check for it before it applies a timeout of its own. A request whose
`follow_redirects` is `false`, such as one to an Actor's server, carries a token that must not
follow a redirect to another host, so a backend must return a redirect response as it is:

```rust,no_run
use apify_client::http_client::{HttpBackend, HttpRequest, HttpResponse};
Expand Down Expand Up @@ -430,13 +436,28 @@ The snippet below constructs a
use std::sync::Arc;
use apify_client::ApifyClient;
use apify_client::http_client::ReqwestBackend;
use reqwest::redirect::Policy;

// Share a pre-configured reqwest client (proxy, TLS, connection pool, ...).
// Share a pre-configured reqwest client (proxy, TLS, connection pool, ...). The requests that must
// not follow redirects, such as those to an Actor's server, go through a second client: give it
// the same settings, and a policy that follows no redirects.
let reqwest_client = reqwest::Client::builder().build().unwrap();
let backend = Arc::new(ReqwestBackend::with_client(reqwest_client));
let no_redirect_client = reqwest::Client::builder()
.redirect(Policy::none())
.build()
.unwrap();
let backend = Arc::new(
ReqwestBackend::with_client(reqwest_client).with_no_redirect_client(no_redirect_client),
);
let client = ApifyClient::builder().token("t").http_backend(backend).build();
```

A `reqwest::Client` fixes its redirect policy when it is built, so `ReqwestBackend` sends the
requests that must not follow redirects with a second client. `with_client` gives it a default one,
without your proxy, TLS or CA settings, so replace it with `with_no_redirect_client` when the
requests to an Actor's server need those too. The replacement has to be built with
`redirect(reqwest::redirect::Policy::none())`.

## License

Licensed under the [Apache License, Version 2.0](LICENSE).
10 changes: 8 additions & 2 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ types is:
- Store: `StoreListOptions`
- Logs: `LogOptions`, `LogRedirect`, `LogSink`, `StreamedLog`, `StreamedLogOptions`
- Shared: `ListOptions`, `StorageListOptions`
- Server Actors: `ActorServerRequestOptions`, `ActorServerBody`, `ActorServerCompression`
- Client configuration: `RequestCompression`

plus the common container `PaginationList`, `ListIterator`
Expand Down Expand Up @@ -148,7 +149,7 @@ Builder options:
| `default_header(name, value)` | none | Header sent with every request. It wins over the built-in `User-Agent` and `Authorization` headers; a header a request sets itself wins over it. Names are compared case-insensitively. |
| `request_compression(c)` | `Brotli` | Encoding for large request bodies: `RequestCompression::Brotli` (`br`) or `RequestCompression::Gzip` (`gzip`). |
| `http_compressor(c)` | Brotli, quality 6 | A custom `Arc<dyn HttpCompressor>`: `BrotliHttpCompressor::new(quality)` (0 to 11, default 6), `GzipHttpCompressor::new(quality)` (1 to 9, default 6), or your own implementation. |
| `http_backend(b)` | reqwest | Replaceable HTTP transport (`apify_client::http_client::{HttpBackend, ReqwestBackend}`). A request whose `timeout` is `timeouts::NO_TIMEOUT` may run for as long as it takes, so a backend must check for it before it applies a timeout of its own. |
| `http_backend(b)` | reqwest | Replaceable HTTP transport (`apify_client::http_client::{HttpBackend, ReqwestBackend}`). A request whose `timeout` is `timeouts::NO_TIMEOUT` may run for as long as it takes, so a backend must check for it before it applies a timeout of its own. A request whose `follow_redirects` is `false`, such as one to an Actor's server, carries a token that must not follow a redirect to another host, so a backend must return a redirect response as it is. |

Request bodies of at least 1024 bytes are compressed once (before retries) with the selected
`request_compression` algorithm and sent with the matching `Content-Encoding` header. Brotli is
Expand Down Expand Up @@ -272,7 +273,12 @@ iterates everything); to control the per-request page size, call `.with_chunk_si
returned iterator (offset-paginated iterators only). An offset-paginated iterator ignores the
`total` a page reports and keeps requesting pages until one scans no rows, so a listing that grows
while being iterated (e.g. a dataset a still-running Actor keeps pushing to) is read to the end
rather than cut off at the size it had when the loop started.
rather than cut off at the size it had when the loop started. Each page advances the offset by the
page's `count` or by the number of items it returned, whichever is larger, capped at the page size
requested. Without `with_chunk_size`, each request gets the API's default page size, capped by
`limit`. Dataset items have no default limit, so iterating `iterate_items()` without
`with_chunk_size` loads the whole dataset, or all `limit` items, in one response: set a page size
to iterate a large dataset.

```rust,no_run
use apify_client::{ApifyClient, ActorListOptions};
Expand Down
131 changes: 131 additions & 0 deletions docs/actors.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ be an Actor ID or a `username~name` (or `username/name`) reference.
| `runs()` | — | `RunCollectionClient` | The Actor's run collection. |
| `version(n)` / `versions()` | `&str` / — | `ActorVersionClient` / collection | Version management. |
| `webhooks()` | — | `WebhookCollectionClient` | The Actor's webhooks. |
| `server()` | — | `ActorServerClient` | Client for the server interface of a [Server Actor](#server-actors). |
| `standby()` | — | `ActorServerClient` | Deprecated alias of `server()`, named after Actor Standby, the former name of Server Actors. |

### `ActorStartOptions`

Expand Down Expand Up @@ -190,6 +192,9 @@ example:
| `is_public` | `Option<bool>` | Whether the Actor is published in Apify Store. |
| `created_at` | `Option<DateTime<Utc>>` | When the Actor was created. |
| `modified_at` | `Option<DateTime<Utc>>` | When the Actor was last modified. |
| `standby_url` | `Option<String>` | URL for sending requests to the Actor's server, or `None` if the Standby mode isn't enabled. |
| `actor_standby` | `Option<ActorStandby>` | The Standby mode configuration: `is_enabled`, `desired_requests_per_actor_run`, `max_requests_per_actor_run`, `idle_timeout_secs`, `build`, `memory_mbytes`, `disable_standby_fields_override`, `should_pass_actor_input`, `tenancy` (`SINGLE_TENANT` or `MULTI_TENANT`), `is_console_auth_enabled` and `is_tokenless_enabled`. When creating an Actor, omit the last two or set them to `false`; when updating one, only Apify administrators can change them. |
| `tagged_builds` | `Option<HashMap<String, Option<TaggedBuildInfo>>>` | Maps build tag names (e.g. `latest`) to `TaggedBuildInfo { build_id, build_number, finished_at }`. |
| `extra` | `Extra` | Any other fields returned by the API. |

```rust,no_run
Expand Down Expand Up @@ -230,13 +235,139 @@ println!("build {} status {:?}", build.id, build.status);
# }
```

## Server Actors

A Server Actor (formerly Actor Standby) runs a web server. You call it over HTTP, and the
platform keeps a run ready to answer, so each call skips the run startup. `ActorClient::server()`
returns an `ActorServerClient` for it. The Actor's `standbyUrl` and `actorStandby` fields still use
the old name.

| Method | Arguments | Returns | Description |
|---|---|---|---|
| `request(path, options)` | `&str`, `ActorServerRequestOptions` | `HttpResponse` | Sends a request to the Actor's server and buffers the response. |
| `request_stream(path, options)` | `&str`, `ActorServerRequestOptions` | `StreamingHttpResponse` | Like `request`, with the response body as a stream. It is retried like `request` until the response starts, and not once the body is being read. Its timeout bounds only the wait for the response to start, so a server that stays silent between two chunks, such as one that sends events, is not cut off. |
| `get_url()` | — | `String` | The URL of the Actor's server. |
| `get_open_api_spec()` | — | `Option<serde_json::Value>` | The Actor's web server schema, or `None` if its definition has none. |

`ActorServerRequestOptions` has these fields, all optional:

| Field | Type | Description |
|---|---|---|
| `method` | `Option<HttpMethod>` | HTTP method of the request. Default is `GET`. |
| `params` | `Vec<(String, String)>` | Query parameters of the request, in order. A key can repeat. |
| `headers` | `HashMap<String, String>` | Additional headers of the request. They reach the Actor's server as they are, `Authorization` included. |
| `body` | `Option<ActorServerBody>` | `Json(Value)` (sent with `Content-Type: application/json` unless you set a content type), `Text(String)` or `Bytes(Vec<u8>)` (both sent as they are). `ActorServerBody::json(&value)` serializes a value. |
| `compression` | `Option<ActorServerCompression>` | `Algorithm(RequestCompression)` or `Custom(Arc<dyn HttpCompressor>)`. The body is sent uncompressed when it's `None`, since the Actor's server may not decode a compressed body. Set it only if the server decodes the resulting `Content-Encoding`. |

```rust,no_run
use apify_client::{ActorServerBody, ActorServerRequestOptions, ApifyClient};
use apify_client::http_client::HttpMethod;
use serde_json::json;

# async fn run() -> Result<(), Box<dyn std::error::Error>> {
let client = ApifyClient::new("my-api-token");
let server = client.actor("username/actor-name").server();

// Send a GET request to the /search endpoint of the Actor's server.
// The method returns error responses too, so check the status first.
let search = server
.request(
"/search",
ActorServerRequestOptions {
params: vec![("q".to_string(), "apify".to_string())],
..Default::default()
},
)
.await?;
if !search.is_success() {
return Err(format!("Search failed with status {}", search.status).into());
}
println!("{}", search.text());

// Send a POST request with a JSON body.
let items = server
.request(
"/items",
ActorServerRequestOptions {
method: Some(HttpMethod::Post),
body: Some(ActorServerBody::Json(json!({ "url": "https://apify.com" }))),
..Default::default()
},
)
.await?;
println!("{}", items.status);
# Ok(())
# }
```

`HttpResponse` has the `status`, the `headers` and the `body` bytes, with `header(name)`,
`is_success()`, `text()` and `json::<T>()` to read them. The body of `request_stream` is a stream of
byte chunks, like the other streaming methods.

How the request is sent:

- The server URL is the Actor's `standby_url`. The first call of `request`, `get_url` or
`get_open_api_spec` fetches the Actor and every client returned by the same `ActorClient` reuses
the URL. A call on an Actor that isn't a Server Actor fails with `InvalidArgument`, and one on a
missing Actor fails with the `404` `ApiError`.
- The path is always resolved against that URL, so it can't send the request to another host. A
fragment (`#...`) is dropped.
- The token goes in the `X-Apify-Authorization` header, which the platform removes before it
forwards the request, so the token doesn't reach the Actor's code. The headers you pass reach
the Actor as they are. If the Actor reads the `Authorization` header for its own
authentication, pass that header in `headers`.
- Redirects are not followed, and a redirect response is returned as it is, since following it would
send the token to a host the Actor's code picked.
- The timeout defaults to the `Long` tier, to cover a cold start. Set another one with the
client's `with_timeout`.
- The server's response is returned whatever its status. Only `429` and `503` are retried, with
the client's retry settings, and the last response is returned once the retries run out. The
platform sends these when it can't pass the request to a server run, but the Actor's code can
return them too, and then a retry sends the request to the Actor again. Network errors and
timeouts are not retried, since the Actor may have already handled the request, and they fail
the call.

`get_open_api_spec` returns the web server schema from the build that serves the server
interface: the build in the Actor's `actor_standby.build` field (a tag or a build number), or
`latest`. It fails with `InvalidArgument` if that build doesn't exist.

```rust,no_run
# use apify_client::ApifyClient;
# async fn run() -> Result<(), Box<dyn std::error::Error>> {
let client = ApifyClient::new("my-api-token");
let server = client.actor("username/actor-name").server();

// Get the URL of the Actor's server.
println!("{}", server.get_url().await?);

// Get the OpenAPI specification and list the server's endpoints.
if let Some(spec) = server.get_open_api_spec().await? {
if let Some(paths) = spec["paths"].as_object() {
println!("{:?}", paths.keys().collect::<Vec<_>>());
}
}
# Ok(())
# }
```

## Actor versions and environment variables

`ActorVersionClient`: `get`, `update`, `delete`, `env_var(name)`, `env_vars()`.
`ActorVersionCollectionClient`: `list()`, `iterate()`, `create(version)`.
`ActorEnvVarClient`: `get`, `update`, `delete`.
`ActorEnvVarCollectionClient`: `list()`, `iterate()`, `create(env_var)`.

A version of the `SOURCE_FILES` or `TARBALL` source type can set `actorPath`, the Actor's directory
inside the uploaded source, for a monorepo: a path relative to the upload root with `/` separators,
for example `actors/typescript-actor`. It has 1 to 255 characters, with no leading, trailing, or
repeated `/` and no `.` or `..` segments, and the API rejects it for `GIT_REPO` and `GITHUB_GIST`
versions. Without it, the upload root is the Actor's directory. `ActorVersion::actor_path` holds it
(for a `TARBALL` version, only users with write access to the Actor get it). An update that sends
`sourceType`, `sourceFiles` or `tarballUrl` without `actorPath` removes the stored path, so send it
again with every upload. An update of other fields, such as `envVars` or `buildTag`, keeps it, and
`"actorPath": null` removes it. An `ActorVersion` sent as an update body leaves `actor_path` out
when it is `None`.

Creating a version needs `versionNumber` and `sourceType` in the body, and so does every item of
the `versions` array in an Actor create or update body. The writable source types are
`SOURCE_FILES`, `GIT_REPO`, `TARBALL` and `GITHUB_GIST`; `SOURCE_CODE` can be read but not sent.
Expand Down
Loading
Loading