Skip to content

Commit 548aeb2

Browse files
committed
fix: Serve standby Actors at the platform-shaped *.localhost URL
The standbyUrl is now http://<username>--<actor-name>.localhost:3333, where the Actor owns `/` of its own origin as on *.apify.actor, so an Actor's web UI with root-relative links works. The path form stays for clients that do not resolve *.localhost, and the Actor object read from an Actor container carries the http://apify-api:3333/actor-runtime/standby/... form it can reach. Adds sample_actor_standby_web and e2e tests for an external client and an Actor-to-Actor call. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013KAxkpoSLVQvpEJUANjWdp
1 parent 2a22c3f commit 548aeb2

21 files changed

Lines changed: 2745 additions & 38 deletions

File tree

‎README.md‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,11 +80,18 @@ through the API - is served over HTTP at its `standbyUrl`, on the API port:
8080
```bash
8181
cd sample_actor_standby_ts # or sample_actor_standby_py
8282
apify push
83-
curl "http://localhost:3333/actor-runtime/standby/<username>--my-standby-actor-ts/hello?name=Ada&token=<token>"
83+
curl "http://<username>--my-standby-actor-ts.localhost:3333/hello?name=Ada&token=<token>"
8484
```
8585

86+
The `standbyUrl` has the platform's shape, one `*.localhost` hostname per Actor, so a web UI served by the
87+
Actor works as on `*.apify.actor`. Clients that do not resolve `*.localhost` use
88+
`http://localhost:3333/actor-runtime/standby/<username>--<actor-name>`, and other Actors
89+
`http://apify-api:3333/actor-runtime/standby/<username>--<actor-name>`.
90+
8691
The two samples are the same server in TypeScript and Python - JSON endpoints, a request body echo, a
8792
Server-Sent Events stream, a websocket and stats kept across runs; each README lists the calls.
93+
`sample_actor_standby_web` serves a web page with root-relative links, and in an ordinary run calls a
94+
standby Actor from inside its container.
8895

8996
Requests are handed to standby runs the runtime starts, scales by `desiredRequestsPerActorRun` /
9097
`maxRequestsPerActorRun` and winds down after `idleTimeoutSecs` without a request, as on the platform.

‎requirements/actor-driver.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,8 @@ start`, ...) is refused by name, naming both the `CMD` fix and how to clear debu
279279
- `ACTOR_STANDBY_PORT` / `ACTOR_WEB_SERVER_PORT` — the port an Actor server listens on, both set to the same
280280
value (the JavaScript SDK reads the first, the Python SDK the second): a version-level
281281
`ACTOR_WEB_SERVER_PORT` or `ACTOR_STANDBY_PORT`, else `4321`, as on the platform.
282-
- `ACTOR_STANDBY_URL` — the Actor's standby URL (`api.md`).
282+
- `ACTOR_STANDBY_URL` — the Actor's host-facing standby URL, `http://<username>--<actor-name>.localhost:3333`
283+
(`api.md`).
283284
- `APIFY_API_BASE_URL` — the runtime's own API, reachable by name from any
284285
Actor container on the shared Docker network (see "Networking" above).
285286
- `APIFY_TOKEN` — the run owner's token

‎requirements/api.md‎

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -156,11 +156,16 @@
156156
`null` while Standby is off. `POST` and `PUT /v2/actors` accept `actorStandby`; a partial object is merged
157157
over the stored settings, over the defaults. An invalid one is `400` `invalid-request`, changing nothing;
158158
a `tenancy` other than `SINGLE_TENANT` is invalid.
159-
- The standby URL is `http://localhost:3333/actor-runtime/standby/<username>--<actor-name>`; the Actor id
160-
also works in place of `<username>--<actor-name>`. Everything after it - path, query, method, headers,
161-
body, websocket upgrades - reaches the Actor's server unchanged, the token included.
162-
- The platform's host-based shape, `http://<username>--<actor-name>.localhost:3333/<path>`, is served too,
163-
for clients that resolve `*.localhost`; its whole path reaches the Actor.
159+
- The standby URL has the platform's host-based shape, `http://<username>--<actor-name>.localhost:3333`:
160+
the Actor owns `/` of its own origin, as on `*.apify.actor`, so a web UI it serves can use root-relative
161+
links. Several standby Actors are served side by side, one hostname each.
162+
- The same Actor is also served at `http://localhost:3333/actor-runtime/standby/<username>--<actor-name>`,
163+
for clients that do not resolve `*.localhost`, and at
164+
`http://apify-api:3333/actor-runtime/standby/<username>--<actor-name>` from Actor containers. The Actor
165+
object read from an Actor container carries this container form as its `standbyUrl`.
166+
- The Actor id also works in place of `<username>--<actor-name>` in every form. Everything after the
167+
standby URL - path, query, method, headers, body, websocket upgrades - reaches the Actor's server
168+
unchanged, the token included.
164169
- Authenticated like the API (`Authorization: Bearer`, `?token=`), plus the platform's
165170
`x-apify-authorization` header; only the Actor's owner is served.
166171
- Errors, as `{ "error": { "type", "message" } }`: no token `401` `user-not-authenticated`; no such Actor of

