gio.config.ts
The optional JavaScript config file: Node plugins that run around every request the render worker handles. Everything declarative belongs in gio.toml.
import { defineConfig } from '@gio.js/core';
import { internalOnly, renderTiming } from './lib/plugins';
export default defineConfig({
plugins: [internalOnly, renderTiming],
});Reference
The file is gio.config.ts or gio.config.js (checked in that order) in the project root, next to gio.toml - the parent of app/ (or of GIO_APP_DIR). Its default export is a GioConfig; defineConfig returns its argument unchanged and only adds the type.
Keys
| Key | Type | Default | Description |
|---|---|---|---|
plugins | GioNodePlugin[] | [] | Node plugins, run in array order around every request the worker handles. See GioNodePlugin. |
That is the only key. Server settings - limits, headers, redirects, guards, caching - are gio.toml keys, and declarative rules can also live in middleware.ts.
GioNodePlugin
import type { GioNodePlugin, IPCRequest, IPCResponse } from '@gio.js/core';
interface GioNodePlugin {
name: string;
version: string;
onRequest?: (req: IPCRequest) => Promise<IPCRequest | IPCResponse>;
onResponse?: (req: IPCRequest, res: IPCResponse) => Promise<IPCResponse>;
onStartup?: () => Promise<void>;
onShutdown?: () => Promise<void>;
}| Field | Type | Default | Description |
|---|---|---|---|
name (required) | string | - | Names the plugin in error logs and in the 500 body when a hook throws. |
version (required) | string | - | Your plugin's version (required by the type, informational). |
onRequest | function | - | Runs before routing. Return the request (changed or not) to continue, or a response to answer at once - later plugins and the page are skipped. |
onResponse | function | - | Runs on the finished response; return it, changed or not. |
onStartup | function | - | Runs when a worker process starts, before it serves. |
onShutdown | function | - | Runs when a worker process stops, plugins in reverse order. |
IPCRequest carries method, path, query, lowercased headers, body, locale, and when known ip, scheme, host and requestId. IPCResponse carries status, headers, body, cacheable, cacheMaxAge and setCookies.
Behavior
- Loading. Each worker imports the file at boot through
tsx, so it can import TypeScript from your project. A missing file means no plugins. - Validation. The export is checked at boot: anything but an object, an unknown key, a
pluginsthat is not an array, or a plugin without a stringnamestops the worker with an error naming the file:gio.config.ts: unknown key "plugin" - did you mean "plugins"? (gio.config takes plugins; server settings belong in gio.toml). The server prints it and exits 1 (see worker boot errors). - Which requests. Every request the Node worker handles: page renders (background revalidations included), page actions, route handlers and metadata routes. Requests Rust answers alone never reach a plugin: cache hits,
public/files and chunks,/_gio/image, and requests that a guard, redirect or rate limit fromgio.toml/middleware.tsalready answered. WebSocket messages do not go through plugins. - Errors. An
onRequestthat throws answers500withInternal Server Error (plugin: <name>). AnonResponsethat throws is logged and skipped: the response goes on as it was. Neither crashes the worker. - Streaming. An
onResponsehook must see the whole body, so while any plugin has one, page renders do not stream (partial prerendering included) and route handlers stream only event streams. Streamed route-handler responses skiponResponse. - Caching. What
onResponsereturns is what Rust caches, so a cache hit replays it without running the hook again. AnonRequestthat reads credentials (cookies,authorization, the client address) and then rewrites the path, query or locale makes that render per-visitor: it is not cached. A page that reads a header a plugin changed is not cached either. - Workers. With
[server] workersabove 1, every worker loads the plugins and runsonStartup- and again when a worker is respawned.GIO_WORKER_INDEXis"0"in one worker per server.
Examples
Refuse a path from onRequest
import type { GioNodePlugin } from '@gio.js/core';
/** Answers /internal/* with 403 unless the request carries the ops key. */
export const internalOnly: GioNodePlugin = {
name: 'internal-only',
version: '1.0.0',
async onRequest(req) {
if (!req.path.startsWith('/internal/')) return req;
const key = process.env.OPS_KEY;
if (key && req.headers['x-ops-key'] === key) return req;
return {
id: req.id,
status: 403,
headers: { 'content-type': 'text/plain; charset=utf-8' },
body: 'Forbidden',
cacheable: false,
cacheMaxAge: 0,
};
},
};A response from onRequest must echo req.id; keep cacheable: false for an answer that depends on the request. Check that the key is set: with OPS_KEY unset, comparing it to a missing header would compare undefined with undefined and let everyone in. For a plain cookie or role check, a [[guards]] rule in gio.toml runs in Rust before Node and needs no plugin.
Add a header in onResponse
import type { GioNodePlugin } from '@gio.js/core';
export const renderTiming: GioNodePlugin = {
name: 'render-timing',
version: '1.0.0',
async onRequest(req) {
req.headers['x-render-start'] = String(Date.now());
return req;
},
async onResponse(req, res) {
const start = Number(req.headers['x-render-start']);
return { ...res, headers: { ...res.headers, 'server-timing': `render;dur=${Date.now() - start}` } };
},
};$ curl -sI http://localhost:3000/about | grep -i server-timing
server-timing: render;dur=51Strip cookies from a response
import type { GioNodePlugin } from '@gio.js/core';
export const noCookiesOnPublic: GioNodePlugin = {
name: 'no-cookies-on-public',
version: '1.0.0',
async onResponse(req, res) {
return req.path.startsWith('/public-api/') ? { ...res, setCookies: [] } : res;
},
};Cookies a page or route handler sets arrive in res.setCookies, one entry per Set-Cookie, never in res.headers['set-cookie']. Append to the array to add one.
Run a startup job once per server
import type { GioNodePlugin } from '@gio.js/core';
import { primeSearchIndex } from './search';
export const warmup: GioNodePlugin = {
name: 'warmup',
version: '1.0.0',
async onStartup() {
if (process.env.GIO_WORKER_INDEX !== '0') return;
await primeSearchIndex(); // keep it idempotent: worker 0 can be respawned
},
};Good to know
giojs-server --check-configandgio doctordo not loadgio.config.ts. A validation error shows when the worker boots: the worker exits with the message above in the log, and the server exits once it gives up waiting for a worker.- In
gio dev, editinggio.config.tsor a module it imports restarts the worker.gio build standalonebundles it intoworker.js, and the testing kit (renderPage,callRoute) runs your plugins too. - Prefer
gio.tomlormiddleware.tsrules for redirects, rewrites, headers and guards: they run in Rust for every request, cache hits included, and cost no Node time. - There is no Rust plugin API in
gio.config.ts; it configures the Node worker only.
Related
Version history
| Version | Changes |
|---|---|
v0.1.0-beta.8 | Validated at boot (unknown keys and plugins without a name are errors). defineConfig and type GioConfig exported from @gio.js/core. onStartup / onShutdown run in every worker of a pool. Cookies reach onResponse in res.setCookies. Renders an onRequest plugin personalized are no longer cached. |
v0.1.0-beta.1 | Introduced: plugins with onRequest / onResponse / onStartup / onShutdown; gio.config.js is read when there is no gio.config.ts. |