Skip to content
Closed
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
60 changes: 60 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,66 @@ All notable changes to the Apify Java client are documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.12.0] - 2026-10-30

### Added

- Timeout tiers: `Timeout` (`SHORT`, `MEDIUM`, `LONG`, `NO_TIMEOUT`, `of(Duration)`), the builder
setters `timeoutShort`, `timeoutMedium`, `timeoutLong` (defaults 5s, 30s, 360s) and `timeoutMax`
(cap of a single attempt, default 360s), and `ApifyClient.withTimeout(Timeout)` to replace the tier
of every request a client sends. Every method now picks its tier as the reference client does
(metadata reads and writes `SHORT`; listing, batch and trigger calls `MEDIUM`; downloads, uploads
and streaming `LONG`), a request with `waitForFinish` adds the hold to its tier, and the polling of
`waitForFinish`/`call` has no timeout. `SetRecordOptions.timeout(Timeout)`.
- Configurable request compression: `HttpCompressor`, `BrotliHttpCompressor` (quality 0-11) and
`GzipHttpCompressor` (level 1-9) with `ApifyClientBuilder.compression(...)`.
- `ApifyClientBuilder.header(name, value)`: default headers sent with every request.
- `RunClient.iterateDatasetItems(...)`: iterate the items of a run's default dataset while the run
is still producing them.
- `RunListOptions.deployment(...)` and `StoreListOptions.includeInputSchema(...)`.
- `UserClient.limits()`, `updateLimits(...)` and `monthlyUsage(...)` work for any user id.
- Typed fields: `ActorRun.getStandby()` (`ActorRunStandby`), `getConsoleUrl()`, `getOutput()`,
`getEventUsage()`, `getAccountedChargedEventCounts()`, `getExternalChargedTotalUsd()`,
`getIntegrationTracking()`, `getCreatedByOrganizationMemberUserId()`; `ActorRunMeta` origin ids;
`title`, `consoleUrl` and public URLs on `Dataset`, `KeyValueStore`, `KeyValueStoreKey` and
`RequestQueue`; `Webhook` integration fields; `Schedule.getUiState()`; `Task.getStandbyUrl()`;
`TaskStats.getLastRunStartedAt()`; `Build` and `BuildOptions` fields; `ActorVersion` webhook ids
and source URLs; `ActorEnvVar.getValueHash()`; `ActorStandby` tenancy and auth flags; the
`maxTotalChargeUsd` run options.

### Changed