‎requirements/test.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,7 @@ Test case must verify full Actor development flow:
5353
- Assert that plain, body-carrying, streamed and websocket requests to its standby URL are all answered by
5454
one `STANDBY` run, that the run ends `SUCCEEDED` once idle with one dataset item per greeting, and that
5555
the next request starts a new run
56+
- For `sample_actor_standby_web`: push, then assert that an external client reaches its standby endpoint at
57+
the `*.localhost` standby URL, where the Actor's page links to its API root-relative, and by the path
58+
form; and that an ordinary run of an Actor calls the standby endpoint from inside its container, by the
59+
`standbyUrl` it reads through the API
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"$schema": "https://apify.com/schemas/v1/actor.ide.json",
3+
"actorSpecification": 1,
4+
"name": "my-standby-web-actor",
5+
"title": "Standby web page sample actor for actor-runtime",
6+
"description": "In Standby mode, a web page and the JSON endpoint it calls with a root-relative URL. An ordinary run calls a standby Actor's endpoint from inside its container.",
7+
"version": "0.0",
8+
"buildTag": "latest",
9+
"usesStandbyMode": true,
10+
"input": "./input_schema.json",
11+
"dockerfile": "../Dockerfile"
12+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"title": "Standby web sample input",
3+
"type": "object",
4+
"schemaVersion": 1,
5+
"properties": {
6+
"standbyActor": {
7+
"title": "Standby Actor",
8+
"type": "string",
9+
"description": "The standby Actor an ordinary run calls, as an id or `username/name`. Empty means this Actor itself.",
10+
"editor": "textfield"
11+
},
12+
"name": {
13+
"title": "Name",
14+
"type": "string",
15+
"description": "Sent to the standby Actor's `/api/greeting` endpoint.",
16+
"editor": "textfield",
17+
"default": "Actor"
18+
}
19+
}
20+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# configurations
2+
.idea
3+
.vscode
4+
.zed
5+
6+
# crawlee and apify storage folders
7+
apify_storage
8+
crawlee_storage
9+
storage
10+
11+
# installed files
12+
node_modules
13+
14+
# git folder
15+
.git
16+
17+
# dist folder
18+
dist
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
FROM apify/actor-node:24 AS builder
2+
3+
COPY --chown=myuser:myuser package*.json ./
4+
RUN npm install --include=dev --audit=false
5+
6+
COPY --chown=myuser:myuser . ./
7+
RUN npm run build
8+
9+
FROM apify/actor-node:24
10+
11+
COPY --chown=myuser:myuser package*.json ./
12+
RUN npm --quiet set progress=false \
13+
&& npm install --omit=dev --omit=optional --audit=false \
14+
&& rm -r ~/.npm
15+
16+
COPY --from=builder --chown=myuser:myuser /usr/src/app/dist ./dist
17+
COPY --chown=myuser:myuser . ./
18+
19+
# The interpreter itself, not `npm start`, so the runtime's debug mode can attach too.
20+
CMD ["node", "dist/main.js"]

‎sample_actor_standby_web/README.md‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Standby web sample Actor
2+
3+
A small Actor for trying Actor Standby callers against the local runtime. `.actor/actor.json` sets
4+
`usesStandbyMode`, so `apify push` enables Standby.
5+
6+
- **Standby run**: serves a web page whose script calls `GET /api/greeting` with a root-relative URL, as most
7+
web UIs do. That works because the Actor owns `/` of its standby URL, as on `*.apify.actor`.
8+
- **Ordinary run** (`apify call`): a client of a standby Actor, this one unless the `standbyActor` input
9+
names another. It reads that Actor's `standbyUrl` through the API - from inside a container that is
10+
`http://apify-api:3333/actor-runtime/standby/<username>--<actor-name>` - calls `/api/greeting` there and
11+
pushes the answer to its default dataset.
12+
13+
```bash
14+
apify push
15+
TOKEN=<your token>
16+
17+
# In a browser:
18+
# http://<username>--my-standby-web-actor.localhost:3333/?token=<your token>
19+
curl "http://<username>--my-standby-web-actor.localhost:3333/api/greeting?name=Ada&token=$TOKEN"
20+
# The same endpoint, for clients that do not resolve *.localhost:
21+
curl "http://localhost:3333/actor-runtime/standby/<username>--my-standby-web-actor/api/greeting?name=Ada&token=$TOKEN"
22+
23+
apify call --input '{"name":"Actor"}' # another run calls the standby endpoint from its container
24+
```

0 commit comments

Comments
 (0)