Response对象 #

一、Response概述 #

Response对象(在Fastify中称为Reply)用于构建和发送HTTP响应。

1.1 Reply对象结构 #

javascript
fastify.get('/test', async (request, reply) => {
  console.log(reply.statusCode)
  console.log(reply.sent)
  console.log(reply.elapsedTime)
  
  return { ok: true }
})

1.2 发送响应的方式 #

javascript
fastify.get('/way1', async (request, reply) => {
  return { message: 'Auto send' }
})

fastify.get('/way2', async (request, reply) => {
  reply.send({ message: 'Manual send' })
})

fastify.get('/way3', async (request, reply) => {
  reply.code(200).send({ message: 'Chained send' })
})

二、状态码 #

2.1 设置状态码 #

javascript
fastify.get('/ok', async (request, reply) => {
  reply.code(200)
  return { message: 'OK' }
})

fastify.get('/created', async (request, reply) => {
  reply.code(201)
  return { message: 'Created' }
})

fastify.get('/no-content', async (request, reply) => {
  reply.code(204).send()
})

fastify.get('/bad-request', async (request, reply) => {
  reply.code(400)
  return { error: 'Bad Request' }
})

fastify.get('/not-found', async (request, reply) => {
  reply.code(404)
  return { error: 'Not Found' }
})

fastify.get('/server-error', async (request, reply) => {
  reply.code(500)
  return { error: 'Internal Server Error' }
})

2.2 状态码别名 #

javascript
fastify.register(require('@fastify/sensible'))

fastify.get('/status-alias', async (request, reply) => {
  reply.badRequest('Invalid parameters')
})

fastify.get('/unauthorized', async (request, reply) => {
  reply.unauthorized('Token required')
})

fastify.get('/forbidden', async (request, reply) => {
  reply.forbidden('Access denied')
})

fastify.get('/not-found-alias', async (request, reply) => {
  reply.notFound('Resource not found')
})

2.3 常用状态码 #

状态码 方法 说明
200 - 成功
201 - 创建成功
204 - 无内容
400 badRequest 请求错误
401 unauthorized 未授权
403 forbidden 禁止访问
404 notFound 未找到
409 conflict 冲突
429 tooManyRequests 请求过多
500 internalServerError 服务器错误

三、响应头 #

3.1 设置响应头 #

javascript
fastify.get('/headers', async (request, reply) => {
  reply
    .header('X-Custom-Header', 'value')
    .header('X-Another-Header', 'another-value')
  
  return { message: 'Headers set' }
})

3.2 常用响应头 #

javascript
fastify.get('/common-headers', async (request, reply) => {
  reply
    .header('Content-Type', 'application/json')
    .header('Cache-Control', 'public, max-age=3600')
    .header('X-Request-Id', request.id)
    .header('X-Response-Time', '10ms')
  
  return { message: 'Common headers' }
})

3.3 CORS响应头 #

javascript
fastify.get('/cors', async (request, reply) => {
  reply
    .header('Access-Control-Allow-Origin', '*')
    .header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE')
    .header('Access-Control-Allow-Headers', 'Content-Type, Authorization')
  
  return { message: 'CORS headers' }
})

3.4 删除响应头 #

javascript
fastify.get('/remove-header', async (request, reply) => {
  reply.removeHeader('X-Powered-By')
  return { message: 'Header removed' }
})

3.5 获取响应头 #

javascript
fastify.get('/get-headers', async (request, reply) => {
  reply.header('X-Custom', 'value')
  
  return {
    customHeader: reply.getHeader('X-Custom'),
    allHeaders: reply.getHeaders()
  }
})

四、响应体 #

4.1 JSON响应 #

javascript
fastify.get('/json', async (request, reply) => {
  return {
    message: 'Hello JSON',
    timestamp: Date.now()
  }
})

fastify.get('/json-explicit', async (request, reply) => {
  reply.send({
    message: 'Explicit JSON',
    timestamp: Date.now()
  })
})

4.2 字符串响应 #

javascript
fastify.get('/string', async (request, reply) => {
  reply.type('text/plain')
  return 'Hello World'
})

