nest中如何使用代理服务

一般使用nest时,在 main.ts 中都会有以下配置
js
async function bootstrap() { const app = await NestFactory.create<NestExpressApplication>( AppModule, new ExpressAdapter(), ); // ... }
当需要添加代理服务的时候,app.module.ts 添加以下代码
js
consumer .apply(ProxyMiddleware) .forRoutes({ path: 'api/proxy/*path', method: RequestMethod.ALL });
意思是 /api/proxy/ 下的所有路由都会走 ProxyMiddleware 中间件,内容为:
js
import { Injectable, NestMiddleware, Logger } from '@nestjs/common'; import { createProxyMiddleware, Options } from 'http-proxy-middleware'; import { Request, Response, NextFunction } from 'express'; import { ConfigService } from '@nestjs/config'; import type { RequestHandler } from 'express'; import { LoggerService } from 'src/global/logger/service'; @Injectable() export class ProxyMiddleware implements NestMiddleware { private proxyMiddleware!: RequestHandler; constructor( private configService: ConfigService, private logger: LoggerService, ) { this.initializeProxy(); } private initializeProxy() { // 从配置文件获取代理配置 const proxyConfig = this.configService.get('proxy') as ProxyConfig; console.log('代理配置:', proxyConfig); const { target, timeout, pathRewrite } = proxyConfig; const proxyOptions: Options = { target, changeOrigin: true, timeout, // 增加超时时间到60秒 proxyTimeout: 60000, // secure: true, // 验证SSL证书 followRedirects: true, pathRewrite, // 使用 on 对象配置事件监听器 on: { // 请求处理 proxyReq: (proxyReq, req, res) => { this.logger.log('onProxyReq 回调被执行!'); // 添加转发头信息 const clientIP = req.socket?.remoteAddress || (req.headers['x-forwarded-for'] as string) || 'unknown'; proxyReq.setHeader('X-Forwarded-For', clientIP); proxyReq.setHeader( 'X-Forwarded-Proto', req.headers['x-forwarded-proto'] || 'http', ); proxyReq.setHeader('X-Forwarded-Host', req.headers.host || 'unknown'); proxyReq.setHeader('X-Real-IP', clientIP); // 移除敏感头信息 proxyReq.removeHeader('cookie'); proxyReq.removeHeader('authorization'); // 记录请求日志 this.logger.log(`代理请求 ${req.method} ${req.url}${target}`); }, // 响应处理 proxyRes: (proxyRes, req, res) => { // 添加自定义响应头 proxyRes.headers['X-Proxy-By'] = 'NestJS-MiddleProxy'; proxyRes.headers['X-Proxy-Timestamp'] = new Date().toISOString(); // 移除敏感响应头 delete proxyRes.headers['set-cookie']; delete proxyRes.headers['server']; // 记录响应日志 this.logger.log( `代理响应: ${proxyRes.statusCode} 请求 ${req.method} ${req.url}`, ); }, // 错误处理 error: (err, req, res) => { this.logger.error( `代理错误 ${req.method} ${req.url}: ${err.message}`, err.stack, ); console.error('代理错误详情:', err); // 检查是否为 ServerResponse 类型 if ('headersSent' in res && !res.headersSent) { res.statusCode = 502; res.setHeader('Content-Type', 'application/json'); res.end( JSON.stringify({ error: '网关错误', message: `代理服务器错误: ${err.message}`, timestamp: new Date().toISOString(), path: req.url, }), ); } }, // WebSocket 代理错误处理 proxyReqWs: (proxyReq, req, socket) => { socket.on('error', (err) => { this.logger.error(`WebSocket 代理错误: ${err.message}`); }); }, }, // 健康检查 router: (req) => { // 可以根据请求动态选择目标服务器 return target; }, }; this.proxyMiddleware = createProxyMiddleware(proxyOptions); } use(req: Request, res: Response, next: NextFunction) { this.logger.log(`ProxyMiddleware被调用: ${req.method} ${req.url}`); try { // 请求验证 if (!this.isValidRequest(req)) { return res.status(400).json({ error: '请求错误', message: '无效的代理请求', timestamp: new Date().toISOString(), }); } // 速率限制检查(可以集成 Redis 实现分布式限流) if (!this.checkRateLimit(req)) { return res.status(429).json({ error: '请求过多', message: '超过速率限制', timestamp: new Date().toISOString(), }); } // 执行代理 this.proxyMiddleware(req, res, next); this.logger.log('代理中间件执行完成'); } catch (error) { const errorMessage = error instanceof Error ? error.message : '未知错误'; const errorStack = error instanceof Error ? error.stack : undefined; this.logger.error(`中间件错误: ${errorMessage}`, errorStack); if (!res.headersSent) { res.status(500).json({ error: '内部服务器错误', message: '代理中间件错误', timestamp: new Date().toISOString(), }); } } } private isValidRequest(req: Request): boolean { // 基本请求验证 if (!req.url || req.url.length > 2048) { return false; } // 检查是否包含恶意路径 const maliciousPatterns = ['../', '.\\', '<script', 'javascript:', 'data:']; const url = req.url.toLowerCase(); return !maliciousPatterns.some((pattern) => url.includes(pattern)); } private checkRateLimit(req: Request): boolean { // 简单的内存限流实现(生产环境建议使用 Redis) // 这里可以集成更复杂的限流逻辑 return true; } }
bash
# 代理配置 proxy: target: 'http://localhost:8020' timeout: 30000 retries: 3 pathRewrite: '^/api/proxy': '' rateLimit: windowMs: 60000 # 1分钟 maxRequests: 100 # 每分钟最多100个请求
以上代码就是一些代理的具体逻辑,当我去使用这个代理的时候,就一直报超时错误,原因我找了好久,原来是因为body-parser的问题,他是干嘛用的?
body-parser是 Node.js/Express 生态中的中间件,核心作用是解析 HTTP 请求体(Request Body)中的数据,并将解析后的数据挂载到 req.body 对象上,供后续路由 / 中间件直接使用。
简单来说,客户端(如浏览器、Postman)通过 POST/PUT 等请求发送数据(如 JSON、表单数据)时,这些数据会放在请求体里;而原生 Express 无法直接读取请求体,body-parser 就是帮你 “拆包” 并把数据整理成便于操作的格式。
Express 内置支持body-parser
js
// 替代 body-parser.json() app.use(express.json()); // 替代 body-parser.urlencoded() app.use(express.urlencoded({ extended: true }));
以下为nest中默认使用body-parser的解释
当使用express适配器时,NestJS应用程序将默认注册body-parser包中的json和urlencoded中间件。这意味着,如果您想通过MiddlewareConsumer自定义该中间件,您需要在使用NestFactory.create()创建应用程序时将全局中间件关闭,方法是将bodyParser标志设置为false。
https://nestjs.inode.club/middleware
问题就在这,http-proxy-middleware这个包在转发时,需要转发原数据结构,所以经过body-parser处理的body数据不能直接用于转发!
所以必须要关掉全局的body-parser,有了如下配置:
js
const app = await NestFactory.create<NestExpressApplication>( AppModule, new ExpressAdapter(), { bodyParser: false }, );
这时代理生效了,但是新的问题产生了,全局的去掉了,本服务之前定义的那些的post接口获取不到body了,怎么办?
需要自定义配置body-parser
方法如下:
修改 app.module.ts,添加多路由配置:
js
export class AppModule implements NestModule { configure(consumer: MiddlewareConsumer) { consumer .apply(ProxyMiddleware) .forRoutes({ path: 'api/proxy/*path', method: RequestMethod.ALL }); consumer .apply( JsonBodyParserMiddleware, UrlEncodedBodyParserMiddleware, ) .exclude({ path: 'api/proxy/*path', method: RequestMethod.ALL, }) .forRoutes('*'); } }
增加的JsonBodyParserMiddleware UrlEncodedBodyParserMiddleware 方法如下:
js
import { Injectable, NestMiddleware } from '@nestjs/common'; import { Request, Response, NextFunction } from 'express'; import * as express from 'express'; @Injectable() export class JsonBodyParserMiddleware implements NestMiddleware { use(req: Request, res: Response, next: NextFunction) { express.json()(req, res, next); } } @Injectable() export class UrlEncodedBodyParserMiddleware implements NestMiddleware { use(req: Request, res: Response, next: NextFunction) { express.urlencoded({ extended: true })(req, res, next); } }
新的问题出现,如果我想在代理中把请求的参数和相应的参数打印出来,该怎么做?
如果是get请求,很简单,直接打印url就好了,因为get的参数都在url上。
如果是post请求,就完了,因为代理中已经去掉了 body-parser ,所以想要获取body中的数据需要自己来实现:
js
proxyReq: (proxyReq, req, res) => { let body = ''; // 监听数据流 req.on('data', (chunk) => { body += chunk; }); // 数据接收完成 req.on('end', () => { console.log('响应体内容:', body); // 在这里可以对响应体进行处理 }); }
自己手撸的问题很多,具体问题是什么可以问问AI,总之需要一个现成的工具库来完成这个操作,这个库去哪里找?去 body-parser 的源码看看它的实现。
93cec24a-1276-4d7a-98ec-a1417ac92f72
可以去read文件找下对应的实现:
0e190b53-1897-4008-a1bb-9ccae1bdc9ef
这里的 getBody 方法就是获取body的核心方法
1a603f65-6375-46c6-80aa-d43bce624c3e
raw-body 就是解决body的第三方包,自己可以组装一个简单的解析body的工具函数
handleHttp.ts
js
import * as rawBody from 'raw-body'; import contentType from 'content-type'; // 辅助解析 Content-Type 的工具包(需安装) import { IncomingMessage } from 'http'; // 解析请求的 Content-Type 头 export function getCharset(req: IncomingMessage) { try { // 用 content-type 包解析头信息 const { parameters } = contentType.parse(req); // 返回 charset,默认 utf-8 return parameters.charset || 'utf-8'; } catch (err) { // 解析失败时默认用 utf-8 return 'utf-8'; } } // 使用示例 export async function handleRequest(req: IncomingMessage) { // 获取编码 const charset = getCharset(req); // 用获取到的编码解析请求体 const str = await rawBody(req, { encoding: charset }); return str; } export async function formatBody(req: IncomingMessage) { // 处理POST请求体 - 直接从原始请求流中读取 let bodyStr = 'null'; try { bodyStr = await handleRequest(req); bodyStr = JSON.stringify(JSON.parse(bodyStr)); } catch (error) { console.log('formatBody-error', error); } return { bodyStr, }; }
在代理中间件中直接使用 formatBody
js
{ // 请求处理 proxyReq: async (proxyReq, req, res) => { this.logger.log('onProxyReq 回调被执行!'); // 添加转发头信息 const clientIP = req.socket?.remoteAddress || (req.headers['x-forwarded-for'] as string) || 'unknown'; proxyReq.setHeader('X-Forwarded-For', clientIP); proxyReq.setHeader( 'X-Forwarded-Proto', req.headers['x-forwarded-proto'] || 'http', ); proxyReq.setHeader('X-Forwarded-Host', req.headers.host || 'unknown'); proxyReq.setHeader('X-Real-IP', clientIP); // 移除敏感头信息 proxyReq.removeHeader('cookie'); proxyReq.removeHeader('authorization'); // 处理POST请求体 - 直接从原始请求流中读取 formatBody(req) .then((body) => { const { bodyStr } = body; // 记录请求日志 this.logger.log( `代理请求 ${req.method} ${req.url}${target}; body: ${bodyStr};`, ); }) .catch((err) => { this.logger.error( `代理请求 ${req.method} ${req.url}${target} 失败; body: ${err.message};`, ); }); }, // 响应处理 proxyRes: (proxyRes, req, res) => { // 添加自定义响应头 proxyRes.headers['X-Proxy-By'] = 'NestJS-MiddleProxy'; proxyRes.headers['X-Proxy-Timestamp'] = new Date().toISOString(); // 移除敏感响应头 delete proxyRes.headers['set-cookie']; delete proxyRes.headers['server']; formatBody(proxyRes) .then((body) => { const { bodyStr } = body; // 记录请求日志 this.logger.log( `代理响应 ${proxyRes.statusCode} ${req.method} ${req.url} body: ${bodyStr};`, ); }) .catch((err) => { this.logger.error( `代理响应 ${proxyRes.statusCode} ${req.method} ${req.url} 失败; body: ${err.message};`, ); }); }, }
目前问题全部解决。
还有一种方法,就是全局开启 bodyParser
js
const app = await NestFactory.create<NestExpressApplication>( AppModule, new ExpressAdapter(), { bodyParser: true }, );
然后再转发的时候,把被转换的body还原回去。
但是,如果有的边界情况没考虑到,就可能导致转发的数据异常,进而影响业务,这个就得不偿失了。
所以我还是建议使用上面的那种方法,虽然是麻烦,但是不会影响业务,因为formatBody 只是消费了body,并没有修改它。