- Updated to Apify OpenAPI specification `v2-2026-10-29T100357Z`.
- The `format: uri` fields of the models are returned in the normalized WHATWG form (empty path `/`,
lowercase punycode host, default port dropped, unsafe characters percent-encoded).
- A resource client's id is percent-encoded into a single path segment, and an empty id or a bare
`.`/`..` is rejected with `IllegalArgumentException` instead of addressing the collection.
- `ApifyClientBuilder.timeout(Duration)` is deprecated: it is an alias of `timeoutMax(Duration)`.
- The silent fallback from a failing brotli encode to gzip is gone: the configured compressor is the
only one that runs, and its failure fails the request. Brotli stays the default (quality 6) where
its native library loads, gzip elsewhere.
- `batchAddRequests` serializes each request once instead of twice.
- A `waitForFinish` hold extends the timeout of its request (the tier's plus the hold) instead of
being clamped below it; a hold is cut down only to what `timeoutMax` (or a `withTimeout` override)
leaves, so the API still answers before the client gives up. The polls of `waitForFinish`/`call`
have no timeout and ignore `withTimeout`.
- `Build.getChangelog()` is deprecated, as the specification deprecates `Build.changelog`.
- The integration tests wait up to 90 s for a just-created resource to appear in a listing.

### Fixed

- A `pushItems` request that times out is no longer retried, so the items cannot be stored twice.
- Cancelling a future the client returned (`CompletableFuture.cancel`) now aborts the request in
flight and stops the retries, `waitForFinish`/`call` polling and iteration pages that would follow;
cancelling a derived stage (`thenApply`, `thenCompose`, ...) cancels the call it comes from.
Cancelling `call(...)` also stops its log redirection, and closing a `StreamedLog` while its
stream is still opening launches no reader thread (the `start()` future is cancelled). See the
Cancellation section of the README.
- `pushItems` rejects what the API rejects (anything but an object or an array of objects) before a
request is sent, and sends a pre-serialized `String` as it is.
- An Actor or task input given as `byte[]` or `String` is sent as it is instead of being wrapped in
a second layer of JSON.

## [0.11.0] - 2026-10-26

### Added
Expand Down
117 changes: 102 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Maven (Maven Central is a default repository, so no extra configuration is neede
<dependency>
<groupId>com.apify</groupId>
<artifactId>apify-client</artifactId>
<version>0.11.0</version>
<version>0.12.0</version>
</dependency>
```

Expand All @@ -35,7 +35,7 @@ repositories {
}

dependencies {
implementation 'com.apify:apify-client:0.11.0'
implementation 'com.apify:apify-client:0.12.0'
}
```

Expand Down Expand Up @@ -133,7 +133,12 @@ ApifyClient configured =
.baseUrl("https://api.apify.com") // /v2 is appended automatically
.maxRetries(8)
.minDelayBetweenRetries(Duration.ofMillis(500))
.timeout(Duration.ofSeconds(360))
.timeoutShort(Duration.ofSeconds(5))
.timeoutMedium(Duration.ofSeconds(30))
.timeoutLong(Duration.ofSeconds(360))
.timeoutMax(Duration.ofSeconds(360))
.compression(new GzipHttpCompressor(6))
.header("X-Integration", "my-platform")
.userAgentSuffix("MyTool/1.0")
.build();
```
Expand All @@ -152,15 +157,96 @@ validated at call time (see below), not deferred to `build()`.
| `publicBaseUrl(String)` | `String` | same as `baseUrl` | non-null, non-blank |
| `maxRetries(int)` | `int` | `8` | `>= 0` |
| `minDelayBetweenRetries(Duration)` | `Duration` | `500ms` | non-null, `>= 0` |
| `maxDelayBetweenRetries(Duration)` | `Duration` | same as `timeout` (`360s`) | non-null, `>= 0` |
| `timeout(Duration)` | `Duration` | `360s` | non-null, strictly `> 0` |
| `maxDelayBetweenRetries(Duration)` | `Duration` | same as `timeoutMax` (`360s`) | non-null, `>= 0` |
| `timeoutShort(Duration)` | `Duration` | `5s` | non-null, strictly `> 0` |
| `timeoutMedium(Duration)` | `Duration` | `30s` | non-null, strictly `> 0` |
| `timeoutLong(Duration)` | `Duration` | `360s` | non-null, strictly `> 0` |
| `timeoutMax(Duration)` | `Duration` | `360s` | non-null, strictly `> 0` |
| `compression(HttpCompressor)` | `HttpCompressor` | `BrotliHttpCompressor` (quality 6), or `GzipHttpCompressor` where the brotli native library is unavailable | non-null |
| `header(String, String)` | `String`, `String` | none | non-blank name, non-null value |
| `userAgentSuffix(String)` | `String` | none (no suffix appended) | any string |
| `httpTransport(HttpTransport)` | `HttpTransport` | `new DefaultHttpTransport()` | non-null |

A violated range throws `IllegalArgumentException` from the setter itself. `timeout` is the ceiling
each retry attempt's per-request (socket) timeout grows toward — not a wall-clock bound on the
cumulative time across all retries; the connection-establishment timeout is a separate,
transport-level setting (see `DefaultHttpTransport`'s constructors below).
A violated range throws `IllegalArgumentException` from the setter itself. The deprecated
`timeout(Duration)` is an alias of `timeoutMax(Duration)`.

### Timeouts

Every method picks the timeout tier that fits the expected duration of its request: `short` (5s) for
simple metadata reads and writes, `medium` (30s) for listing, batch and trigger operations, `long`
(360s) for downloads, uploads and streaming. The three durations are set with `timeoutShort`,
`timeoutMedium` and `timeoutLong`. A method that asks the API to hold the response with
`waitForFinish` (`getWithWait`, `start`, building an Actor, ...) gets its tier plus that hold (at
most 60s), so the client does not abort a request the API is still holding. The polling of
`waitForFinish(...)`/`call(...)` and the start inside `call(...)` have no timeout.

`timeoutMax` caps the timeout of a single request attempt. The first attempt gets the timeout of the
method's tier, each retry doubles it, and none exceeds `timeoutMax`; a tier or timeout above the cap
is capped and logged as a warning. It bounds each attempt, not the wall-clock time across all
retries. The connection-establishment timeout is a separate, transport-level setting (see
`DefaultHttpTransport`'s constructors below).

A call that legitimately needs a different timeout gets it from `ApifyClient.withTimeout(Timeout)`,
which returns a client that sends every request with that timeout instead of the method's tier. A
`Timeout` is a tier (`Timeout.SHORT`, `MEDIUM`, `LONG`), an exact duration (`Timeout.ofSeconds(120)`,
`Timeout.of(Duration)`) or `Timeout.NO_TIMEOUT` (the request is never aborted, so a stalled connection
is not retried either):

```java
ApifyClient client = ApifyClient.create("my-api-token");
List<JsonNode> items =
client
.withTimeout(Timeout.ofSeconds(900))
.dataset("my-dataset-id")
.listItems(new DatasetListItemsOptions())
.join()
.getItems();
```

`KeyValueStoreClient.setRecord(...)` additionally takes the timeout in its `SetRecordOptions`.

A `pushItems` request that times out is not retried, since the API may already have stored the items.

A `waitForFinish` hold is cut down to what the request timeout can cover, so the API answers before
the client gives up on the request: with a small `timeoutMax`, the hold is what `timeoutMax` leaves
above the tier, and under a `withTimeout` override (which is capped at `timeoutMax` like any timeout)
it is the override less 5 seconds. The polls of `waitForFinish(...)` and `call(...)` are not affected
by `withTimeout`: they have no timeout and always hold for up to a minute, so cancel the returned
future (see below) to stop one that never answers.

### Cancellation

Every method returns a `CompletableFuture` (or a `Flow.Publisher`), and cancelling it cancels the
work behind it: `future.cancel(true)` aborts the request in flight and stops the retries, the
`waitForFinish`/`call` polling and the pages of an iteration that would follow; cancelling the
`Flow.Subscription` of a publisher does the same for the page being fetched. This is the way out of a
request that has no timeout (the polls of `waitForFinish`/`call` and `Timeout.NO_TIMEOUT`):

```java
CompletableFuture<ActorRun> call =
client.actor("my-actor").call(null, new ActorCallOptions(), null); // redirects the run's log
// ... later, when the result is no longer needed:
call.cancel(true); // stops the polling and the log redirection; the run itself keeps running
```

A cancelled future completes with a `CancellationException`. Cancelling a stage derived from a
client future (`future.thenApply(...)`, `.thenCompose(...)`) cancels the call it comes from, and so
fails the other stages derived from the same future with a `CancellationException` too; derive one
stage per call, or cancel the future the client returned. Cancelling does not undo what the API
already did: a started run keeps running (abort it with `RunClient.abort(...)`).

### Request compression and default headers

Request bodies of at least 1 KiB with a compressible content type are compressed with the
configured `HttpCompressor`: `BrotliHttpCompressor` (quality 0-11, default 6; used by default where
its native library is available) or `GzipHttpCompressor` (level 1-9, default 9), or your own
implementation of the interface. A failing compressor fails the request; the client does not fall
back to another coding. A body that already carries a `Content-Encoding`, and already-compressed
content types (images, archives, ...), are sent as they are.

`header(name, value)` adds a header to every request, such as an integration platform marker. It
replaces a built-in header of the same name (`User-Agent`, `Authorization`) and is replaced by a
header a request sets itself.

`publicBaseUrl` does not affect ordinary API requests (those always use `baseUrl`); it only changes
the origin embedded in the handful of public, directly-fetchable URLs the client builds without a
Expand Down Expand Up @@ -280,10 +366,11 @@ A few methods validate their own preconditions synchronously, before ever buildi
future, and throw a plain JDK exception directly from the call site instead, not an
`ApifyClientException` subtype (so no `.join()`/`.get()` unwrapping is involved for these):
`ApifyClient.setStatusMessage(message, options)` throws `IllegalStateException` if the
`ACTOR_RUN_ID` environment variable is not set, `ApifyClient.me()`'s limits methods (`limits()`,
`updateLimits(...)`, `monthlyUsage(...)`) throw the same `IllegalStateException` if called on a
`UserClient` that is not `me()`, and `ApifyClientBuilder`'s setters throw `IllegalArgumentException`
on an invalid configuration value (e.g. a negative retry count). These are local, no-request-sent
`ACTOR_RUN_ID` environment variable is not set, `ApifyClientBuilder`'s setters throw
`IllegalArgumentException` on an invalid configuration value (e.g. a negative retry count), a
resource client's id is rejected with `IllegalArgumentException` when it is empty or a bare `.`/`..`
(any other id is percent-encoded into one path segment), and `DatasetClient.pushItems(...)` throws
it for items the API would reject (anything but an object or an array of objects). These are local, no-request-sent
failures, not something the API responded with or the transport failed to deliver, which is why
they stay outside the request/transport exception hierarchy above — catch
`IllegalStateException`/`IllegalArgumentException` separately if you call any of them.
Expand Down Expand Up @@ -317,10 +404,10 @@ try {
The public `com.apify.client.Version` class (`import com.apify.client.Version;`) exposes two
constants:

- `Version.CLIENT_VERSION` — the semantic version of this client (`0.11.0`).
- `Version.CLIENT_VERSION` — the semantic version of this client (`0.12.0`).
- `Version.API_SPEC_VERSION` — the version of the [Apify OpenAPI specification](https://docs.apify.com/api/openapi.json)
(its `info.version` field) that this client's endpoints, parameters and models were last generated
and checked against (`v2-2026-10-26T112451Z`). It is a snapshot, not a live compatibility
and checked against (`v2-2026-10-29T100357Z`). It is a snapshot, not a live compatibility
guarantee: the client keeps working against newer, backward-compatible spec revisions, but a
feature added to the API after this snapshot has no corresponding method here yet.

Expand Down
20 changes: 15 additions & 5 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ ApifyClient client = ApifyClient.create("my-api-token");
```

`ApifyClient.create` takes the token as an explicit argument — it does not read `APIFY_TOKEN`
automatically. Use `ApifyClient.builder()` for non-default settings (base URL, retries, timeout,
user-agent suffix, custom HTTP transport).
automatically. Use `ApifyClient.builder()` for non-default settings (base URL, retries, timeout tiers,
compression, default headers, user-agent suffix, custom HTTP transport). `client.withTimeout(...)`
returns a client that sends every request with a different timeout (see
[Timeouts](../README.md#timeouts)).

Get your API token from the
[Apify Console → Settings → API & Integrations](https://console.apify.com/settings/integrations).
Expand All @@ -39,10 +41,10 @@ option types, so import each resource you use from its own package:
| Package | Contains |
|---|---|
| `com.apify.client` (root) | `ApifyClient`, `ApifyClientBuilder`, `Version`, `PaginationList<T>`, `ListOptions`, `StorageListOptions`, `ApifyResource` |
| `com.apify.client.http` | `ApifyClientException`, `ApifyApiException`, `ApifyTransportException`, `HttpTransport`, `DefaultHttpTransport`, `HttpTimeoutException` |
| `com.apify.client.http` | `ApifyClientException`, `ApifyApiException`, `ApifyTransportException`, `HttpTransport`, `DefaultHttpTransport`, `HttpTimeoutException`, `Timeout`, `HttpCompressor`, `BrotliHttpCompressor`, `GzipHttpCompressor` |
| `com.apify.client.actor` | `Actor`, `ActorClient`, `ActorCollectionClient`, `ActorListOptions`, `ActorStartOptions`, `ActorCallOptions`, `ActorBuildOptions`, `ActorStandby`, `ActorVersion`, `SourceCodeFile`, `ActorVersionClient`, `ActorVersionCollectionClient`, `ActorEnvVar`, `ActorEnvVarClient`, `ActorEnvVarCollectionClient`, `ValidateInputOptions`, `ActorStats`, `ActorDefaultRunOptions` |
| `com.apify.client.build` | `Build`, `BuildClient`, `BuildCollectionClient`, `BuildActVersion`, `ActorDefinition`, `ActorDefinitionMeta`, `ActorDefinitionStorages`, `BuildMeta`, `BuildStats`, `BuildOptions`, `BuildUsage` |
| `com.apify.client.run` | `ActorRun`, `ActorRunStats`, `ActorRunOptions`, `ActorRunMeta`, `ActorRunUsage`, `RunClient`, `RunCollectionClient`, `RunListOptions`, `LastRunOptions`, `MetamorphOptions`, `RunChargeOptions`, `RunResurrectOptions`, `SetStatusMessageOptions` |
| `com.apify.client.run` | `ActorRun`, `ActorRunStats`, `ActorRunOptions`, `ActorRunMeta`, `ActorRunStandby`, `ActorRunUsage`, `RunClient`, `RunCollectionClient`, `RunListOptions`, `LastRunOptions`, `MetamorphOptions`, `RunChargeOptions`, `RunResurrectOptions`, `SetStatusMessageOptions` |
| `com.apify.client.dataset` | `Dataset`, `DatasetClient`, `DatasetCollectionClient`, `DatasetListItemsOptions`, `DatasetDownloadOptions`, `DownloadItemsFormat` |
| `com.apify.client.keyvalue` | `KeyValueStore`, `KeyValueStoreClient`, `KeyValueStoreCollectionClient`, `KeyValueStoreRecord`, `KeyValueStoreKeysPage`, `KeyValueStoreKey`, `GetRecordOptions`, `SetRecordOptions`, `ListKeysOptions` |
| `com.apify.client.requestqueue` | `RequestQueue`, `RequestQueueClient`, `RequestQueueCollectionClient`, `RequestQueueRequest`, `RequestQueueHead`, `LockedRequestQueueHead`, `RequestQueueOperationInfo`, `RequestLockInfo`, `UnlockRequestsResult`, `RequestsList`, `BatchAddResult`, `BatchDeleteResult`, `DeletedRequestInfo`, `ListRequestsOptions`, `BatchAddRequestsOptions` |
Expand Down Expand Up @@ -71,13 +73,21 @@ Raw-JSON return values use Jackson's `tools.jackson.databind.JsonNode`. Jackson
`tools.jackson.*` package, not the older `com.fasterxml.jackson.databind.*` from Jackson 2) is a
transitive dependency of this client, so it is already on your classpath.

## Cancellation

Cancelling a future a client method returned (`future.cancel(true)`), or the `Flow.Subscription` of
an iteration, aborts the request in flight and stops the retries, the `waitForFinish`/`call` polling
and the pages that would follow; see [Cancellation](../README.md#cancellation) for the details and
the caveat about derived stages. A cancelled `call(...)` also stops redirecting the run's log, but
the run itself keeps running.

## Raw JSON values

A few methods return data whose shape is not modelled by this client and is instead exposed as a
Jackson `JsonNode` (or accept an arbitrary `Object` serialized to JSON):

- Read, returning a required `JsonNode` (never absent; a 404 throws rather than returning empty —
see [error handling](../README.md#error-handling)): `me().monthlyUsage(...)`, `me().limits()`,
see [error handling](../README.md#error-handling)): `me().monthlyUsage(...)`, `me().limits()` (and the same on `user(id)`),
`dataset(id).getStatistics()`, `task(id).getInput()`.
- Read, returning `Optional<JsonNode>` (empty when the underlying resource has none):
`build(id).getOpenApiDefinition()`.
Expand Down
Loading
Loading