大家好,我是 Yubin
這篇文章會講 Fastify 的 Hook,可以監聽特定的事件,在事件觸發前呼叫自己想要的處理函式。
本文中的 server 指的是 FastifyInstance,如下程式片段。
import fastify, { FastifyInstance } from 'fastify'
const server: FastifyInstance = fastify()
Hook 藉由 server.addHook()
註冊,可以監聽應用程式的事件或 request/response 生命週期中的事件。
應用程式事件。
有 onReady、onClose、onRoute、onRegister 四種。
onReady
觸發 onReady hook 有兩個情況。
第一是伺服器開始聽 request 的時候。
第二是 server 的 .ready()
被呼叫的時候觸發。
Hook 的註冊有兩種風格可以用,一種是 callback、一種是 async/await。
// callback style
server.addHook('onReady', (done) => {
// Some code
done()
})
done()
不能用在 async/await 的寫法上,done
的 Type 是 HookHandlerDoneFunction
。
代表這個 hook 的動作結束。
server.addHook('onReady', async () => {
// Some async code
await loadCache()
})
如果 hook handler 中需要進行 async function 的呼叫,非常方便使用。
onClose
當 server 的 .close()
被呼叫時觸發。
當需要在關閉伺服器前做一些事情的時候很好用,例如寫 log 或關閉資料庫連線。
// callback style
server.addHook('onClose', (instance, done) => {
// some code
done()
})
// or async/await style
server.addHook('onClose', async (instance) => {
// some async code
await closeDatabaseConnections()
})
onRoute
當有一個新的 route 被註冊時觸發。
會傳入一個 RouteOptions
物件。
server.addHook('onRoute', (routeOptions) => {
// Some code
routeOptions.method
routeOptions.schema
routeOptions.url
routeOptions.path
routeOptions.routePath
routeOptions.bodyLimit
routeOptions.logLevel
routeOptions.prefix
})
備註:官網的程式範例有誤,RouteOptions 裡面已經沒有
logSerializers
欄位。
onRegister
當有一個新的 plugin 被註冊 前 觸發。
但若該 plugin 是被 fastify-plugin 所封裝的,不會觸發此 hook。
Request 和 Reply 都是 Fastify 核心的物件。
done()
在前面提到的 Application Hook 中代表著該 hook 的結束,對於 Request/Reply Hooks 來說,除了代表 hook 的結束,也代表該 request/response 進入下一個生命週期。
onRequest
request 進來的第一個 hook。
// callback style
server.addHook('onRequest', (request, reply, done) => {
// some code
done()
})
// async/await style
server.addHook('onRequest', async (request, reply) => {
// some async code
await asyncMethod()
})
值得注意的是,在這個 hook 中,request.body
永遠都是 undefined
,因為在觸發這個 hook 的時候,Request 的 body 還沒被 parsing。
preParsing
preParsing 這個 hook,可以在解析 Request Payload 成 request.body
前,對 Request 的 Payload 進行操作。
server.addHook('preParsing', (request, reply, payload, done) => {
// some code
done(null, newPayload)
})
這裡的 done(null, newPayload)
,第一個參數是 Error,第二個參數是修改過後的 Request Payload。
payload 的 Type 是 RequestPayload
。
一樣也可以用 async/await 的寫法。
server.addHook('preParsing', async (request, reply, payload) => {
// some async code
return newPayload
})
跟 onRequest
hook 一樣,preParsing
hook 也是在實際解析 Request Payload 成 request.body
前進行,所以 request.body
必定為 undefined
。
如果有對 payload 進行修改,則必須一並調整新的 RequestPayload 中的 receivedEncodedLength
屬性,避免後面的操作發現 header 中的 Content-Length
欄位值與實際的 Request Payload 長度不符。
preValidation
preValidation hook 在執行 body 的 validation 之前被呼叫,可以在進行驗證工作前修改 request.body
。
// callback style
server.addHook('preValidation', (request, reply, done) => {
// some code
const oldBody = request.body
request.body = { oldBody, importantKey: 'randomString' }
done()
})
// async/await style
server.addHook('preValidation', async (request, reply) => {
const oldBody = request.body
// some async code
const key = await getRandomString()
request.body = { oldBody, importantKey: key}
})
preHandler
preHandler hook 在進到 route handler 之前被呼叫。
// callback style
fastify.addHook('preHandler', (request, reply, done) => {
// some code
done()
})
// async/await style
fastify.addHook('preHandler', async (request, reply) => {
// some async code
await asyncMethod()
})
preSerialization
藉由 preSerialization hook,可以讓你在進行 Response 的序列化 (Serialization) 之前修改 Payload。
// callback style
server.addHook('preSerialization', (request, reply, payload, done) => {
const err = null
const newPayload = { wrapped: payload }
done(err, newPayload)
})
// async/await style
server.addHook('preSerialization', async (request, reply, payload) => {
return { wrapped: payload }
})
要注意的是,如果 Payload 是 string
, Buffer
, stream
或 null
的時候,不會觸發此 hook。
onError
onError hook 在自訂的 error handler 拋出錯誤時被呼叫。
當要對特別的 error handler 進行處理的時候很常用,例如寫 log 或增加特別的 header。
// callback style
server.addHook('onError', (request, reply, error, done) => {
// some code
done()
})
// async/await style
server.addHook('onError', async (request, reply, error) => {
// Useful for custom error logging
// You should not use this hook to update the error
})
這個 hook 不是用來改變 error 用的,呼叫 reply.send
會噴錯。
只有在執行 customErrorHandler
的過程出錯才會觸發此 hook。
跟其他 hook 不同的是,done
function 不能傳入錯誤。(因為這邊是錯誤的終點了)
onSend
onSend hook 可以讓你在 Response 出去前更改 Payload。
// callback style
server.addHook('onSend', (request, reply, payload, done) => {
const err = null;
const newPayload = payload.replace('some-text', 'some-new-text')
done(err, newPayload)
})
// async/await style
server.addHook('onSend', async (request, reply, payload) => {
const newPayload = payload.replace('some-text', 'some-new-text')
return newPayload
})
也可以藉由把 Payload 設為 null
來回傳空的 Payload 出去。
server.addHook('onSend', (request, reply, payload, done) => {
reply.code(304) // for 'Not Modified'
const newPayload = null
done(null, newPayload)
})
要注意的是,這邊的 Payload 已經被序列化 (Serialization) 過了,只能把 Payload 改為 string
, Buffer
, stream
或 null
等型態。
onResponse
onResponse hook 在 Response 發送出去後被呼叫。
所以不能藉由這個 hook 來修改 Response 的內容 (阿就已經送出去了),但要寫 log 或做一些統計數據的用途很好用。
// callback style
server.addHook('onResponse', (request, reply, done) => {
// some code
done()
})
// async/await style
server.addHook('onResponse', async (request, reply) => {
// some async code
await asyncMethod()
})
onTimeout
在發生 timeout 時候觸發,如果要監控 Request timeout 的時候很好用。
// callback style
server.addHook('onTimeout', (request, reply, done) => {
// some code
done()
})
// async/await style
server.addHook('onTimeout', async (request, reply) => {
// some async code
await asyncMethod()
})
要注意的是,onTimeout hook 執行的時候 HTTP socket 已經掛斷,所以沒辦法再發送 Response 給對方。
我們在寫 hook 的 handler 的時候也常常會有例外處理或拋出錯誤的需求。
可以藉由 done()
把錯誤往下傳。(除了 onError
hook。)
server.addHook('onRequest', (request, reply, done) => {
done(new Error('Some error'))
})
除了拋出錯誤,也可以指定要回應的 Status Code。
server.addHook('preHandler', (request, reply, done) => {
reply.code(400)
done(new Error('Some error'))
})
Error 會在 Reply 的時候被處理。
有時候我們想在 hook 的處理過程就回應 Respone 出去。
例如,在一個身分驗證的 hook 中,判斷出 Request 沒有符合認證,就可以回應 401 出去了,而不用走到 Route 定義的 Handler 那邊處理。
// callback style
server.addHook('onRequest', (request, reply, done) => {
// some code
reply.send('Early response')
})
// async/await style
server.addHook('preHandler', async (request, reply) => {
// some async code
await something()
reply.send({ hello: 'world' })
return reply // mandatory, so the request is not executed further
})
注意,使用 async function 的時候要回傳 reply 物件。
Hook 的註冊除了定義 Global 的伺服器行為,也可以定義 Route Level 的 hook。
對於特定 Route 有特別的處理非常實用。
以上是 Fastify hook 的介紹。