Next.js API路由:请求唯一标识与响应压缩中间件实现方案
在Next.js中间件中实现请求ID注入与响应压缩
一、给每个请求加唯一ID并关联Pino日志
1. 中间件生成并传递Request ID
在项目根目录创建middleware.ts,生成UUID作为请求ID,写入请求头和响应头,同时在中间件的Pino日志里带上这个ID,方便后续API路由复用:
import { NextResponse } from 'next/server'; import type { NextRequest } from 'next/server'; import pino from 'pino'; const logger = pino({ level: process.env.NODE_ENV === 'production' ? 'info' : 'debug', }); export function middleware(request: NextRequest) { // 生成唯一请求ID(Edge Runtime原生支持crypto.randomUUID) const requestId = crypto.randomUUID(); // 把ID写入请求头,让后续API路由能拿到 const modifiedReq = new NextRequest(request); modifiedReq.headers.set('x-request-id', requestId); // 中间件日志带上requestId logger.info({ requestId, path: request.nextUrl.pathname }, '请求已接收'); // 传递修改后的请求,同时给响应头也加上ID const response = NextResponse.next({ request: { headers: modifiedReq.headers } }); response.headers.set('x-request-id', requestId); return response; } // 只对API路由生效,避免影响静态资源 export const config = { matcher: '/api/:path*', };
2. API路由中用Request ID打日志
不管是App Router的Route Handler还是Pages Router的API路由,都可以从请求头取到x-request-id,注入到Pino的日志上下文里:
// App Router示例:app/api/files/route.ts import { NextRequest } from 'next/server'; import pino from 'pino'; export async function GET(request: NextRequest) { const requestId = request.headers.get('x-request-id') || 'unknown'; const logger = pino().child({ requestId }); // 业务逻辑:比如读取文件、处理数据 logger.info('开始处理文件请求'); // 返回响应时也带上ID return new Response(JSON.stringify({ message: '文件返回成功' }), { headers: { 'Content-Type': 'application/json', 'x-request-id': requestId, }, }); }
二、压缩返回的图片和文本文件
1. 文本类文件压缩(Gzip/Brotli)
利用Edge Runtime原生的CompressionStream,根据客户端支持的压缩格式(从Accept-Encoding头判断),对文本、JSON等内容进行压缩:
在middleware.ts里扩展响应处理逻辑:
export function middleware(request: NextRequest) { // 前面的requestId生成逻辑... const response = NextResponse.next({ request: { headers: modifiedReq.headers } }); response.headers.set('x-request-id', requestId); // 处理压缩逻辑 const acceptEncoding = request.headers.get('Accept-Encoding') || ''; let compressionStream: CompressionStream | null = null; let encoding: string | null = null; // 优先用Brotli,其次Gzip if (acceptEncoding.includes('br')) { compressionStream = new CompressionStream('br'); encoding = 'br'; } else if (acceptEncoding.includes('gzip')) { compressionStream = new CompressionStream('gzip'); encoding = 'gzip'; } // 只对文本类内容压缩 const contentType = response.headers.get('Content-Type') || ''; const shouldCompress = contentType.startsWith('text/') || contentType.includes('application/json'); if (compressionStream && shouldCompress) { const compressedBody = response.body?.pipeThrough(compressionStream); const compressedRes = new NextResponse(compressedBody, response); compressedRes.headers.set('Content-Encoding', encoding!); compressedRes.headers.delete('Content-Length'); // 压缩后长度变化,移除原长度头 compressedRes.headers.set('x-request-id', requestId); return compressedRes; } return response; }
2. 图片压缩
Edge Runtime不支持Sharp这类Node.js原生图片处理库,所以分两种场景处理:
场景1:API路由用Node.js Runtime处理(推荐)
如果你的API路由用Node.js Runtime(Pages Router默认是,App Router需要指定runtime: 'nodejs'),可以用Sharp压缩图片:
先安装Sharp:
npm install sharp
然后在API路由里实现:
// App Router示例:app/api/images/[name]/route.ts import { NextRequest } from 'next/server'; import sharp from 'sharp'; import fs from 'fs/promises'; import path from 'path'; // 指定用Node.js Runtime export const runtime = 'nodejs'; export async function GET(request: NextRequest, { params }: { params: { name: string } }) { const requestId = request.headers.get('x-request-id') || 'unknown'; const logger = pino().child({ requestId }); // 读取本地图片 const imagePath = path.join(process.cwd(), 'public', `${params.name}.jpg`); const imageBuffer = await fs.readFile(imagePath); // 压缩图片:JPEG质量设为70,可根据需求调整 const compressedBuffer = await sharp(imageBuffer) .jpeg({ quality: 70 }) .toBuffer(); logger.info('图片压缩完成'); return new Response(compressedBuffer, { headers: { 'Content-Type': 'image/jpeg', 'x-request-id': requestId, }, }); }
场景2:中间件用Edge兼容库压缩
如果必须在中间件处理,可以用@squoosh/lib的WebAssembly版本(注意Edge环境兼容性):
先安装依赖:
npm install @squoosh/lib
然后在middleware.ts里添加图片压缩逻辑:
export async function middleware(request: NextRequest) { // 前面的requestId和文本压缩逻辑... const response = await NextResponse.next({ request: { headers: modifiedReq.headers } }); response.headers.set('x-request-id', requestId); const contentType = response.headers.get('Content-Type'); if (contentType?.startsWith('image/')) { const imageBuffer = await response.arrayBuffer(); const imagePool = new ImagePool(); const image = imagePool.ingestImage(imageBuffer); // 根据图片类型设置压缩参数 if (contentType === 'image/jpeg') { await image.encode({ jpeg: { quality: 70 } }); } else if (contentType === 'image/png') { await image.encode({ oxipng: { level: 3 } }); } const encoded = await image.encodedWith; const compressedBuffer = encoded?.binary; if (compressedBuffer) { const compressedRes = new NextResponse(compressedBuffer, response); compressedRes.headers.set('x-request-id', requestId); compressedRes.headers.delete('Content-Length'); await imagePool.close(); return compressedRes; } } return response; }
关键注意点
- 中间件的
matcher要精准,只覆盖需要处理的API路由,避免浪费资源。 - 压缩时必须检查客户端的
Accept-Encoding头,不要返回客户端不支持的压缩格式。 - Request ID要同时出现在请求头和响应头,方便端到端追踪问题。
- 图片压缩参数要根据业务需求调整,平衡文件大小和画质。
内容的提问来源于stack exchange,提问作者EternalObserver
相关产品推荐
相关产品推荐

