Skip to content

feat: add Server Actor support (formerly Actor Standby) - #1179

Open
vdusek wants to merge 12 commits into
masterfrom
feat/actor-server
Open

vdusek wants to merge 12 commits into
masterfrom
feat/actor-server

Conversation

@vdusek

@vdusek vdusek commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Adds ActorClient.server(), which returns an ActorServerClient for Server Actors (formerly Actor Standby):

const actorServerClient = client.actor('username/actor-name').server();
const response = await actorServerClient.request('/search', { params: { q: 'apify' } });
const url = await actorServerClient.getUrl();
const spec = await actorServerClient.getOpenApiSpec();
  • request() sends a request to the Actor's server. The host always comes from the Actor's standbyUrl. The token goes in the X-Apify-Authorization header, which the platform strips, so headers the caller passes, Authorization included, reach the Actor as they are.
  • request() returns the server's response whatever its status. Only 429 and 503 are retried, since other statuses may come from the Actor's code. Network errors and timeouts aren't retried, since the Actor may have already handled the request. It throws only when it gets no response: the Actor doesn't exist or isn't a Server Actor, or the request fails or times out.
  • request() sends bodies uncompressed unless compression is set, returns the body parsed, as a buffer, or as a stream depending on responseType, and defaults to the long timeout to cover a cold start.
  • getUrl() returns the server URL. It's fetched once per ActorClient, by getUrl() or getOpenApiSpec().
  • getOpenApiSpec() returns the web server schema from the build that serves the server (actorStandby.build, or latest), or undefined if there's none.

ActorClient.standby() is a deprecated alias of server() and will be removed in v4.

Other changes:

  • ApifyRequestConfig takes new per-request options: compression, authorizationHeader, throwOnErrorStatus, retryStatusCodes, and retryTransportErrors. A custom HTTP client that overrides call() should honor them.
  • AxiosHttpClient drops the X-Apify-Authorization header on a redirect to another origin, so an Actor can't redirect the token to another host.
  • A Server Actors concepts page, unit tests, and an integration test against a temporary echo Server Actor.

Port of apify/apify-client-python#1153.

Closes: #1159

✍️ Drafted by Claude Code

@vdusek vdusek added the t-tooling Issues with this label are in the ownership of the tooling team. label Oct 29, 2026
@vdusek vdusek self-assigned this Oct 29, 2026
@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

⚠️ There are broken links in the documentation.

See more at https://github.com/apify/apify-client-js/actions/runs/41030185240#summary-123977815884

@vdusek
vdusek requested review from barjin and janbuchar October 29, 2026 09:40
@vdusek
vdusek marked this pull request as ready for review October 29, 2026 09:40
@vdusek
vdusek requested a review from szaganek as a code owner October 29, 2026 09:40
@apify-service-account apify-service-account added the tested Temporary label used only programatically for some analytics. label Oct 29, 2026

@barjin barjin left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @vdusek !

I checked mostly the docs and marked some discussion points ⬇️ I also briefly skimmed the CLI RFC, but couldn't find the relevant points for these (I might have missed these, though).

Comment thread docs/02_concepts/14_server_actors.mdx Outdated

The request carries the client's API token in the `Authorization` header. The platform removes the token before it forwards the request, so the token doesn't reach the Actor's code.

If the Actor reads the `Authorization` header for its own authentication, pass that header in `headers`, and send your Apify token in the `X-Apify-Authorization` header instead. A header you pass in `headers` replaces the client's `Authorization` header for that request.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why don't we do this by default (send the token as X-Apify-Authorization with every standby request)?

Imo we shouldn't limit the set of allowed user-based headers only because some names are used internally.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, so:

  • we always use X-Apify-Authorization, platforms authenticates with it, and strips it (it doesnt reach the Actor's code)
  • Authorization header is free for the Actor's own auth and is passed through unchanged

Comment thread docs/02_concepts/14_server_actors.mdx Outdated

## Timeouts, retries, and errors

When no server run is ready, the first request waits for the platform to start one, which can take a few minutes. The method uses the `long` timeout tier by default for that reason. You can pass a different `timeoutSecs`, or a `signal` to abort the request, the same way as for API calls. For details, see [Timeouts](./10_timeouts.mdx).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

first request waits for the platform to start one, which can take a few minutes

...few minutes? I guess we should have kept the "stand-by" naming 😅

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👀 ... dropping all the time-scoped information 😄

Comment thread docs/02_concepts/14_server_actors.mdx Outdated

When no server run is ready, the first request waits for the platform to start one, which can take a few minutes. The method uses the `long` timeout tier by default for that reason. You can pass a different `timeoutSecs`, or a `signal` to abort the request, the same way as for API calls. For details, see [Timeouts](./10_timeouts.mdx).

An error status from the Actor's server throws an `ApifyApiError`, like an error from the API. Server errors and rate-limit responses are retried with the client's retry settings, so a request that the server failed to answer may reach it more than once. If your endpoint isn't safe to call twice, lower `maxRetries` on the client. For details, see [Retries](./04_retries.mdx) and [Error handling](./03_error_handling.mdx).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I'm sold on this... we don't know anything about the requests / responses of the Actor Server. If it's the standby controller speaking (e.g., rate-limiting the request before it reaches the Actor itself), there it might make sense.

But in case of the Actor Server returning a non-2xx response, I'd rather just return that from the .request() call, not touching it at all.

@vdusek vdusek Oct 30, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, updated - we now just return the response as is (and only retry 429 and 503)

Comment thread docs/02_concepts/14_server_actors.mdx Outdated
{ServerExample}
</CodeBlock>

The method resolves to the server's response, with the body parsed by its content type in `data`. To find out which endpoints an Actor serves, see its page in [Apify Store](https://apify.com/store), its README, or its OpenAPI specification. For details, see [Server URL and OpenAPI specification](#server-url-and-openapi-specification).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

with the body parsed by its content type in data.

idk how to feel about this. I suppose this means that Content-Type: application/json gets parsed into a JS object?

Is there a way to opt-out of this? Or, e.g., stream the response, or get it as raw bytes etc etc.?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could add an option to specify a response type. Check it out now.

@vdusek
vdusek requested a review from barjin October 30, 2026 08:28

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

t-tooling Issues with this label are in the ownership of the tooling team. tested Temporary label used only programatically for some analytics.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add support for calling Standby Actors

3 participants