Endpoints
The URLs the GioJS server answers itself: the /_gio endpoints, /_next/static assets and public/ files, with their methods, auth, responses and switches.
curl -s http://localhost:3000/_gio/health
{"cacheEntries":0,"deploymentId":"0e92bc3a01f4ea44","http2":true,"nodeReady":true,"status":"ok","tls":false,"uptimeSecs":28,"workers":{"configured":1,"ready":1}}These are answered by the Rust server, never by your app: no page, route.ts or gio.config.ts plugin can take over a path under /_gio/.
Reference
| Endpoint | Exists | Access | Turn it off |
|---|---|---|---|
GET /_gio/health | always | anyone | [health] enabled = false |
GET /_gio/metrics | with a [metrics] section | loopback, or token / ip_allowlist | leave out [metrics], or enabled = false |
GET /_gio/image | always | anyone; guards apply to local files | [images] enabled = false |
POST /_gio/revalidate | with a revalidation token | Authorization: Bearer | no token |
GET /_gio/fonts/* | always | anyone | - |
/_gio/devtools* | development only | local hosts and [dev] allowed_hosts | [dev] devtools = false |
GET /_next/static/* | always | anyone | - |
public/ files | always | anyone; rules apply | - |
Any other path under /_gio/ answers 404 (see The /_gio namespace). An endpoint that is turned off is not routed, so it answers that same 404.
/_gio/health
GET. The liveness and readiness endpoint for load balancers, orchestrators and uptime monitors. Always 200 with application/json, even while every worker is restarting: cached pages and static files keep serving then, so the server is up. Readiness probes should read nodeReady.
| Field | Type | Default | Description |
|---|---|---|---|
status | "ok" | - | Always "ok". |
nodeReady | boolean | - | false only while no render worker is connected (all of them restarting at once). |
deploymentId | string | - | The current deployment ID. |
workers | { configured: number; ready: number } | - | The pool size and how many workers are connected. |
http2 | boolean | - | Whether the server negotiates HTTP/2. |
tls | boolean | - | Whether [server.tls] is on. |
cacheEntries | number | - | Pages in the in-memory cache. |
uptimeSecs | number | - | Seconds since the server started. |
With [health] details = false the body is only {"nodeReady":true,"status":"ok"}, for a server reachable from the internet that should not tell visitors its deployment ID or topology. With enabled = false the path is a 404; gio dev, gio start and the testing kit then take that 404 as ready, since the port only opens once a worker is connected.
/_gio/metrics
GET, in Prometheus text format (text/plain; version=0.0.4). The route answers 404 unless gio.toml has a [metrics] section (whose enabled defaults to true). Who may scrape it:
- With
ip_allowlist: clients in the listed IPs or CIDR blocks, else403. - With neither
ip_allowlistnortoken: loopback clients only, else403. A loopback request carryingX-Forwarded-For,ForwardedorX-Real-IPfrom a proxy[server] trusted_proxiesdoes not list is refused too. - With
token: alsoAuthorization: Bearer <token>, compared in constant time, else401.
The client is the one resolved through trusted_proxies. The metrics:
| Metric | Type | Labels |
|---|---|---|
gio_requests_total | counter | method, status, cache, route |
gio_request_duration_seconds | histogram | route |
gio_node_ipc_latency_seconds | histogram | route |
gio_cache_entries, gio_cache_size_bytes | gauge | - |
gio_prefetch_rejected_total | counter | - |
gio_image_processed_total | counter | format |
gio_ratelimit_checked_total | counter | path |
gio_ratelimit_rejected_total | counter | path, rule |
gio_memory_bytes | gauge | type="rss" (the Rust process only; 0 off Linux) |
gio_workers | gauge | - |
gio_worker_ready, gio_worker_in_flight | gauge | worker |
gio_worker_restarts_total | counter | worker |
route is the matched pattern (/posts/:id), or static (assets and public files), internal (the /_gio endpoints) or unmatched, so the label set stays bounded however many URLs are requested; past 1024 patterns, the others count as _other. cache is hit, stale, miss (rendered by the worker, stored or not), stream (a streamed render), error (the worker failed or timed out), bypass (the /_gio endpoints, and a body over max_body_bytes) or static.
/_gio/image
GET. The image optimizer behind <GioImage>: it resizes and re-encodes a source image, caches the result on disk, and serves it.
| Parameter | Type | Default | Description |
|---|---|---|---|
src (required) | string | - | A file in public/ (/hero.jpg or /public/hero.jpg) or an http(s) URL matching [images] remote_patterns. |
w | number | - | Target width; must be one of [images] allowed_widths (default 16 ... 3840). Left out, the source width is kept. |
q | number | [images] quality (75) | Quality, 1-100. |
f | "avif" | "webp" | "jpeg" | "jpg" | "png" | - | Force an output format. A modern format left out of [images] formats, or an unknown value, falls back to negotiation. |
Without f, the format is the first of [images] formats (default AVIF, then WebP) that the request's Accept header names, else JPEG. A 200 carries Vary: Accept, X-Gio-Cache: HIT or MISS (the optimizer's own disk cache), and Cache-Control: public, max-age=31536000, immutable, or private, no-cache when a guard covers the source file and admitted this visitor.
| Status | When |
|---|---|
400 | w not in allowed_widths, q outside 1-100, a remote image over max_remote_bytes, or a malformed query |
403 | a remote host not in remote_patterns, a remote redirect, a path leaving public/, or a guard that turns this visitor away from the local file |
404 | no src, or no such file |
500 | the download failed (including an HTTP error status), or the image could not be decoded within max_source_dimension / max_decode_bytes |
Unlike the other /_gio endpoints, it counts against [[rate_limits]] rules that match it: it is the most CPU-expensive endpoint there is. See [images] for every limit.
/_gio/revalidate
POST only. On-demand revalidation for CMS webhooks, deploy scripts and other systems outside the app (code inside it calls revalidateTag and revalidatePath). The route exists only when GIO_REVALIDATE_TOKEN or [revalidate] token is set (at least 32 bytes); otherwise it is a 404.
| Field | Type | Default | Description |
|---|---|---|---|
tags | string[] | - | Purge every page tagged with one of these (up to 64; each 1-256 bytes, no control characters, not starting with _gio:). |
paths | string[] | - | Purge these URL paths (up to 64; each starts with /, at most 2048 bytes, no ./.. segments or malformed escapes; a query string is ignored). Pages are cached under their locale-free path, so /fr/blog purges /blog in every locale. |
prefix | boolean | false | Purge each path and everything below it. |
Send Authorization: Bearer <token> and a JSON body (the Content-Type is not checked) of at most 64 KiB with at least one tag or path. Unknown fields are an error, so a misspelled tag cannot silently purge nothing. Every answer is JSON with Cache-Control: no-store:
| Status | Body | When |
|---|---|---|
200 | {"ok":true,"purged":2} | Done: the entries are gone from memory and disk, PPR shells included. purged counts the entries removed. |
400 | {"error":"..."} | Not the expected JSON, an unknown field, an invalid tag or path, too many, or nothing to revalidate. |
401 | {"error":"unauthorized"} | Missing or wrong token; carries WWW-Authenticate: Bearer. |
408 | {"error":"request body timed out"} | The body took longer than [server] request_body_timeout_secs. |
413 | {"error":"request body too large"} | Over 64 KiB. |
429 | {"error":"too many failed attempts"} | After 10 failed attempts from one client (an IPv6 client counts by its /64), every request from it is refused this way, with Retry-After and before its token is even checked, until a minute has passed since its first failure. The 10 failures themselves get 401. |
/_gio/fonts
GET /_gio/fonts/*: the self-hosted [[fonts]] files, downloaded or copied at startup into .gio/fonts (GIO_FONTS_DIR), and /_gio/fonts/fonts.css with their @font-face rules. Pages link the stylesheet and preload each font (unless preload = false). Font files are immutable (public, max-age=31536000, immutable); fonts.css is rewritten at every start under the same URL, so it revalidates (public, max-age=0, must-revalidate). Both carry X-Gio-Cache: static.
/_gio/devtools
Development endpoints, routed only when the server runs in development mode and [dev] devtools is on (the default). In production they are 404. Every one checks the request before it runs: the Host must be a localhost name or loopback IP (on a connection from this machine), the specific [server] host, or an entry of [dev] allowed_hosts; otherwise a 403 explains which entry to add. This defeats DNS rebinding.
| Endpoint | Answers | Extra check |
|---|---|---|
GET /_gio/devtools | The dev dashboard (HTML): routes, cache, connections, memory, live log. | Host only. |
GET /_gio/devtools/state | The dashboard's data as JSON. | Not cross-site: Sec-Fetch-Site other than cross-site, and an Origin (if any) naming the host. |
GET /_gio/devtools/stream | text/event-stream of log lines and snapshots; the live-reload channel. | Same as state. |
GET /_gio/devtools/codeframe?file=&line= | { file, line, lines: [{ no, text }] }: the line and 4 lines around it, for the error overlay. 403 outside the project root (symlinks resolved), 404 for a missing file, 400 for a non-source file, a line out of range or a file over 2 MiB. | Same as state. |
POST /_gio/devtools/open-in-editor?file=&line= | {"ok":true} after launching GIO_EDITOR on the file. GET is 405. | Same-origin only: Sec-Fetch-Site, when sent, must be same-origin or none, and an Origin must name the host. |
With [dev] devtools = false none of them is routed, and the error overlay shows no codeframes, editor links or live reload.
/_next/static
GET /_next/static/*: the client bundles and stylesheets the worker builds at startup: /_next/static/chunks/ (route chunks) and /_next/static/css/ (route stylesheets, CSS Modules). Names carry a content hash, so they are served with Cache-Control: public, max-age=31536000, immutable and X-Gio-Cache: static. The directory is .gio/build/static (GIO_STATIC_DIR).
public/ files
Every file in public/ (GIO_PUBLIC_DIR) answers at two URLs:
- At the site root,
/robots.txtforpublic/robots.txt, onGETandHEAD, ahead of your pages: a public file wins over a page with the same path. Served withCache-Control: public, max-age=0, must-revalidateandLast-Modified, since the URL stays the same across deploys. Dotfiles (except under.well-known/), symlinks and a top-levelpublic/_gio/are never served here. The set of files is indexed at startup (and by the dev watcher): in production, a file added later needs a restart. - Under
/public/,/public/robots.txt, withLast-Modifiedand noCache-Controlof its own. An escaped separator (%2F,%5C) is a400there.
Both answer X-Gio-Cache: static and never reach Node. Guards, header rules and rate limits written for the /public/... URL also apply to the root URL. A file named like a metadata route (public/robots.txt, public/sitemap.xml, public/manifest.webmanifest) wins over app/robots.ts and the others, with a startup warning.
The /_gio namespace
/_gio/ belongs to the server. A path under it that is not one of the endpoints above answers 404 from Rust before rate limits, rules, the cache or Node see it, so /_gio/settings can never render app/[org]/settings with org = "_gio", and a guard on /:org/settings cannot be sidestepped that way. The check uses the first non-empty segment of the normalized path (//_gio/x counts), and also applies after a locale prefix is stripped or a rewrite lands there. It cannot be turned off.
Examples
Kubernetes probes
livenessProbe:
httpGet: { path: /_gio/health, port: 3000 }
readinessProbe:
exec:
command: ["node", "-e", "fetch('http://127.0.0.1:3000/_gio/health').then(r => r.json()).then(h => process.exit(h.nodeReady ? 0 : 1), () => process.exit(1))"]Purge from a CMS webhook
curl -X POST https://example.com/_gio/revalidate \
-H "Authorization: Bearer $GIO_REVALIDATE_TOKEN" \
-H "Content-Type: application/json" \
-d '{"tags":["post:42"],"paths":["/blog"],"prefix":true}'
# {"ok":true,"purged":3}Scrape metrics from a Prometheus server
[metrics]
token = "a-long-random-scrape-token"
ip_allowlist = ["10.0.0.0/8"]scrape_configs:
- job_name: giojs
metrics_path: /_gio/metrics
authorization:
credentials: a-long-random-scrape-token
static_configs:
- targets: ["app-1:3000", "app-2:3000"]Request an optimized image
curl -sI "http://localhost:3000/_gio/image?src=/hero.jpg&w=640&q=75" -H "Accept: image/avif,image/webp"
# HTTP/1.1 200 OK
# content-type: image/avif
# cache-control: public, max-age=31536000, immutable
# vary: Accept
# x-gio-cache: MISSGood to know
- The
/_gioendpoints are exempt from[[rate_limits]](except/_gio/image), from guards, redirects, rewrites and header rules, and from the CSRF check; each has its own access control instead. They still get the default security headers and anX-Request-Id. - Fixed, by design: the closed
/_gionamespace, the same-origin and host checks on open-in-editor (allowed_hosts = ["*"]does not cover it), the optimizer's path-traversal and redirect checks, and the 32-byte minimum for the revalidation token. /_gio/healthis answered even when the app cannot render, so it proves the server is up, not that your pages work; probe a page of your own for that.- A static export has no server: none of these endpoints exist there, and
<GioImage>renders its plainsrc. - Metadata routes (
/sitemap.xml,/robots.txt,/manifest.webmanifest) are app routes rendered by the worker; see sitemap, robots and manifest.
Related
- Headers - what these endpoints and your pages send and read
- Health check and Metrics in production
- On-demand revalidation
- Image Optimization and Font Optimization
- Development error overlay
[health],[metrics],[images],[revalidate],[dev]- Turning Protections On and Off
Version history
| Version | Changes |
|---|---|
v0.1.0-beta.8 | POST /_gio/revalidate added. Unknown /_gio/ paths answer 404 in Rust. public/ served at the site root. [health] enabled / details, [images] enabled and [dev] devtools switches. /_gio/metrics answers loopback clients only without a token or allowlist, adds route labels and worker metrics; /_gio/health adds workers. Dev endpoints answer local hosts only, and open-in-editor is same-origin POST. Guards cover local /_gio/image sources. fonts.css revalidates, and the fonts carry X-Gio-Cache: static. /_gio/health sends a Content-Length instead of a chunked body. |
v0.1.0-beta.6 | /_gio/health reports deploymentId, nodeReady, cacheEntries and uptimeSecs. /_gio/image honors [[rate_limits]]. Dev codeframe and open-in-editor endpoints. |
v0.1.0-beta.1 | /_gio/health, /_gio/metrics, /_gio/image, /_gio/devtools, /_next/static and public/ serving introduced. |