fastify.get('/html', async (request, reply) => {
  reply.type('text/html')
  return '<h1>Hello World</h1>'
})

4.3 Buffer响应 #

javascript
fastify.get('/buffer', async (request, reply) => {
  const buffer = Buffer.from('Hello Buffer')
  reply.type('application/octet-stream')
  return buffer
})

4.4 Stream响应 #

javascript
const fs = require('fs')
const path = require('path')

fastify.get('/stream', async (request, reply) => {
  const stream = fs.createReadStream(path.join(__dirname, 'file.txt'))
  reply.type('text/plain')
  return stream
})

4.5 空响应 #

javascript
fastify.get('/empty', async (request, reply) => {
  reply.code(204).send()
})

fastify.get('/no-content', async (request, reply) => {
  reply.code(204)
  return reply.send()
})

五、Content-Type #

5.1 设置Content-Type #

javascript
fastify.get('/json-type', async (request, reply) => {
  reply.type('application/json')
  return { message: 'JSON' }
})

fastify.get('/html-type', async (request, reply) => {
  reply.type('text/html')
  return '<h1>HTML</h1>'
})

fastify.get('/xml-type', async (request, reply) => {
  reply.type('application/xml')
  return '<root><message>XML</message></root>'
})

fastify.get('/csv-type', async (request, reply) => {
  reply.type('text/csv')
  return 'name,email\nJohn,john@example.com'
})

5.2 字符集设置 #

javascript
fastify.get('/charset', async (request, reply) => {
  reply.type('text/html; charset=utf-8')
  return '<h1>UTF-8 Content</h1>'
})

六、重定向 #

6.1 基本重定向 #

javascript
fastify.get('/redirect', async (request, reply) => {
  reply.redirect('/target')
})

fastify.get('/target', async (request, reply) => {
  return { message: 'Redirected here' }
})

6.2 外部重定向 #

javascript
fastify.get('/external', async (request, reply) => {
  reply.redirect('https://example.com')
})

6.3 指定状态码 #

javascript
fastify.get('/permanent', async (request, reply) => {
  reply.redirect(301, '/new-location')
})

fastify.get('/temporary', async (request, reply) => {
  reply.redirect(307, '/temporary-location')
})

七、Cookie #

7.1 设置Cookie #

javascript
fastify.register(require('@fastify/cookie'))

fastify.get('/set-cookie', async (request, reply) => {
  reply.setCookie('sessionId', 'abc123', {
    httpOnly: true,
    secure: true,
    sameSite: 'strict',
    maxAge: 3600,
    path: '/'
  })
  
  return { message: 'Cookie set' }
})

7.2 Cookie选项 #

javascript
fastify.get('/cookie-options', async (request, reply) => {
  reply.setCookie('token', 'xyz789', {
    domain: 'example.com',
    path: '/',
    secure: true,
    httpOnly: true,
    sameSite: 'strict',
    expires: new Date(Date.now() + 3600000),
    maxAge: 3600
  })
  
  return { message: 'Cookie with options' }
})

7.3 清除Cookie #

javascript
fastify.get('/clear-cookie', async (request, reply) => {
  reply.clearCookie('sessionId', {
    path: '/'
  })
  
  return { message: 'Cookie cleared' }
})

7.4 签名Cookie #

javascript
fastify.register(require('@fastify/cookie'), {
  secret: 'my-secret-key'
})

fastify.get('/signed-cookie', async (request, reply) => {
  reply.setCookie('data', 'value', { signed: true })
  return { message: 'Signed cookie set' }
})

八、文件下载 #

8.1 发送文件 #

javascript
fastify.get('/download', async (request, reply) => {
  reply.download('/path/to/file.pdf')
})

fastify.get('/download-custom', async (request, reply) => {
  reply.download('/path/to/file.pdf', 'custom-name.pdf')
})

8.2 发送静态文件 #

javascript
fastify.register(require('@fastify/static'), {
  root: path.join(__dirname, 'public'),
  prefix: '/static/'
})

fastify.get('/file', async (request, reply) => {
  reply.sendFile('document.pdf')
})

九、响应序列化 #

9.1 Schema序列化 #

