GioJSdocs
On this page

Endpoints

The URLs the GioJS server answers itself: the /_gio endpoints, /_next/static assets and public/ files, with their methods, auth, responses and switches.

bash
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

EndpointExistsAccessTurn it off
GET /_gio/healthalwaysanyone[health] enabled = false
GET /_gio/metricswith a [metrics] sectionloopback, or token / ip_allowlistleave out [metrics], or enabled = false
GET /_gio/imagealwaysanyone; guards apply to local files[images] enabled = false
POST /_gio/revalidatewith a revalidation tokenAuthorization: Bearerno token
GET /_gio/fonts/*alwaysanyone-
/_gio/devtools*development onlylocal hosts and [dev] allowed_hosts[dev] devtools = false
GET /_next/static/*alwaysanyone-
public/ filesalwaysanyone; 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.

FieldTypeDefaultDescription
status"ok"-Always "ok".
nodeReadyboolean-false only while no render worker is connected (all of them restarting at once).
deploymentIdstring-The current deployment ID.
workers{ configured: number; ready: number }-The pool size and how many workers are connected.
http2boolean-Whether the server negotiates HTTP/2.
tlsboolean-Whether [server.tls] is on.
cacheEntriesnumber-Pages in the in-memory cache.
uptimeSecsnumber-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, else 403.
  • With neither ip_allowlist nor token: loopback clients only, else 403. A loopback request carrying X-Forwarded-For, Forwarded or X-Real-IP from a proxy [server] trusted_proxies does not list is refused too.
  • With token: also Authorization: Bearer <token>, compared in constant time, else 401.

The client is the one resolved through trusted_proxies. The metrics:

MetricTypeLabels
gio_requests_totalcountermethod, status, cache, route
gio_request_duration_secondshistogramroute
gio_node_ipc_latency_secondshistogramroute
gio_cache_entries, gio_cache_size_bytesgauge-
gio_prefetch_rejected_totalcounter-
gio_image_processed_totalcounterformat
gio_ratelimit_checked_totalcounterpath
gio_ratelimit_rejected_totalcounterpath, rule
gio_memory_bytesgaugetype="rss" (the Rust process only; 0 off Linux)
gio_workersgauge-
gio_worker_ready, gio_worker_in_flightgaugeworker
gio_worker_restarts_totalcounterworker

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.

ParameterTypeDefaultDescription
src (required)string-A file in public/ (/hero.jpg or /public/hero.jpg) or an http(s) URL matching [images] remote_patterns.
wnumber-Target width; must be one of [images] allowed_widths (default 16 ... 3840). Left out, the source width is kept.
qnumber[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.

StatusWhen
400w not in allowed_widths, q outside 1-100, a remote image over max_remote_bytes, or a malformed query
403a 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
404no src, or no such file
500the 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.

FieldTypeDefaultDescription
tagsstring[]-Purge every page tagged with one of these (up to 64; each 1-256 bytes, no control characters, not starting with _gio:).
pathsstring[]-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.
prefixbooleanfalsePurge 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:

StatusBodyWhen
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.

EndpointAnswersExtra check
GET /_gio/devtoolsThe dev dashboard (HTML): routes, cache, connections, memory, live log.Host only.
GET /_gio/devtools/stateThe 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/streamtext/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.txt for public/robots.txt, on GET and HEAD, ahead of your pages: a public file wins over a page with the same path. Served with Cache-Control: public, max-age=0, must-revalidate and Last-Modified, since the URL stays the same across deploys. Dotfiles (except under .well-known/), symlinks and a top-level public/_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, with Last-Modified and no Cache-Control of its own. An escaped separator (%2F, %5C) is a 400 there.

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

yaml
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

bash
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

gio.toml
[metrics]
token = "a-long-random-scrape-token"
ip_allowlist = ["10.0.0.0/8"]
prometheus.yml
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

bash
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: MISS

Good to know

  • The /_gio endpoints 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 an X-Request-Id.
  • Fixed, by design: the closed /_gio namespace, 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/health is 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 plain src.
  • Metadata routes (/sitemap.xml, /robots.txt, /manifest.webmanifest) are app routes rendered by the worker; see sitemap, robots and manifest.

Version history

VersionChanges
v0.1.0-beta.8POST /_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.