You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.20 13:06:29