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
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,28 @@ 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.13.0] - 2026-11-06

### Added

- Server Actor support: `ActorClient.server()` returns an `ActorServerClient` (`request`, `requestStream`, `getUrl`, `getOpenApiSpec`) configured by `ActorServerRequestOptions`; `ActorClient.standby()` is a deprecated alias.
- `ServerActorException` for an Actor that is not a Server Actor or a build that does not exist.
- `HttpTransport.sendAsync(HttpRequest, boolean)` and `sendStreamingAsync(HttpRequest, boolean)` to send a request without following redirects.
- `ApiResponse.bodyAsString()` and `ApiResponse.bodyAs(Class)`.
- `Actor.getStandbyUrl()`, `ActorVersion.getActorPath()` and `BuildActVersion.getActorPath()`.

### Changed

- Updated to Apify OpenAPI specification `v2-2026-11-04T151257Z`.
- `PaginationList.getCount()` of a dataset items page is the number of rows scanned, or the items returned if larger; offset iteration advances by it, and its `limit` counts rows.
- `createItemsPublicUrl` and `createKeysPublicUrl` throw `IllegalArgumentException` before any request when `expiresInSecs` is not positive or exceeds 4611686018427387.
- Documented that `DatasetClient.iterateItems(...)` without `chunkSize` loads the whole dataset in one response.
- Documented the `.actor/`-relative Actor definition paths, the `ActorStandby` fields `tenancy`, `isConsoleAuthEnabled` and `isTokenlessEnabled`, and that a `TARBALL` source is a zip archive.

### Deprecated

- `PaginationList.getScannedCount()` and `setScannedCount(...)`, replaced by `getCount()`.

## [0.12.0] - 2026-10-30

