The node's half of the browser service: the Node program in
browser-service/ that a user starts on their own machine, outside the node,
which runs Playwright code a script sends it — each browser call in a fresh
browser context — and keeps the storage states.
The browser, Playwright and Node.js stay out of the node, its image and its
release: a browser is hundreds of megabytes that most nodes never use, and
Playwright's own release cadence would become the node's. So the node owns
four things and no more — where the service is, the token it sends there,
how long a call may wait, and how a failure reads — and a script reaches the
service only through YmerNode.Script.Context.playwright/3, never by URL.
Where the service is
url/0. A release reads BROWSER_SERVICE_URL; without it, a node inside its
own image — where the bind marker is — reaches the host's loopback-bound
service at http://host.docker.internal:8013, and a node on the host at
http://127.0.0.1:8013. That default reaches on macOS Docker and on a host
node; on Linux Docker it does not, and the variable is the way there. A VM
that sets nothing — a script's own repository testing it under runtime: false — answers the host default, and its calls meet the test's stub
rather than the network.
The token
Every request carries the service's token as Authorization: Bearer, and
the service refuses one without it: a browser call's code runs in the
service's own process with the privileges of whoever started it, and a
loopback bind does not keep other programs out — every container on a Docker
host reaches the host through host.docker.internal, as the node's image
does. The token is the contents of a file only its owner can read,
~/.ymer-node/browser-service-token unless BROWSER_SERVICE_TOKEN_FILE
moves it, for the service and any node on the host alike, and whichever of
them needs it first writes it: the file 0600, and any directory it makes
0700. A node inside its own image cannot read a file on the host, so a
release there reads no file and sends the value of BROWSER_SERVICE_TOKEN,
which wins wherever it is set. A node that cannot read the file sends no
token, and neither does one that finds the file readable by others — that
one logs a warning naming the chmod — and the service's refusal says where
the token is. A request a plug answers — a test's stub — reaches no service,
so the node never reads or writes the default file for one; a token or a
file the configuration names is still sent.
How long a call may wait
What remains of the run's deadline, and nothing the script picks: the
service is told that remainder less a margin, which leaves it time to stop
the code, take the failure screenshot and answer before the node stops
listening at the deadline itself. Connecting takes at most a second of the
remainder, so a URL where nothing refuses and nothing answers reads as
:timeout rather than :unreachable. A run with no time left answers
:timeout and sends nothing. A longer browser flow raises its action's
timeout key, up to the run's cap.
The wire
Every request goes through YmerNode.Script.Context.request_options/0 — the
node's Req policy, and in tests the one plug every script call is stubbed
at — with the receive bound set per call. POST /call carries the code, its
arguments, the storage state and whether to save it, the options handed to
Playwright's browser.newContext(), whether to take the failure screenshot,
and bound_ms; the service answers {"ok": true, …} or {"ok": false, "error": {…}}. Anything else is :protocol: when it carries the service's
own {"error": text} — a request refused before any code ran — the text is
the message, and otherwise the answer is taken for another program's.
GET /status is what status/0 reports.
Summary
Functions
One browser call, bounded by deadline — the run's, as the
System.monotonic_time(:millisecond) instant it falls at.
YmerNode.Script.Context.playwright/3 is the door a script uses, and its
doc is where the options and the answer are described. save_storage: true
without a storage: name raises ArgumentError: there is no name to save
under, and no call is sent.
What scripts browser reports: the URL, whether anything answered there,
and — when the browser service did — its Playwright version, the Chromium
its headless browser runs (or why none launched), the storage state names
and the names an interactive window holds. When nothing answered,
something that is not the service did, or the service refused the request —
one without its token, say — message says what to do. Facts the node
measured at the call, never a verdict.
The browser service's URL (config :ymer_node, YmerNode.Scripts.BrowserService, :url), http://127.0.0.1:8013 when nothing sets it — § Where the service is. A
trailing slash is dropped, since the service answers only its own paths.
Functions
One browser call, bounded by deadline — the run's, as the
System.monotonic_time(:millisecond) instant it falls at.
YmerNode.Script.Context.playwright/3 is the door a script uses, and its
doc is where the options and the answer are described. save_storage: true
without a storage: name raises ArgumentError: there is no name to save
under, and no call is sent.
What scripts browser reports: the URL, whether anything answered there,
and — when the browser service did — its Playwright version, the Chromium
its headless browser runs (or why none launched), the storage state names
and the names an interactive window holds. When nothing answered,
something that is not the service did, or the service refused the request —
one without its token, say — message says what to do. Facts the node
measured at the call, never a verdict.
The browser service's URL (config :ymer_node, YmerNode.Scripts.BrowserService, :url), http://127.0.0.1:8013 when nothing sets it — § Where the service is. A
trailing slash is dropped, since the service answers only its own paths.