javascript
fastify.get('/users', {
  schema: {
    response: {
      200: {
        type: 'array',
        items: {
          type: 'object',
          properties: {
            id: { type: 'integer' },
            name: { type: 'string' }
          }
        }
      }
    }
  }
}, async (request, reply) => {
  return [
    { id: 1, name: 'John', password: 'secret' },
    { id: 2, name: 'Jane', password: 'secret' }
  ]
})

9.2 自定义序列化器 #

javascript
fastify.get('/custom-serialize', {
  serializerCompiler: ({ schema, method, url, httpStatus }) => {
    return (data) => JSON.stringify({
      success: true,
      data,
      timestamp: Date.now()
    })
  }
}, async (request, reply) => {
  return { message: 'Custom serialization' }
})

9.3 禁用序列化 #

javascript
fastify.get('/raw', async (request, reply) => {
  reply.serializer(null)
  return 'Raw response without serialization'
})

十、响应钩子 #

10.1 preSerialization #

javascript
fastify.addHook('preSerialization', async (request, reply, payload) => {
  if (payload && typeof payload === 'object') {
    return {
      ...payload,
      timestamp: Date.now()
    }
  }
  return payload
})

10.2 onSend #

javascript
fastify.addHook('onSend', async (request, reply, payload) => {
  reply.header('X-Response-Time', Date.now() - request.startTime)
  return payload
})

十一、响应装饰 #

11.1 装饰Reply #

javascript
fastify.decorateReply('success', function (data) {
  return this.send({
    success: true,
    data,
    timestamp: new Date().toISOString()
  })
})

fastify.get('/success', async (request, reply) => {
  reply.success({ message: 'Hello' })
})

11.2 自定义响应方法 #

javascript
fastify.decorateReply('paginated', function (items, total, page, limit) {
  return this.send({
    success: true,
    data: items,
    pagination: {
      page,
      limit,
      total,
      pages: Math.ceil(total / limit)
    }
  })
})

fastify.get('/users', async (request, reply) => {
  const { page = 1, limit = 10 } = request.query
  const users = await getUsers(page, limit)
  const total = await countUsers()
  
  reply.paginated(users, total, page, limit)
})

十二、最佳实践 #

12.1 统一响应格式 #

javascript
fastify.decorateReply('success', function (data) {
  return this.send({
    success: true,
    data,
    timestamp: new Date().toISOString()
  })
})

fastify.decorateReply('error', function (message, code = 400) {
  return this.code(code).send({
    success: false,
    error: message,
    timestamp: new Date().toISOString()
  })
})

fastify.get('/success-response', async (request, reply) => {
  reply.success({ users: [] })
})

fastify.get('/error-response', async (request, reply) => {
  reply.error('User not found', 404)
})

12.2 分页响应 #

javascript
fastify.decorateReply('paginate', function (items, total, page, limit) {
  return this.send({
    success: true,
    data: items,
    meta: {
      pagination: {
        page: parseInt(page),
        limit: parseInt(limit),
        total,
        totalPages: Math.ceil(total / limit)
      }
    }
  })
})

12.3 条件响应 #

javascript
fastify.get('/conditional', async (request, reply) => {
  const etag = 'abc123'
  const lastModified = new Date().toUTCString()
  
  reply.header('ETag', etag)
  reply.header('Last-Modified', lastModified)
  
  if (request.headers['if-none-match'] === etag) {
    reply.code(304).send()
    return
  }
  
  return { message: 'Fresh content' }
})

十三、总结 #

本章我们学习了:

  1. 状态码:设置状态码、状态码别名
  2. 响应头:设置、删除、获取响应头
  3. 响应体:JSON、字符串、Buffer、Stream
  4. Content-Type:设置类型、字符集
  5. 重定向:基本重定向、外部重定向
  6. Cookie:设置、清除、签名Cookie
  7. 文件下载:发送文件、静态文件
  8. 响应序列化:Schema序列化、自定义序列化
  9. 响应钩子:preSerialization、onSend
  10. 响应装饰:自定义响应方法
  11. 最佳实践:统一格式、分页响应、条件响应

接下来让我们学习数据验证!

最后更新:2026-03-28