Skip to content

Fastify & Node HTTP ​

New in 0.2.0, extended in 0.3.0

The Node handler and the Fastify adapter arrived in 0.2.0. 0.3.0 adds raw-byte uploads through both (uploadBodyLimit on Fastify), NodeHandlerOverrides.error, the 413 answer in the server's own envelope, and RouteResult.close so a stream ends as soon as the client leaves.

Use the Node handler for plain node:http (or any stack that gives you IncomingMessage / ServerResponse) and the Fastify adapter when the API lives under a Fastify prefix, possibly one with parameters (/api/portal/:portalKey). Both apply the server's cors setting.

Fastify ​

typescript
import Fastify from 'fastify';
import { createAgentServer } from '@cognipeer/agent-server';
import { mountOnFastify } from '@cognipeer/agent-server/fastify';

const app = Fastify();
const server = createAgentServer({ basePath: '/unused', storage, resolveContext });

// Static or parameterised prefix. Params arrive as req.mountParams in resolveContext.
mountOnFastify(app, server, { prefix: '/api/portal/:portalKey', bodyLimit: 15 * 1024 * 1024 });

Or register the plugin yourself: app.register(createFastifyPlugin(server), { prefix }).

Behaviour:

  • Routes are resolved relative to the prefix; the server's own basePath is not used.
  • Fastify has already parsed the JSON body when our route runs - the parsed body is used (the request stream is not read again).
  • A file uploaded as itself (POST /files?name=... with the file's own Content-Type) reaches the route as a Buffer: the plugin registers buffer parsers for text/plain and for every type Fastify has no parser for, in its own scope - your app's other routes keep Fastify's parsers (and a parser you registered for the same type in the mounted scope is left alone).
  • The reply is hijacked and written on the raw response (needed for SSE). Headers set by earlier hooks/plugins (request ids, CORS) are copied over.
  • Raise bodyLimit (Fastify default 1 MiB) if clients upload base64 files, or use uploadBodyLimit, the body limit of POST /files alone (a base64 file is a third bigger than the file; a raw one is its size). A body over the limit is answered in our envelope (413, { error: { code: "PAYLOAD_TOO_LARGE", message } }), not Fastify's own error shape; every other Fastify error still goes to your error handler.
  • fastify is an optional peer dependency (>= 4).

Options of mountOnFastify(app, server, options) / createFastifyPlugin(server, options):

OptionDefault
prefixmountOnFastify only: the Fastify prefix, static or parameterised
bodyLimitFastify's 1 MiBbody limit of the mounted routes. Raise it when clients upload base64 files
uploadBodyLimitunsetbody limit of POST /files alone; set it above your largest file
maxBodyByteslimits.maxBodySize, else ~1.4 x the largest file an agent acceptsthe Node handler's own limit, applied to what it reads itself

A complete example of an embedded chat that authenticates with a bearer token per request (no cookies) and is bound to the portal in the URL:

typescript
import Fastify from 'fastify';
import { createAgentServer } from '@cognipeer/agent-server';
import { mountOnFastify } from '@cognipeer/agent-server/fastify';

const server = createAgentServer({
  basePath: '/unused',                     // ignored by the Fastify adapter
  storage,
  cors: { enabled: true, origins: ['https://intranet.example.com'] },
  resolveContext: async (req) => {
    const header = req.headers.authorization;
    const token = typeof header === 'string' && header.startsWith('Bearer ') ? header.slice(7) : undefined;
    const session = token ? await sessions.lookup(token, { portal: req.mountParams.portalKey }) : undefined;
    return session ? { user: { id: session.userId }, data: { portal: session.portal } } : null;
  },
});

const app = Fastify();
mountOnFastify(app, server, { prefix: '/api/portal/:portalKey', uploadBodyLimit: 30 * 1024 * 1024 });
await app.listen({ port: 3000 });

Raw Node HTTP ​

typescript
import http from 'node:http';
import { createNodeHandler } from '@cognipeer/agent-server';

const handle = createNodeHandler(server, { basePath: '/api/agents' });

http.createServer(async (req, res) => {
  const handled = await handle(req, res); // false: path is outside basePath, nothing written
  if (!handled) { res.statusCode = 404; res.end(); }
}).listen(3000);

createNodeHandler(server, options):

OptionDefault
basePathserver.getBasePath()prefix to strip; "" if a router already stripped it
maxBodyByteslimits.maxBodySize, else ~1.4 x the largest file an agent accepts (server.maxUploadBytes())larger bodies answer 413

The returned function takes optional per-call overrides { path, body, mountParams, raw, error } for hosts that did part of the work (this is how the Fastify adapter is built). error answers with that { statusCode, code, message } instead of routing, with the usual CORS headers.

It handles token auth, resolveContext, JSON parsing (400 on invalid JSON) - and the bytes of a file uploaded as itself (POST /files with a non-JSON Content-Type) - CORS (config.cors), SSE with backpressure, and stops consuming the stream when the client disconnects.

Troubleshooting ​

SymptomCause and fix
404 Route not found under the prefixRoutes are relative to the Fastify prefix: with prefix: '/api/portal/:portalKey' the agents route is /api/portal/<key>/agents. The server's basePath is not used.
413 PAYLOAD_TOO_LARGE with { error: { code, message } }The body is over bodyLimit (or uploadBodyLimit for POST /files). Raise it above your largest file, a third more for base64 uploads.
resolveContext gets an empty mountParamsThe prefix has no parameter, or the plugin was registered without the prefix option. Parameters of the prefix become req.mountParams.
The browser blocks the call (CORS)Set cors.enabled and the exact origins on the server (the adapter applies it), or register a Fastify CORS plugin: headers set by earlier hooks are carried over to the answer.
A JSON parser of your own breaks the routesAn application/json parser that keeps the body as a string or Buffer is fine: the adapters decode it (invalid JSON is a 400). A parser you register for text/plain or * in the same scope is left alone.

Studio · Pulse · Console · Agent SDK and more — the Cognipeer documentation hub