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
4 changes: 4 additions & 0 deletions .github/workflows/rust-integration-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ jobs:
- name: Clippy with rustls
run: cargo clippy --no-default-features --features rustls -- -D warnings

# The `zstd` feature is on by default, so the build without it needs its own check.
- name: Clippy without zstd
run: cargo clippy --no-default-features --features native-tls --all-targets -- -D warnings

# Fail fast if the integration-test secret is missing or empty. Without this guard the
# integration tests silently "pass" (they early-return when APIFY_TOKEN is unset), so a
# green run would not prove the API logic actually executed.
Expand Down
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,22 @@ 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.17.0] - 2026-11-10

### Added
- `ZstdHttpCompressor` (quality 1 to 19, default 3) and `RequestCompression::Zstd`, which send
request bodies with `Content-Encoding: zstd`. They need the new `zstd` cargo feature, which is
on by default.
- `ActorServerRequestOptions::parent_request_id`, sent as the `X-Actor-Request-ID` header so a
Server Actor that calls another one passes on the pricing tier of the request it handles.

### Changed
- Synced with Apify OpenAPI spec `v2-2026-11-09T143926Z` (`API_SPEC_VERSION`), which lists `zstd`
among the `Content-Encoding` values and describes `actorPath` on Actor versions.
- **Breaking:** `ActorServerRequestOptions` has the new field `parent_request_id`, so a struct
literal without `..Default::default()` no longer compiles.
- Shortened code comments and documentation, and removed the references to other clients.

## [0.16.0] - 2026-11-06

