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/dotnet-integration-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,11 @@ jobs:

# Offline unit tests (mock transport): prove the retry/error/signature logic without the API. Every test step
# sets TreatNoTestsAsError, so a filter that matches no tests fails the workflow instead of passing.
# The tests run in a non-UTC time zone, so that a date converted with the machine's local time zone shows
# up as a failure instead of passing by accident on the UTC runner.
- name: Unit tests
env:
TZ: Asia/Tokyo
run: dotnet test --configuration Release --no-build --filter Category=Unit -- RunConfiguration.TreatNoTestsAsError=true

# Fail fast if the integration-test secret is missing or empty. Without this guard the
Expand Down
6 changes: 5 additions & 1 deletion .github/workflows/dotnet-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,12 @@ jobs:
- name: Build (warnings as errors)
run: dotnet build --configuration Release --no-restore

# The same settings as the unit-test step of the CI workflow: a non-UTC time zone, and a filter that matches
# no tests fails the release gate instead of passing it.
- name: Unit tests
run: dotnet test --configuration Release --no-build --filter Category=Unit
env:
TZ: Asia/Tokyo
run: dotnet test --configuration Release --no-build --filter Category=Unit -- RunConfiguration.TreatNoTestsAsError=true

- name: Resolve version from csproj
id: version
Expand Down
31 changes: 31 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,36 @@
# Changelog

## 0.9.0

