fastify-hooks-lifecycle — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited fastify-hooks-lifecycle (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
You are working with Fastify's hook system. Follow every rule in this document precisely. Violations of the critical rules below cause silent double executions, lost context, and runtime exceptions that are difficult to trace.
Obey these rules in every hook you write. There are no exceptions.
done callback. If the hook function is declared async, do not accept or call done. If the hook uses done, do not use async or return a Promise. Mixing the two causes double execution of downstream hooks and handlers.this, which means you lose access to the Fastify instance context (this is the encapsulating Fastify server). Always use function declarations or expressions.reply.send() and then return reply. If you forget return reply, Fastify continues executing the remaining lifecycle, resulting in double execution and potential "Reply already sent" errors.fastify-plugin (which breaks encapsulation) becomes global. A hook registered inside a normal plugin is scoped to that plugin and its children only. Be intentional about which you need.done callback MUST call it. Failing to call done hangs the request indefinitely.These hooks execute in the order listed below for every incoming request. Understand exactly where in the lifecycle each hook fires and what data is available at that point.
This is the first hook that fires, before any parsing occurs.
function (request, reply, done) (or async function (request, reply))request.body is UNDEFINED at this point. The request body has not been parsed yet. Do not attempt to read it.request.params, request.query, and request.headers ARE available.reply.code(401).send({ error: 'Unauthorized' }) and return reply (in async hooks) or call done(error) (in callback hooks).This hook fires before the body parsing phase begins.
function (request, reply, payload, done) (or async function (request, reply, payload))payload is the raw incoming request stream (a Node.js Readable stream). You can replace it with a different stream (e.g., for decompression or decryption).request.body is still UNDEFINED. Parsing has not happened yet.done, it MUST be a stream. You cannot return a plain object or string here.receivedEncodedLength property on the new stream to enable proper Content-Length validation. Example: newStream.receivedEncodedLength = oldStream.receivedEncodedLength || 0.This hook fires after the body has been parsed but BEFORE JSON Schema validation runs.
function (request, reply, done) (or async function (request, reply))request.body IS available and contains the parsed body. This is the earliest point where you can read the body.request.body here, the modified version is what the schema validator will see.This hook fires after schema validation succeeds, immediately before your route handler executes.
function (request, reply, done) (or async function (request, reply))request.body, request.params, and request.querystring are all fully parsed, typed, and schema-validated at this point.request.user, checking resource ownership, and performing any pre-handler logic that depends on a fully validated request.This hook fires after the route handler returns its payload but before Fastify serializes it to JSON.
function (request, reply, payload, done) (or async function (request, reply, payload))payload is the JavaScript object returned by your route handler.{ data: payload, meta: { ... } }), adding pagination metadata, or injecting HATEOAS links into response objects.done.This hook fires when an exception occurs during the request lifecycle, BEFORE the custom error handler runs.
function (request, reply, error, done) (or async function (request, reply, error))reply.send() inside this hook. Doing so throws an exception. The error handler is responsible for sending the response.done(err) in this hook. Passing an error into the done callback is not supported and will cause undefined behavior.setErrorHandler), not here.This hook fires right before the response is written to the socket.
function (request, reply, payload, done) (or async function (request, reply, payload))payload is the final serialized value (a string or Buffer). If the handler returned a stream, payload is that stream.reply.header('X-Request-Id', id)), replacing or clearing the response payload, compressing the response, or adding security headers.This hook fires after the response has been completely sent to the client.
function (request, reply, done) (or async function (request, reply))reply.send() here will throw.reply.elapsedTime gives you the time in milliseconds since the request started, which is useful for latency metrics.This hook fires when a request exceeds the configured connectionTimeout and the socket is hung up.
function (request, reply, done) (or async function (request, reply))connectionTimeout on the Fastify instance for this hook to fire.This hook fires when the client prematurely closes the connection before the response is sent.
function (request, reply, done) (or async function (request, reply))These hooks are not tied to individual requests. They fire during server lifecycle events.
fastify.ready() is called explicitly).function (done) (or async function ()) -- note there is no request or reply.fastify.close() is called, after the server has stopped accepting new connections and all in-flight requests have completed.function (instance, done) (or async function (instance))function (routeOptions) -- there is NO done callback. This is purely synchronous.routeOptions contains the full route configuration: method, url, schema, handler, preHandler, etc.routeOptions object to dynamically modify routes as they are registered.fastify-plugin (since fastify-plugin prevents the creation of a new encapsulation context).When writing or reviewing Fastify hook code, actively check for and prevent these errors:
preValidation or later.onError hook.done() inside an async function causes the hook to complete twice.this from the enclosing scope, not the Fastify instance. You lose access to decorators, server config, and other instance properties via this.onSend must be a string, Buffer, stream, ReadableStream, Response, or null. Objects are not valid.When registering hooks, follow these patterns:
// CORRECT: named function, preserves this binding
fastify.addHook('onRequest', function onRequestAuth(request, reply, done) {
// this === fastify instance
done()
})
// CORRECT: async without done
fastify.addHook('preHandler', async function preHandlerAuth(request, reply) {
const user = await this.db.findUser(request.headers.authorization)
if (!user) {
reply.code(401).send({ error: 'Unauthorized' })
return reply // REQUIRED: signals short-circuit
}
request.user = user
})
// WRONG: arrow function loses this
fastify.addHook('onRequest', async (request, reply) => {
this.log.info('request') // this is NOT fastify
})
// WRONG: async + done = double execution
fastify.addHook('onRequest', async function (request, reply, done) {
await someAsyncWork()
done() // BUG: promise resolution AND done both signal completion
})Understand the encapsulation rules:
fastify-plugin, the hook "leaks" to the parent context and behaves as if registered there.~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.