GioJSdocs
On this page

Turning Protections On and Off

Every protection and feature GioJS turns on by default, the gio.toml key that turns it off or loosens it, what that costs, and the few behaviors that stay fixed.

A new GioJS app is locked down without any configuration: cross-site requests are refused, responses carry security headers, connections and bodies are bounded, and the dev tools answer only your own machine. Each of these is a key in gio.toml, so an app with a reason to differ can turn one off - and the server tells you when you have. This page is the map: one table per area, with the key, its default, the value that turns it off, what you give up, and whether startup warns. Each section links to the full reference for its keys.

The rules every switch follows

  • On by default. A feature section has enabled = true unless you write otherwise; sub-features have their own booleans, also true.
  • 0 lifts a limit. Every numeric limit uses 0 for unlimited and every timeout uses 0 for none, in every section. To turn a feature off, use its enabled key, not a limit of 0. The exceptions: [cache] memory_max_entries must be at least 1 ([cache] enabled = false is the page cache's off switch); in [[rate_limits]], per_ip = 0 means no refill (only burst requests, ever: the strictest setting) and window_seconds = 0 is treated as 1 second; and [images] quality = 0 is treated as 1.
  • Never silent. Turning a protection off, or lifting a limit that guards memory or connections to 0, logs one warn line at startup that names the key and what it costs - the Warns column below says which keys do. Timeouts and the prefetch budget lift without a warning, except [server] render_timeout_secs and [images] remote_timeout_secs, which warn: at 0, a render that never answers holds its connection and a worker slot indefinitely, and a slow remote source holds its request open. giojs-server --check-config and gio doctor report the same text under warnings, so CI can catch it before a deploy.
  • Misspellings stop the server. An unknown key anywhere in gio.toml is a startup error naming the file, the line and the closest valid key. A typo such as enabeld = false can never leave a protection on that you meant to turn off, or the reverse.
bash
# Print what startup would decide, without binding a port: errors (unknown keys,
# invalid values, rules that cannot be enforced) and one warning per loosened protection.
npx giojs-server --check-config

The command loads the .env files and gio.toml exactly as startup does, prints a JSON report and exits with 1 when the server would refuse to start. It never prints secrets.

Request protections

Enforced in the Rust server before any of your Node code runs. Reference: [security], [security.csrf], [security.websocket].

ProtectionKey and defaultOff or loosenedWhat you give upWarns
CSRF check on POST, PUT, PATCH, DELETE[security.csrf] enabled = truefalse; or list origins in trusted_origins and paths in exemptAny website can submit forms and other unsafe requests to your app with your visitors' cookies.yes (off)
WebSocket Origin check[security.websocket] check_origin = truefalseAny website can open a WebSocket to your app as the visitor (cross-site WebSocket hijacking). Independent of the CSRF switch.yes
Default security headers: X-Content-Type-Options: nosniff, X-Frame-Options: SAMEORIGIN, Referrer-Policy: strict-origin-when-cross-origin[security] default_headers = truefalse drops all three; [security.headers] name = "" drops oneMIME sniffing, framing by other sites (clickjacking) and full-URL referrers come back.yes (default_headers = false)
HSTS (Strict-Transport-Security)[security] hsts unset: sent only when [server.tls] is onfalse or "" never sends it; true, a string or a table sends it behind a TLS proxyBrowsers may reach the site over plain HTTP first.no
Content-Security-Policyoff: [security] csp and csp_report_only unsetOpt-in - see Content Security Policy--
gio.toml
# Loosen instead of turning off, wherever you can.
[security.csrf]
trusted_origins = ["https://admin.example.com"]    # another origin of yours
exempt = ["/api/webhooks/*rest", "/saml/acs"]      # endpoints other sites post to

[[headers]]
path = "/embed/*rest"
[headers.headers]
x-frame-options = ""                               # let partners frame one section

Connection and request limits

These bound what one client, or a flood of them, can make the server hold. Reference: [server] and [[rate_limits]].

LimitKey and defaultOffWhat you give upWarns
Request body size (413 past it)[server] max_body_bytes = 2097152 (2 MiB)0Bodies are buffered in memory up to the worker's 64 MiB message cap (about 48 MiB of binary body), which still answers 413 above it.yes (also when set above what a message can carry)
Open connections, server-wide[server] max_connections = 100000A connection flood can exhaust file descriptors and memory.yes
Worker answer deadline (504 past it)[server] render_timeout_secs = 300A render that never answers holds its connection and a worker slot for good.yes
Slow clients: TLS handshake, request head, request body (408), idle connectionstls_handshake_timeout_secs = 10, header_read_timeout_secs = 10, request_body_timeout_secs = 30, idle_timeout_secs = 600 eachSlowloris-style clients can keep connections open indefinitely.no
HTTP/2 streams per connection, keep-alive pingshttp2_max_concurrent_streams = 250, http2_keep_alive_interval_secs = 20, http2_keep_alive_timeout_secs = 200 eachOne connection can open any number of streams; dead peers are noticed later.no
Rate-limit buckets kept in memory[server] rate_limit_max_buckets = 1000000Clients rotating addresses grow memory without bound. Warns only when [[rate_limits]] rules exist.yes
key_header values one client may hold a budget for[[rate_limits]] max_keys_per_client = 640One client can mint a fresh budget for every header value it sends. Warns only for rules with a key_header.yes
Prefetch budget per client (429 past it)[prefetch] max_concurrent = 5, max_per_second = 200 each; [prefetch] enabled = false refuses every prefetchPrefetching links can render pages for one client without limit.no
WebSocket connections (closed with 1013 past it)[websocket] max_connections = 10000Every open socket holds memory and a file descriptor, without a cap.yes

[[rate_limits]] rules themselves are opt-in: no rule, no limit. See Rate limits.

Client identity

Who the client is decides rate limits, the metrics allowlist, logs and ctx.ip. Reference: [server].

BehaviorKey and defaultLoosenedWhat you give upWarns
Forwarding headers are ignored unless a trusted proxy sent them[server] trusted_proxies = [] (trust nobody)a list of your proxies; an entry covering everything (0.0.0.0/0, ::/0)With a /0 entry, any client can pick its own IP, rate-limit bucket and request id.yes (/0 only)
An incoming X-Request-Id is kept only from a trusted proxy[server] accept_request_id = truefalse is stricter: every id is generated here-no

Operations endpoints

Reference: [metrics], [health], [revalidate]. See Endpoints & Headers for every /_gio path.

EndpointKey and defaultOff or loosenedWhat you give upWarns
/_gio/metrics (Prometheus)off without a [metrics] section; with one and no token or ip_allowlist, loopback clients onlyip_allowlist = ["0.0.0.0/0", "::/0"] opens it to everyone; enabled = false turns it offAnyone can read your traffic, routes and worker state.yes (open to everyone without a token)
/_gio/health[health] enabled = true, details = trueenabled = false answers 404; details = false answers only {"status":"ok","nodeReady":...}Load balancers lose the endpoint; gio dev, gio start and the testing kit then treat any answer as ready.no
POST /_gio/revalidateoff: exists only with [revalidate] token or GIO_REVALIDATE_TOKENOpt-in--

Development server

These keys apply only when the server runs with NODE_ENV=development (gio dev). In production /_gio/devtools* always answers 404. Reference: [dev].

BehaviorKey and defaultOff or loosenedWhat you give upWarns
Dev endpoints and error details answer only local hosts (DNS-rebinding protection)[dev] allowed_hosts = []list hostnames you browse from; ["*"] answers any Host from any machineWith "*", any site can read your source codeframes and live server state through DNS rebinding.yes ("*"; invalid entries are ignored with a warning)
Dev dashboard, codeframes, open-in-editor, live reload[dev] devtools = truefalse: /_gio/devtools* is not routed, and the error overlay shows no codeframes or editor links and does not live-reloadThe overlay tooling.no (logged as info)
Restart the worker when a source file changes[dev] watch = truefalse; or keep it and list paths in watch_ignoreEdits need a manual restart.no

Features

Turning one of these off saves work or hands it to something else (a CDN, an image service). None of them warns, except skew_protection = false and the image limits at 0.

FeatureKey and defaultOffWhat happens instead
Page cache (memory and disk)[cache] enabled = truefalseEvery request renders (X-Gio-Cache: bypass). Cache-Control still follows revalidate, so a CDN can keep caching.
Disk tier of the page cache[cache] disk_enabled = truefalseMemory only; nothing written, nothing survives a restart.
Page ETag and 304[cache] etag = truefalsePages are always sent in full.
Serving stale pages while one refresh runs[cache] swr_multiplier = 100A stale page is never served; Cache-Control drops stale-while-revalidate.
Image optimizer (/_gio/image)[images] enabled = truefalse/_gio/image answers 404; <GioImage> renders its plain src without a srcset.
Image optimizer limitsmax_remote_bytes = 20971520, remote_timeout_secs = 30, max_source_dimension = 10000, max_decode_bytes = 2684354560 each (warns)Large or slow sources, or a small file declaring huge dimensions, can exhaust memory and CPU.
Prefetching[prefetch] enabled = truefalseEvery prefetch gets 429 before it renders; links still navigate.
Compression (gzip, Brotli)[compression] enabled = truefalseResponses go out uncompressed (a proxy in front may compress them).
CSS served by path, minification, critical CSS[css] enabled, minify, critical_extraction, all truefalse eachenabled covers path-served app/*.css only; imported CSS is always bundled.
Font preload[[fonts]] preload = truefalse per entryThe @font-face stays; the browser fetches the file when text needs it.
WebSocket routes[websocket] enabled = truefalseUpgrades get 501. ping_interval_secs = 0 keeps sockets but sends no pings.
Deployment skew protection[server] skew_protection = truefalse (warns)x-deployment-id is ignored: a tab running an older build keeps navigating softly, with no 409 hard reload.
.env file loading[env] files = truefalse, or GIO_ENV_FILES=0The process environment is all there is. GIO_ENV_FILES wins over the key (1 forces loading on).
HTTP/2[server] http2 = truefalseHTTP/1.1 only.

What stays fixed, and why

A few behaviors have no switch. Each one either protects every other rule on this page or keeps one visitor's data away from another, and none blocks something an app legitimately needs:

  • Canonical request paths. Repeated and trailing slashes collapse and escaped unreserved characters are decoded before any rule matches; dot segments, a raw \ and invalid % escapes get 400. Every guard, rate limit, header rule and CSRF exemption relies on one spelling of a path - with it off, /api//login or /api/%6Cogin would slip past a rule for /api/login. Browsers never send the refused forms.
  • The closed /_gio namespace. Paths under /_gio/ that are not built-in endpoints answer 404 from Rust and never reach the app, so a dynamic route like app/[org]/settings cannot be rendered as /_gio/settings past its guard. Use any other prefix for your own routes.
  • No error details in production. A failed render answers a generic page with a digest, and the message and stack are logged under that digest. Raw messages leak file paths, SQL and sometimes secrets; the digest leads you to the full log line. See Error Handling.
  • Personalized renders are never shared. A render that read cookies, credentials, the client's IP or host is never stored, whatever revalidate says. A switch would hand one visitor's page to everyone. To cache and personalize, cache the shell with shell = 'cache' and personalize inside Suspense holes.
  • The server-only import guard is opt-in per module: importing @gio.js/core/server-only or naming a file *.server.ts marks it. Removing the marker is the off switch; a global one would ship the marked modules (database clients, keys) to browsers. See server-only.
  • Unknown gio.toml keys are errors. Tables for other tools go in [x-...] tables, which the server skips.
  • Only GIO_PUBLIC_* variables reach the browser. The prefix is the opt-in; rename a variable to expose it. See Environment Variables.

Parts of switchable features stay fixed too:

  • Open-in-editor requires a same-origin request (or Sec-Fetch-Site: none), and allowed_hosts = ["*"] does not cover it: it answers only localhost hosts from this machine, a specific [server] host and hosts listed by name, so no website, not even one rebound onto the dev server through DNS, can launch your editor.
  • The image optimizer always rejects path traversal, never follows redirects for remote sources, and holds a local src to the guards of its URL.
  • An adopted X-Request-Id must be 1 to 128 characters of letters, digits, ., _, : and -; anything else is replaced, which keeps log and header injection out.
  • The revalidation token must be at least 32 bytes: a short bearer token can be guessed, and a long one costs nothing.

Example: an internal app behind a gateway

An admin tool reachable only through a company gateway that terminates TLS, adds its own headers and posts webhooks from a partner origin. Each loosening is deliberate, and startup lists them:

gio.toml
[server]
trusted_proxies = ["10.0.0.0/8"]          # the gateway; never 0.0.0.0/0
max_body_bytes = 20971520                 # 20 MiB uploads

[security]
default_headers = false                   # the gateway sets them (warns)
hsts = true                               # TLS ends at the gateway

[security.csrf]
trusted_origins = ["https://partner.example.com"]

[metrics]
ip_allowlist = ["10.0.0.0/8"]             # the Prometheus network; loopback only without it

[health]
details = false                           # no deployment id or topology on a public probe

--check-config accepts it and lists the one protection it turns off (output trimmed):

bash
$ npx giojs-server --check-config
{...,"errors":[],...,"ok":true,...,"trustedProxies":1,"warnings":["[security] default_headers = false: responses no longer carry x-content-type-options, x-frame-options or referrer-policy (MIME sniffing, clickjacking and full-URL referrers are back) unless [security.headers] sets them"]}

Version history

VersionChanges
v0.1.0-beta.8The request protections, connection limits, trusted proxies and the dev host check are new, on by default. Introduced the switches [security] default_headers, [server] skew_protection, render_timeout_secs, rate_limit_max_buckets, [dev] devtools, watch, [health], [env], [images] enabled, [cache] enabled, disk_enabled, etag, swr_multiplier, [prefetch] enabled and [[fonts]] preload, and the limits [[rate_limits]] max_keys_per_client, [images] remote_timeout_secs, max_source_dimension and max_decode_bytes. 0 now lifts every limit. One startup warning per loosened protection.