### Added
Expand Down
39 changes: 26 additions & 13 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.12.0</version>
<version>0.13.0</version>
</dependency>
```

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

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

Expand Down Expand Up @@ -285,25 +285,34 @@ exponential backoff and jitter on `429`, `5xx` and network errors.

#### `HttpTransport` contract

A custom implementation (`com.apify.client.http.HttpTransport`) must provide both methods, both
non-blocking. `HttpRequest` and `HttpResponse<T>` below are the JDK's `java.net.http.HttpRequest`/
A custom implementation (`com.apify.client.http.HttpTransport`) must provide the two methods below,
both non-blocking. `HttpRequest` and `HttpResponse<T>` below are the JDK's `java.net.http.HttpRequest`/
`java.net.http.HttpResponse<T>` (not custom client types) — same as `Duration` above:

| Method | Signature | Contract |
|---|---|---|
| `sendAsync` | `CompletableFuture<HttpResponse<byte[]>> sendAsync(HttpRequest request)` | Sends the single, fully-prepared request (auth, `User-Agent` and any other headers are already set by the client) and completes the future with the response with its body fully buffered as `byte[]`. Used for every non-streaming call. |
| `sendStreamingAsync` | `CompletableFuture<HttpResponse<InputStream>> sendStreamingAsync(HttpRequest request)` | Sends the request and completes the future with the response body as a live `InputStream` for incremental consumption, used by log streaming (`LogClient.stream(...)`) and dataset export streaming (`DatasetClient.streamItems(...)`). The *caller* (the client) is responsible for closing the returned stream; an implementation must not close it itself before completing the future. |

Both methods perform exactly one network round-trip each — no retrying, no request mutation
(retries, auth, and the `User-Agent` header are handled one layer up, by the client itself), and
neither may block the calling thread waiting on the network. A non-2xx HTTP status is **not** a
Two further methods, `sendAsync(HttpRequest, boolean followRedirects)` and
`sendStreamingAsync(HttpRequest, boolean followRedirects)`, have default implementations that send
the request through the methods above when `followRedirects` is `true` and **fail** the future with an
`UnsupportedOperationException` when it is `false`. A request to an Actor's server (see
[Server Actors](docs/actors.md#server-actors)) is sent with `followRedirects` set to `false`, so
that the API token never follows a redirect to another host. Override both to support it: with
`false`, return a redirect response as it is. `DefaultHttpTransport` does, also when it wraps your own
JDK `HttpClient` (it then copies the client's settings into one that does not follow redirects).

Every one of the four methods performs exactly one network round-trip — no retrying, no request
mutation (retries, auth, and the `User-Agent` header are handled one layer up, by the client
itself), and none may block the calling thread waiting on the network. A non-2xx HTTP status is **not** a
transport-level error: complete the future normally with it, as a normal `HttpResponse` carrying its
actual status code. Only fail the future for a genuine transport failure — connection refused, DNS
resolution failure, or a timeout. Any exception the future is completed exceptionally with is
translated by the client into an `ApifyTransportException`; complete exceptionally with
`com.apify.client.http.HttpTimeoutException` specifically for a timeout so
`ApifyTransportException.isTimeout()` reports it correctly. `DefaultHttpTransport` implements both
methods directly on top of the JDK `HttpClient`'s own `sendAsync`.
`ApifyTransportException.isTimeout()` reports it correctly. `DefaultHttpTransport` implements all
four directly on top of the JDK `HttpClient`'s own `sendAsync`.

## Fetching single resources

Expand Down Expand Up @@ -337,7 +346,7 @@ try {
## Error handling

Every exception this client throws for a request/transport failure is an unchecked
`com.apify.client.http.ApifyClientException`. It has two concrete subtypes, both also in
`com.apify.client.http.ApifyClientException`. It has three concrete subtypes, all also in
`com.apify.client.http`. Since every network-calling method is asynchronous (see
[Asynchronous API](#asynchronous-api) above), these exceptions arrive as the returned
`CompletableFuture`'s exceptional completion rather than a direct `throw`; `.join()` re-throws the
Expand All @@ -362,7 +371,11 @@ to it without unwrapping at all:
`DefaultHttpTransport` catches internally and translates into this one) — use an explicit,
fully-qualified import or a clear alias if a file needs both.

Catch `ApifyClientException` to handle both failure modes uniformly, or catch a specific subtype to
- `ServerActorException` — a call to an Actor's server (see [Server Actors](docs/actors.md#server-actors))
cannot be made, since the Actor is not a Server Actor, or since the build that serves its server
interface does not exist. The API was reached, but it has nothing to send the call to.

Catch `ApifyClientException` to handle all the failure modes uniformly, or catch a specific subtype to
handle one of them differently. `ApifyApiException` is imported from `com.apify.client.http`:

A few methods validate their own preconditions synchronously, before ever building a request or
Expand Down Expand Up @@ -407,10 +420,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.12.0`).
- `Version.CLIENT_VERSION` — the semantic version of this client (`0.13.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-29T100357Z`). It is a snapshot, not a live compatibility
and checked against (`v2-2026-11-04T151257Z`). 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
19 changes: 13 additions & 6 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,11 @@ PaginationList<Build> builds = client.builds().list(new ListOptions().limit(50L)
## Pagination — `PaginationList<T>`

`list` methods return a `PaginationList<T>` page with `getTotal()`, `getOffset()`, `getLimit()`,
`getCount()`, `isDesc()` and `getItems()`. Within-storage listers (`listKeys`, `listHead`) return
`getCount()`, `isDesc()` and `getItems()`. `getCount()` is the number of entries on the page. On a
page of dataset items it is the number of dataset rows the API scanned for it, or the number of items
returned when that is larger, which filters (`clean`, `skipEmpty`) can leave above `getItems().size()`.
The next page starts at `offset + Math.min(count, limit)`; the cap matters only with `unwind`, which
can return more items than the rows scanned. Within-storage listers (`listKeys`, `listHead`) return
their own page/head containers instead.

## Iteration — `iterate` / `iterateItems` / `iterateKeys`
Expand All @@ -182,10 +186,13 @@ Each paginated collection also offers a lazy `Flow.Publisher` (`java.util.concur
the JDK's built-in Reactive Streams type) that fetches pages only as the subscriber signals demand:
`iterate(...)` on the collection clients, `DatasetClient.iterateItems(...)`, and
`KeyValueStoreClient.iterateKeys(...)` (request-queue requests use
`RequestQueueClient.paginateRequests(...)`). The options' `limit` caps the **total** number of items
yielded; `null`/unset — or a non-positive value such as `0` — means no cap, so every item is
yielded. (This differs from `list(...)`, which sends `limit=0` to the server verbatim rather than
treating it as unbounded — the iteration behavior matches the reference JS client.) The per-request
`RequestQueueClient.paginateRequests(...)`). The options' `limit` caps the **total** number of rows read,
that is the rows each page covers (`PaginationList.getCount()`, capped at the page limit that was
requested). That is the number of items yielded, unless a page's count differs from its items:
filtered or unwound dataset items, or a Store list that drops entries after counting them.
`null`/unset — or a non-positive value such as `0` — means no cap, so every item is yielded. (This
differs from `list(...)`, which sends `limit=0` to the server verbatim rather than treating it as
unbounded — the iteration behavior matches the reference JS client.) The per-request
page size is an optional trailing `chunkSize` argument: the per-resource tables below show the
`chunkSize` form, and each publisher also has an overload that omits it (using the server's default
page size). The page size does not change which items a collection publisher yields; note the one
Expand All @@ -203,7 +210,7 @@ List<Actor> actors = Publishers.collect(client.actors().iterate(new ActorListOpt

## Resource pages

- [Actors, versions & environment variables](actors.md)
- [Actors, versions, environment variables & Server Actors](actors.md)
- [Builds](builds.md)
- [Runs](runs.md)
- [Storages (datasets, key-value stores, request queues)](storages.md)
Expand Down
89 changes: 88 additions & 1 deletion docs/actors.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ Actor created = client.actors().create(Map.of(
| `builds()` / `runs()` / `versions()` | Nested collection clients. |
| `webhooks()` | Read-only nested webhook collection (`NestedWebhookCollectionClient`, `list` + `iterate`, no `create`). |
| `version(String)` | An `ActorVersionClient`. |
| `server()` | The `ActorServerClient` for the Actor's web server (a Server Actor, formerly Actor Standby); the same instance on every call. See [Server Actors](#server-actors). |
| `standby()` | Deprecated alias of `server()` that logs a warning; will be removed in a future major version. |

`ActorStartOptions` fields (all optional): `build(String)` (the tag or number of the build to run,
e.g. `latest`, `0.1.2`), `memoryMbytes(Long)` (memory in megabytes allocated for the run),
Expand Down Expand Up @@ -104,12 +106,97 @@ System.out.println(run.getStatus());
(`ActorDefaultRunOptions` — `getBuild()`, `getTimeoutSecs()`/`getMemoryMbytes()` as `Long`,
`getRestartOnError()` as `Boolean`), `getTaggedBuilds()` (`JsonNode`; dynamic tag-name keys),
`getIsDeprecated()` (`Boolean`), `getDeploymentKey()`, `getSeoTitle()`, `getSeoDescription()`,
`getCategories()` (`List<String>`), `getActorStandby()` (`ActorStandby`, from
`getCategories()` (`List<String>`), `getStandbyUrl()` (the URL of the Actor's server, `null` unless
Standby mode is enabled), `getActorStandby()` (`ActorStandby`, from
`com.apify.client.actor`, nullable — see [Tasks](tasks.md) for its field list; only on `Actor` does
it additionally expose
`getIsEnabled()`), `getActorPermissionLevel()` (e.g. `"OWNER"`), plus `getExtra()` for any
unmodelled fields.

## Server Actors

A [Server Actor](https://docs.apify.com/platform/actors/running/standby) (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. `client.actor(id).server()` returns an `ActorServerClient` that sends
requests to the Actor's server with your token, and returns the server's URL and OpenAPI
specification. The platform and the API still use the name Actor Standby, for example in the
Actor's `standbyUrl` and `actorStandby` fields.

| Method | Description |
|---|---|
| `request(String path, ActorServerRequestOptions)` | Send an HTTP request to the Actor's server. Completes with an `ApiResponse` (`statusCode()`, `headers()`, the buffered `body()` bytes, and the helpers `bodyAsString()` and `bodyAs(Class<T>)` that parse JSON). It completes with **any** response of the server, error statuses included, so check `statusCode()`. `request(String path)` sends a `GET`, `request()` a `GET` to the root. |
| `requestStream(String path, ActorServerRequestOptions)` | As `request`, but completes as soon as the headers arrive with an `ActorServerStreamResponse` (`statusCode()`, `headers()`, an unread `body()` `InputStream`). Close it (it is `AutoCloseable`) when done. A `429`/`503` is retried like for `request`. |
| `getUrl()` / `getUrl(Timeout)` | The URL of the Actor's server (its `standbyUrl`), fetched once and cached on the client. Use it to call the server with another HTTP client. Completes with `String`. The `Timeout` (`Timeout.SHORT` by default) is for the API request that fetches it. |
| `getOpenApiSpec()` / `getOpenApiSpec(Timeout)` | The Actor's web server OpenAPI specification, from the build that serves the server (`actorStandby.build`: a tag or a build number, `latest` by default). Completes with `Optional<JsonNode>` (empty when the Actor's definition has no web server schema). The `Timeout` (`Timeout.SHORT` by default) is for each API request it makes. |

`ActorServerRequestOptions` fields (all optional): `method(String)` (`GET` by default; also `HEAD`,
`POST`, `PUT`, `DELETE`, `OPTIONS`, `TRACE`, `PATCH`), `param(String, Object)`/`params(Map)` (query
parameters, converted as for API calls: `null` is left out, a boolean is sent as `1`/`0`, a
collection comma-joined), `header(String, String)`/`headers(Map)`, `body(Object)` (a `byte[]` or
`String` is sent as it is; anything else is serialized to JSON and sent as `application/json`
unless you set a `Content-Type` header), `compression(HttpCompressor)` and `timeout(Timeout)`
(`Timeout.LONG` by default, since the first request to an idle Actor waits for the platform to
start a server run).

```java
ActorServerClient server = client.actor("username/actor-name").server();

// Send a GET request to the /search endpoint of the Actor's server.
// The future completes with error responses too, so check the status first.
ApiResponse search =
server.request("/search", new ActorServerRequestOptions().param("q", "apify")).join();
if (search.statusCode() != 200) {
throw new IllegalStateException("Search failed with status " + search.statusCode());
}
System.out.println(search.bodyAsString());

// Send a POST request with a JSON body.
ApiResponse created =
server
.request(
"/items",
new ActorServerRequestOptions().method("POST").body(Map.of("url", "https://apify.com")))
.join();
System.out.println(created.statusCode());
```

```java
ActorServerClient server = client.actor("username/actor-name").server();

// Get the URL of the Actor's server.
System.out.println(server.getUrl().join());

// Get the OpenAPI specification and list the server's endpoints.
Optional<JsonNode> spec = server.getOpenApiSpec().join();
spec.ifPresent(openApi -> openApi.get("paths").propertyNames().forEach(System.out::println));
```

How the request is sent, so that the token and the Actor's code stay apart:

- The token travels in the `X-Apify-Authorization` header, which the platform consumes before the
request reaches the Actor, so the `Authorization` header stays free for the Actor's own
authentication. Headers you pass, `Authorization` included, reach the server as they are.
- The `path` is always resolved against the server URL, so the request, and the token with it,
cannot be sent to another host. A fragment (`#...`) is dropped, since it is never sent.
- A `429` or `503` response, of `requestStream` too, is retried with the client's retry settings (the Actor's code can return
these statuses too, and then it gets the request again), and the last one is returned once the
retries run out. Other statuses are returned without a retry. A transport error, a timeout
included, is not retried, since the Actor may have handled the request before it failed.
- A redirect is returned as it is, since following it would send the token to a host the Actor's
code picked. A custom `HttpTransport` has to support that (see the
[transport contract](../README.md#httptransport-contract)): a transport that does not fails such a
request instead of risking the token.
- The body is sent uncompressed unless you set `compression(...)`, since the Actor's server may not
decode a compressed body.
- An Actor that is not a Server Actor fails the call with a `ServerActorException`, and an Actor
that does not exist with a `NotFoundError`.

`Actor` versions with `SOURCE_FILES` or `TARBALL` sources can set an `actorPath`, the Actor's
directory inside the uploaded source (for example `actors/typescript-actor` in a monorepo), when
you create or update them; `ActorVersion.getActorPath()` and `BuildActVersion.getActorPath()` read
it. An update that sends `sourceType`, `sourceFiles` or `tarballUrl` without `actorPath` removes the
stored one, so send it again with every upload.

## `ActorVersionClient` and `ActorVersionCollectionClient`

`client.actor(id).versions()` lists/creates versions; `client.actor(id).version(v)` reads, updates
Expand Down
2 changes: 1 addition & 1 deletion docs/misc.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Browse public Actors in the Apify Store. `client.store()` returns a `StoreCollec
| Method | Description |
|---|---|
| `list(StoreListOptions)` | A page of Store Actors. Completes with `PaginationList<ActorStoreListItem>`. |
| `iterate(StoreListOptions, Long chunkSize)` | A lazy `Flow.Publisher<ActorStoreListItem>` over all matches, fetching pages on demand. The options' `limit` caps the total number of Actors yielded (`null`/unset or non-positive = all); `chunkSize` is the per-request page size (`null` = server default). |
| `iterate(StoreListOptions, Long chunkSize)` | A lazy `Flow.Publisher<ActorStoreListItem>` over all matches, fetching pages on demand. The options' `limit` caps the total number of rows read, which are the Actors yielded unless the API drops entries after counting them (`null`/unset or non-positive = all); `chunkSize` is the per-request page size (`null` = server default). |

`StoreListOptions` fields: `offset` (number of Actors to skip), `limit` (maximum number of Actors to
return), `search` (full-text search query), `sortBy` (the sort field, e.g. `popularity`, `newest`),
Expand Down
Loading
Loading