- Added `ActorClient.Server()`, which returns an `ActorServerClient` for Server Actors (formerly Actor
Standby): `RequestAsync` sends a request to the Actor's server (token in `X-Apify-Authorization`, any
response returned, only 429 and 503 retried, no redirects followed), `GetUrlAsync` returns the server URL
and `GetOpenApiSpecAsync` its OpenAPI specification. New `ActorServerRequestOptions`,
`ActorServerResponse` and `ActorServerException`. `Content-*` headers of a request reach the server with the
request body.
- Requests that must not follow redirects set `ApifyRequestOptions.FollowRedirects` to `false`; a custom
`IHttpTransport` has to honor it. `HttpClientTransport` takes an optional second `HttpClient` with
automatic redirects disabled for them (it creates one when it creates its own `HttpClient`).
- **Breaking:** `PaginationList<T>.Count` is now the number of entries the API went through for the page, as
`count` in the reference client, instead of the number of items. On a page from
`DatasetClient.ListItemsAsync` it is the larger of the rows the API scanned for it
(`X-Apify-Pagination-Count`) and the items returned, so filters such as `SkipEmpty` or `Clean` can leave it
above `Items.Count`. On any other list it is the `count` the API reports, which
exceeds `Items.Count` only where the API drops entries after counting them, as the Store listing does. Use
`Items.Count` for the number of items: the list's own count (`IReadOnlyCollection<T>.Count`, LINQ's
`Count()`), enumeration and indexer still cover the items. Offset iteration steps by `Count`, capped at
the requested limit.
- `CreateItemsPublicUrlAsync` and `CreateKeysPublicUrlAsync` throw `ArgumentOutOfRangeException` before any
request for a zero or negative `expiresInSecs`, which used to produce a signed URL that had already expired.
Leave it out for a URL that never expires.
- Documented the page size used when iterating dataset items without `ChunkSize` (the whole dataset in one
response).
- Added `Actor.StandbyUrl`/`ActorStandby`/`TaggedBuilds`, `Build.ActorDefinition` and
`ActorVersion.ActorPath` (the Actor's directory inside an uploaded `SOURCE_FILES` or `TARBALL` source).
- Bumped `ApifyClientVersion.ApiSpecVersion` to the Apify OpenAPI spec `v2-2026-11-04T151257Z` and the
project version to `0.9.0`; regenerated the embedded response schemas (`actorPath` on versions and
builds).

## 0.8.0

- `PushItemsAsync` no longer retries a request that timed out, because the API may already have stored the
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ var run = await client.Actor("apify/hello-world").CallAsync();

// Read items from the run's default dataset.
var items = await client.Dataset(run.DefaultDatasetId!).ListItemsAsync();
// Count is the number of items in THIS page; Total is the dataset's full count across all pages.
Console.WriteLine($"Items on this page: {items.Count} (of {items.Total} total)");
// Items.Count is the number of items in THIS page; Total is the dataset's full count across all pages.
Console.WriteLine($"Items on this page: {items.Items.Count} (of {items.Total} total)");
```

`new ApifyClient("my-api-token")` takes the token as an explicit argument — it does **not** read
Expand Down
22 changes: 16 additions & 6 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ signatures on the pages below for brevity). Method names mirror the reference cl

## Contents

- [Actors](actors.md) — create, run, build, validate input, versions and environment variables.
- [Actors](actors.md) — create, run, build, validate input, Server Actors, versions and environment variables.
- [Builds](builds.md) — fetch, wait, abort, logs, OpenAPI definition.
- [Runs](runs.md) — get/wait, abort, metamorph, reboot, resurrect, charge, storages, logs.
- [Storages](storages.md) — datasets, key-value stores, request queues.
Expand Down Expand Up @@ -51,8 +51,8 @@ var run = await client.Actor("apify/hello-world").CallAsync();

// Read items from the run's default dataset.
var items = await client.Dataset(run.DefaultDatasetId!).ListItemsAsync();
// Count is the number of items in THIS page; Total is the dataset's full count across all pages.
Console.WriteLine($"Items on this page: {items.Count} (of {items.Total} total)");
// Items.Count is the number of items in THIS page; Total is the dataset's full count across all pages.
Console.WriteLine($"Items on this page: {items.Items.Count} (of {items.Total} total)");
```

`new ApifyClient("my-api-token")` takes the token as an explicit argument — it does **not** read
Expand All @@ -74,7 +74,7 @@ the `using` directives for whichever ones a file references:
| `Apify.Client.Resources` | Every resource client the entry point returns — `ActorClient`, `RunClient`, `BuildClient`, `DatasetClient`, `KeyValueStoreClient`, `RequestQueueClient`, `TaskClient`, `ScheduleClient`, `LogClient`, `UserClient`, `ActorVersionClient`, `ActorEnvVarClient`, the `…CollectionClient` types (including `ActorVersionCollectionClient`, `ActorEnvVarCollectionClient`, `NestedWebhookCollectionClient`, and `WebhookDispatchCollectionClient`), etc. |
| `Apify.Client.Models` | Data models returned by the clients — `Actor`, `ActorRun`, `Build`, `Dataset`, `RequestQueueRequest`, `ActorEnvVar`, `PaginationList<T>`, and so on. |
| `Apify.Client.Options` | The option/request objects passed into methods — `ActorStartOptions`, `ActorBuildOptions`, `ActorListOptions`, `RunListOptions`, `RunResurrectOptions`, `RunChargeOptions`, `MetamorphOptions`, `LastRunOptions`, `DatasetListItemsOptions`, `DatasetDownloadOptions`, `DownloadItemsFormat`, `GetRecordOptions`, `SetRecordOptions`, `ListKeysOptions`, `IterateKeysOptions`, `ListRequestsOptions`, `PaginateRequestsOptions`, `BatchAddRequestsOptions`, `RequestQueueClientOptions`, `TaskStartOptions`, `ValidateInputOptions`, `LogOptions`, `ListOptions`, `StorageListOptions`, `StoreListOptions`. |
| `Apify.Client.Exceptions` | `ApifyClientException` (common base), `ApifyApiException` (and its per-status subclasses), `ApifyTransportException`, `InvalidResponseBodyException` and `ResponseValidationException`. |
| `Apify.Client.Exceptions` | `ApifyClientException` (common base), `ApifyApiException` (and its per-status subclasses), `ApifyTransportException`, `InvalidResponseBodyException`, `ResponseValidationException` and `ActorServerException`. |
| `Apify.Client.Http` | The replaceable transport: `IHttpTransport` and the default `HttpClientTransport`. |
| `System.Text.Json.Nodes` (BCL) | Not an Apify namespace, but required whenever you name the JSON escape-hatch types the client returns/accepts — `JsonObject`/`JsonNode` (e.g. dataset items, `GetInputAsync`/`UpdateInputAsync`, `GetStatisticsAsync`, `GetOpenApiDefinitionAsync`, `MonthlyUsageAsync`/`LimitsAsync`, `RequestQueueRequest.UserData`). Add `using System.Text.Json.Nodes;`. |

Expand Down Expand Up @@ -203,6 +203,15 @@ var client = new ApifyClient(new ApifyClientOptions
});
```

Requests to an Actor's server (`client.Actor(id).Server()`, see [Actors](actors.md)) must not follow
redirects, because they carry the API token in a header. They set
`Apify.Client.Http.ApifyRequestOptions.FollowRedirects` to `false` in `HttpRequestMessage.Options`, and a
custom `IHttpTransport` has to return a redirect response as it is for such a request. `HttpClientTransport`
does this with a second `HttpClient` that has automatic redirects disabled: it creates one itself, but a
transport wrapping your own `HttpClient` needs one too, passed as the second constructor argument
(`new HttpClientTransport(httpClient, noRedirectHttpClient)`), otherwise such a request throws
`InvalidOperationException`.

## Error handling

Methods that fetch a single resource return `null` when the resource does not exist (rather than
Expand Down Expand Up @@ -248,8 +257,9 @@ not break the client. The response arrived in full, so neither is
retried. Both expose `StatusCode`, `HttpMethod`, `Path` and the raw `Body`. An error status with an
unparsable body throws the `ApifyApiException` for its status code, with the raw body in its message.

`ApifyApiException`, `ApifyTransportException`, `InvalidResponseBodyException` and
`ResponseValidationException` extend the common
`ApifyApiException`, `ApifyTransportException`, `InvalidResponseBodyException`,
`ResponseValidationException` and `ActorServerException` (an Actor that is not a Server Actor, see
[Actors](actors.md)) extend the common
`ApifyClientException`, so `catch (ApifyClientException)` catches every error this client raises for a
failed call without naming the concrete types:

Expand Down
75 changes: 75 additions & 0 deletions docs/actors.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ without re-encoding it as JSON.
- `Builds()` → `BuildCollectionClient`; `Runs()` → `RunCollectionClient`.
- `Version(string versionNumber)` → `ActorVersionClient`; `Versions()` → `ActorVersionCollectionClient` — a single Actor version and the version collection.
- `Webhooks()` → read-only `NestedWebhookCollectionClient`.
- `Server()` → `ActorServerClient` — the Actor's server interface, see [Server Actors](#server-actors--clientactoridserver).

`ActorStartOptions` fields:

Expand Down Expand Up @@ -95,6 +96,80 @@ against) and `ContentType` (`string?`, content type of the input; defaults to `a
`LastRunOptions` fields: `Status` (`string?`, only consider the last run with this status, e.g.
`SUCCEEDED`) and `Origin` (`string?`, only consider the last run started from this origin, e.g. `API`).