Ports the `apify-client-js` Server Actor support and the `count` of dataset item pages.
Expand Down
8 changes: 6 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "apify-client"
version = "0.16.0"
version = "0.17.0"
authors = ["Apify Technologies <[email protected]>"]
description = "Rust client for the Apify API (https://apify.com)."
license = "Apache-2.0"
Expand All @@ -27,6 +27,7 @@ hmac = "0.12"
sha2 = "0.10"
brotli = "7"
flate2 = "1"
zstd = { version = "0.13", optional = true }
# Minimum-version floors for the TLS trust stack behind the `rustls` feature. The crates are not
# used directly: `reqwest` pulls them in, and the floor here makes Cargo resolve at least the
# releases that no longer trust the compromised intermediate CA certificates.
Expand All @@ -38,6 +39,9 @@ tokio = { version = "1", features = ["macros", "rt-multi-thread", "time", "test-
uuid = { version = "1", features = ["v4"] }

[features]
default = ["native-tls"]
default = ["native-tls", "zstd"]
native-tls = ["reqwest/native-tls"]
rustls = ["reqwest/rustls-tls", "dep:rustls-webpki", "dep:webpki-roots"]
# Zstd request compression. It compiles a C library. To build without it, disable the default
# features and name a TLS backend (`native-tls` or `rustls`).
zstd = ["dep:zstd"]
72 changes: 33 additions & 39 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@
> relying on it in production and report issues on the repository.

An idiomatic Rust client for the [Apify API](https://docs.apify.com/api/v2).
It provides a resource-oriented, async interface that mirrors the official
[JavaScript](https://github.com/apify/apify-client-js) and
[Python](https://github.com/apify/apify-client-python) clients.
It provides a resource-oriented, async interface that follows the
[JavaScript client](https://github.com/apify/apify-client-js).

- Async (Tokio-friendly), built on `reqwest`.
- Transparent authentication, retries with exponential backoff, and timeouts.
Expand All @@ -21,7 +20,7 @@ It provides a resource-oriented, async interface that mirrors the official

```toml
[dependencies]
apify-client = "0.15"
apify-client = "0.17"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
serde_json = "1" # for the `serde_json::Value` responses used in the Quick start
```
Expand All @@ -45,10 +44,12 @@ project needs `serde_json`. Two more dependencies are needed only for specific f
snippet needs it as a direct dependency, and its version must match this crate's `reqwest`
version (`0.12`) so the `reqwest::Client` types are compatible.

By default the client uses the system TLS (`native-tls`). To use rustls instead:
By default the client uses the system TLS (`native-tls`) and has the `zstd` feature. To use rustls
instead, disable the default features and name `rustls`. The `"zstd"` entry keeps zstd compression,
so drop it if you do not need it:

```toml
apify-client = { version = "0.15", default-features = false, features = ["rustls"] }
apify-client = { version = "0.17", default-features = false, features = ["rustls", "zstd"] }
```

The `rustls` feature requires `rustls-webpki` 0.103.16 or newer and `webpki-roots` 1.0.10 or
Expand Down Expand Up @@ -108,7 +109,7 @@ let client = ApifyClient::builder()
.timeout_max(Duration::from_secs(360)) // default: 360s, cap on a single attempt
.user_agent_suffix("my-app/1.0")
.default_header("X-My-Header", "value") // sent with every request
.request_compression(RequestCompression::Brotli) // default: Brotli; or RequestCompression::Gzip
.request_compression(RequestCompression::Brotli) // default: Brotli; or Gzip or Zstd
.build();
```

Expand Down Expand Up @@ -158,12 +159,16 @@ let items = client

Request bodies of at least 1024 bytes are compressed once (before retries) and sent with the
matching `Content-Encoding` header. The default is Brotli (`br`); select
`RequestCompression::Gzip` (`gzip`) for environments or intermediaries that do not handle brotli.
`RequestCompression::Gzip` (`gzip`) for environments or intermediaries that do not handle brotli,
or `RequestCompression::Zstd` (`zstd`) for fast compression at a good ratio.
`RequestCompression` is re-exported at the crate root
(`use apify_client::RequestCompression;`). To tune the quality or use another algorithm, pass an
`HttpCompressor` to `ApifyClientBuilder::http_compressor`: the built-in
`BrotliHttpCompressor` (quality 0 to 11, default 6) and `GzipHttpCompressor` (quality 1 to 9,
default 6), or your own implementation of the trait.
`BrotliHttpCompressor` (quality 0 to 11, default 6), `GzipHttpCompressor` (quality 1 to 9,
default 6), and `ZstdHttpCompressor` (quality 1 to 19, default 3), or your own implementation of
the trait. Zstd needs the `zstd` cargo feature, which is on by default and compiles a C library.
To build without it, disable the default features and name the TLS backend you use, for example
`default-features = false, features = ["native-tls"]`.

```rust,no_run
use std::sync::Arc;
Expand Down Expand Up @@ -192,10 +197,9 @@ it: `calls()` (each call counts once), `requests()` (every attempt counts) and

Every request carries a `User-Agent` of the form
`ApifyClient/{client_version} ({os}; Rust/{rust_version}); isAtHome/{true|false}`, plus any
`user_agent_suffix` you set. The `{os}` token matches the reference clients' `os.platform()` value
(the client maps Rust's `macos`/`windows`/`solaris`/`illumos` to `darwin`/`win32`/`sunos`;
`linux`, `android`, and the rest pass through), so it is identical across all Apify clients.
`isAtHome` reflects the `APIFY_IS_AT_HOME` environment variable.
`user_agent_suffix` you set. The `{os}` token is the Node.js `os.platform()` value (Rust's
`macos`, `windows`, `solaris` and `illumos` become `darwin`, `win32`, `sunos` and `sunos`; the
rest pass through). `isAtHome` reflects the `APIFY_IS_AT_HOME` environment variable.

## Resource clients

Expand Down Expand Up @@ -316,23 +320,18 @@ See [`docs/`](docs) for the full API documentation.

## Scope

This client deliberately mirrors the public surface of the official JavaScript reference
client. A few documented API endpoints are intentionally **not** exposed because the
reference client does not expose them either, keeping the clients consistent:
The client has the same public surface as the JavaScript client. These documented endpoints are
not exposed, because the JavaScript client does not expose them either:

- Synchronous run endpoints (`run-sync`, `run-sync-get-dataset-items`).
- Cryptographic tools (`/tools/encode-and-sign`, `/tools/decode-and-verify`).
- `/browser-info`.
- The `POST` create-with-explicit-key variants of Actor versions
(`POST /v2/actors/{actorId}/versions/{versionNumber}`) and version env-vars
(`POST /v2/actors/{actorId}/versions/{versionNumber}/env-vars/{envVarName}`). The reference
client creates versions/env-vars via `POST` to the collection and upserts via `PUT` on the
keyed path (both supported here as `versions().create(...)` / `version(n).update(...)` and
the env-var equivalents), so the redundant keyed-`POST` create is intentionally omitted.
(`POST /v2/actors/{actorId}/versions/{versionNumber}/env-vars/{envVarName}`). Create them with
`versions().create(...)` and the env-var equivalent, and update them with `version(n).update(...)`.
- The `POST` store-record variant (`POST /v2/key-value-stores/{storeId}/records/{recordKey}`),
which the spec defines as behaving identically to the covered `PUT` variant. Records are
stored via `set_record_raw(...)` / `set_record_json(...)` (`PUT`), matching the reference
client; the redundant keyed-`POST` alias is intentionally omitted.
which behaves like the `PUT` variant. Use `set_record_raw(...)` or `set_record_json(...)`.

If you need these, call them directly through a custom `HttpBackend` or open an issue.

Expand Down Expand Up @@ -367,34 +366,29 @@ match client.actor("nonexistent~actor").get().await {
```

`get`/`delete` operations resolve a missing resource (any `404`) to `Ok(None)` / a successful
no-op, matching the reference clients, but only on a client that names its resource by id. A
no-op, but only on a client that names its resource by id. A
client chained from a parent without an id of its own, such as `run.dataset()` or `run.log()`,
returns the `404` as an error: the response cannot tell a missing run from a missing dataset. The
same goes for methods that address a fixed sub-path of a resource (`get_statistics`, `get_input`,
`get_log`, `get_openapi_definition`, `limits`, `monthly_usage`): they return the `404` as an error.

`ApiError` also exposes the `is_invalid_request()` (400), `is_unauthorized()` (401),
`is_forbidden()` (403), `is_not_found()` (404), `is_conflict()` (409), `is_rate_limited()` (429) and
`is_server_error()` (5xx) predicates classifying it by HTTP status — the idiomatic equivalent of the reference
clients' `NotFoundError`/`RateLimitError`/... error subclasses, since Rust has no exception
hierarchy to mirror them with.
`is_server_error()` (5xx) predicates that classify it by HTTP status.

### Cancellation

The reference JavaScript client accepts an `AbortSignal` option on most methods. Rust futures do
nothing until polled, so dropping one — e.g. via `tokio::select!`, or letting a
`tokio::time::timeout` elapse — stops the underlying request with no separate API needed; that is
the idiomatic equivalent here.
Methods take no cancellation parameter. Drop the future a method returns, for example through
`tokio::select!` or `tokio::time::timeout`, to stop the request.

## Custom HTTP transport

The transport is a replaceable component: any type implementing `http_client::HttpBackend` can be
injected with `ApifyClientBuilder::http_backend`. The most common customization is to reuse the
built-in `ReqwestBackend` with a `reqwest::Client` you pre-configure (custom proxy, TLS, connection
pool, …), as shown below; to route requests through an entirely different HTTP stack, implement
`HttpBackend` on your own type instead — just `async fn send`; `send_streaming` (used by
`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
Any type that implements `http_client::HttpBackend` can be injected with
`ApifyClientBuilder::http_backend`. To customize the proxy, TLS, or connection pool, wrap a
`reqwest::Client` you configure in the built-in `ReqwestBackend`, as shown below. To use another
HTTP stack, implement `HttpBackend` yourself: `send` is required, and `send_streaming` (used by
`log().stream()` and `download_items_stream()`) buffers the whole response through `send` unless
you override it for 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. A request whose
`follow_redirects` is `false`, such as one to an Actor's server, carries a token that must not
Expand Down
16 changes: 7 additions & 9 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Add the crate and an async runtime:

```toml
[dependencies]
apify-client = "0.15"
apify-client = "0.17"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
```

Expand Down Expand Up @@ -147,8 +147,8 @@ Builder options:
| `timeout_max(d)` | `360s` | Cap on the timeout of a single attempt. `timeout(d)` is the same option under its original name. |
| `user_agent_suffix(s)` | none | Extra text appended to the `User-Agent` header. |
| `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. |
| `request_compression(c)` | `Brotli` | Encoding for large request bodies: `RequestCompression::Brotli` (`br`), `Gzip` (`gzip`), or `Zstd` (`zstd`; needs the `zstd` feature, on by default). |
| `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), `ZstdHttpCompressor::new(quality)` (1 to 19, default 3), 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. 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
Expand All @@ -166,12 +166,10 @@ let client = ApifyClient::builder()

The `User-Agent` header has the form
`ApifyClient/{client_version} ({os}; Rust/{rust_version}); isAtHome/{true|false}` where the `{os}`
token matches the reference client's `os.platform()` value (e.g. `darwin`, `win32`, `linux`) so it
is identical across all Apify clients, and `isAtHome` reflects the `APIFY_IS_AT_HOME` environment
variable (the platform variable the reference clients read; rendered lowercase to match them). To
reach those values the client maps Rust's native `std::env::consts::OS` spellings to Node's
(`macos` → `darwin`, `windows` → `win32`, `solaris`/`illumos` → `sunos`); all other tokens
(`linux`, `android`, `freebsd`, …) are already identical and pass through unchanged.
token is the Node.js `os.platform()` value (e.g. `darwin`, `win32`, `linux`), and `isAtHome` is
`true` or `false` according to the `APIFY_IS_AT_HOME` environment variable. The client maps Rust's
`macos`, `windows`, `solaris` and `illumos` to `darwin`, `win32`, `sunos` and `sunos`; other tokens
pass through unchanged.

### Timeouts

Expand Down
Loading
Loading