## Server Actors — `client.Actor(id).Server()`

A Server Actor (formerly Actor Standby) runs a web server that is called over HTTP without starting a run
for each call. `Server()` returns an `ActorServerClient`; every call returns the same client, so the server
URL it fetches is reused.

- `RequestAsync(string path = "/", ActorServerRequestOptions? options = null)` → `ActorServerResponse` —
sends a request to the Actor's server and returns its response, whatever its status.
- `GetUrlAsync()` → `string` — the server URL (the Actor's `standbyUrl`), to call the server with another
HTTP client. Fetched from the API on the first call of `GetUrlAsync`, `GetOpenApiSpecAsync` or
`RequestAsync`, then reused.
- `GetOpenApiSpecAsync()` → `JsonObject?` — the web server schema (OpenAPI specification) of the build that
serves the server: `actorStandby.build` (a tag or a build number) or `latest`; `null` if the definition has
none.
- `WithTimeout(TimeSpan timeout)` — the timeout of the API calls made by `GetUrlAsync` and
`GetOpenApiSpecAsync`.

They throw `ActorServerException` when the Actor is not a Server Actor (or the serving build does not exist),
and the usual `ApifyApiException` when it does not exist. `RequestAsync` also throws
`InvalidOperationException` when the client's `HttpClientTransport` wraps your own `HttpClient` without a
second one that does not follow redirects (see
[Replaceable HTTP transport](README.md#replaceable-http-transport)).

`RequestAsync` details:

- The request goes to the server URL, and `path` is always resolved against it (a fragment is dropped), so
neither the request nor the token can reach another host.
- The token travels in the `X-Apify-Authorization` header, which the platform removes before the request
reaches the Actor's code. Headers you pass, `Authorization` included, reach the Actor as they are.
- Any response is returned, error statuses included. Only 429 and 503 are retried (with the client's retry
settings, returning the last response when they run out), because other statuses can come from the Actor's
code. Network errors and timeouts are not retried, since the Actor may have already handled the request, and
a redirect is returned as it is instead of followed.
- Bodies are sent uncompressed unless `Compressor` is set. The timeout defaults to the `Long` tier, which
covers the cold start of an idle Actor.

`ActorServerRequestOptions` fields:

| Field | Type | Description |
|---|---|---|
| `Method` | `HttpMethod?` | HTTP method. Defaults to `GET`. |
| `Query` | `IReadOnlyDictionary<string, object?>?` | Query parameters; `null` values are left out, booleans become `1`/`0`, dates ISO 8601 in UTC (a `DateTime` of unspecified kind is taken as UTC) and lists their comma-joined values. |
| `Headers` | `IReadOnlyDictionary<string, string>?` | Additional request headers. A `Content-Type` header equals `ContentType`; a `Content-Encoding` header marks a body that is already encoded and is then not compressed; other `Content-*` headers (e.g. `Content-Disposition`) are sent only with a `Body`. |
| `Body` | `object?` | Request body: `byte[]` and `string` are sent as they are, anything else is serialized to JSON. |
| `ContentType` | `string?` | Content type of the body; defaults to `application/json`, `text/plain; charset=utf-8` for a string and `application/octet-stream` for bytes. |
| `Compressor` | `HttpCompressor?` | Compressor of the body (e.g. `new GzipHttpCompressor()`). Set it only if the Actor's server decodes the resulting `Content-Encoding`. |
| `StreamResponse` | `bool` | Return the response body unread, to read it with `ReadAsStreamAsync`. The response holds its connection until disposed. |
| `Timeout` | `TimeSpan?` | Timeout of the request; defaults to the `Long` tier. |

`ActorServerResponse` is `IDisposable` (dispose it when done) and has `StatusCode`, `IsSuccess`, `Headers`
(case-insensitive), `ContentType`, `ReadAsBytesAsync()`, `ReadAsStringAsync()`, `ReadAsStreamAsync()` and
`ReadAsJsonAsync()` (`JsonNode?`; throws `InvalidResponseBodyException` for a body that is not JSON).

```csharp
using System;
using System.Collections.Generic;
using Apify.Client;
using Apify.Client.Options;

var client = new ApifyClient("my-api-token");
var server = client.Actor("username/actor-name").Server();

using var response = await server.RequestAsync(
"/search",
new ActorServerRequestOptions { Query = new Dictionary<string, object?> { ["q"] = "apify" } });
if (response.IsSuccess)
{
Console.WriteLine(await response.ReadAsJsonAsync());
}

Console.WriteLine(await server.GetUrlAsync());
Console.WriteLine(await server.GetOpenApiSpecAsync());
```

## Versions and environment variables

Manage an Actor's versions with `client.Actor(id).Versions()` (the whole collection) and
Expand Down
6 changes: 3 additions & 3 deletions docs/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ var client = new ApifyClient(
// The third argument bounds the wait in seconds (120 here); pass null to wait indefinitely.
var run = await client.Actor("apify/hello-world").CallAsync(null, null, 120);
var items = await client.Dataset(run.DefaultDatasetId!).ListItemsAsync(new DatasetListItemsOptions());
// Count is the number of items in THIS page; Total is the dataset's full count across all pages.
Console.WriteLine($"Items on this page: {items.Count} (of {items.Total} total)");
// Items.Count is the number of items in THIS page; Total is the dataset's full count across all pages.
Console.WriteLine($"Items on this page: {items.Items.Count} (of {items.Total} total)");
```

## Each storage: create, push, read
Expand All @@ -40,7 +40,7 @@ Console.WriteLine($"Items on this page: {items.Count} (of {items.Total} total)")
var dataset = await client.Datasets().GetOrCreateAsync("example-ds");
await client.Dataset(dataset.Id!).PushItemsAsync(new[] { new { hello = "world" } });
var items = await client.Dataset(dataset.Id!).ListItemsAsync(new DatasetListItemsOptions());
Console.WriteLine("Dataset items: " + items.Count);
Console.WriteLine("Dataset items: " + items.Items.Count);

// Key-value store
var store = await client.KeyValueStores().GetOrCreateAsync("example-kvs");
Expand Down
7 changes: 6 additions & 1 deletion docs/models.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ Base class for every JSON-backed model below.
| `IsPublic` | `bool?` | Whether the Actor is published publicly in the Apify Store. |
| `CreatedAt` | `string?` | ISO 8601 creation timestamp. |
| `ModifiedAt` | `string?` | ISO 8601 last-modification timestamp. |
| `StandbyUrl` | `string?` | URL of the Actor's server when it is a Server Actor (see [`ActorServerClient`](actors.md#server-actors--clientactoridserver)); `null` otherwise. |
| `ActorStandby` | `JsonObject?` | The Server Actor settings: `isEnabled`, `build`, `memoryMbytes`, `idleTimeoutSecs`, `tenancy` (`SINGLE_TENANT` or `MULTI_TENANT`) and others. |
| `TaggedBuilds` | `JsonObject?` | The builds the Actor's tags point to, keyed by tag (e.g. `latest`); each value has `buildId` and `buildNumber`. |

## `ActorRun`

Expand Down Expand Up @@ -78,6 +81,7 @@ Base class for every JSON-backed model below.
| `ExitCode` | `long?` | Exit code of the build process (`0` on success); absent while building. |
| `GitBranchName` | `string?` | Git branch the build's source was taken from, for Git-based sources. |
| `GitCommitId` | `string?` | Git commit the build's source was taken from, for Git-based sources. |
| `ActorDefinition` | `JsonObject?` | The Actor definition the build was made from, including its `webServerSchema`. |
| `IsTerminal` | `bool` | `true` if `Status` is a terminal state. |

## `ActorVersion`
Expand All @@ -86,6 +90,7 @@ Base class for every JSON-backed model below.
|---|---|---|
| `VersionNumber` | `string?` | The version's number (e.g. `0.1`). |
| `SourceType` | `string?` | How the source is provided (e.g. `SOURCE_FILES`, `GIT_REPO`, `TARBALL`, `GITHUB_GIST`). |
| `ActorPath` | `string?` | The Actor's directory inside the uploaded source of a `SOURCE_FILES` or `TARBALL` version (relative to the upload root, `/` separators, e.g. `actors/typescript-actor`); `null` when the upload root is the Actor's directory. A `TARBALL` source is a zip archive. |

## `ActorEnvVar`

Expand Down Expand Up @@ -351,7 +356,7 @@ One page of a paginated listing, returned by every `ListAsync` method.
| Property | Type | Description |
|---|---|---|
| `Items` | `IReadOnlyList<T>` | The items on this page. |
| `Count` | `long` | Number of items on this page (equals `Items.Count`). |
| `Count` | `long` | Number of entries the API went through for this page. On a page of dataset items, the larger of the dataset rows it scanned and the items returned, so filters such as `SkipEmpty` or `Clean` can leave it above `Items.Count`; the next page starts at `Offset + Math.Min(Count, Limit)`. On any other page, the `count` the API reports, which is the number of items except where the API drops entries after counting them (the Store listing). Use `Items.Count` for the number of items: the list's own count (`IReadOnlyCollection<T>.Count`, LINQ's `Count()`), enumeration and indexer cover the items only. |
| `Total` | `long` | Total number of items across all pages. |
| `Offset` | `long` | The offset this page started at. |
| `Limit` | `long` | The page-size limit that was applied. |
Expand Down
Loading
Loading