一般使用nest时,在 main.ts 中都会有以下配置
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(
AppModule,
new ExpressAdapter(),
);
// ...
}
当需要添加代理服务的时候,app.module.ts 添加以下代码
consumer
.apply(ProxyMiddleware)
.forRoutes({ path: 'api/proxy/*path', method: RequestMethod.ALL });
意思是 /api/proxy/
下的所有路由都会走 ProxyMiddleware 中间件,内容为:
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;
}
}
# 代理配置
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
// 替代 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
,有了如下配置:
const app = await NestFactory.create<NestExpressApplication>(
AppModule,
new ExpressAdapter(),
{ bodyParser: false },
);
这时代理生效了,但是新的问题产生了,全局的去掉了,本服务之前定义的那些的post接口获取不到body了,怎么办?
需要自定义配置body-parser
方法如下:
修改 app.module.ts,添加多路由配置:
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
方法如下:
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中的数据需要自己来实现:
proxyReq: (proxyReq, req, res) => {
let body = '';
// 监听数据流
req.on('data', (chunk) => {
body += chunk;
});
// 数据接收完成
req.on('end', () => {
console.log('响应体内容:', body);
// 在这里可以对响应体进行处理
});
}
自己手撸的问题很多,具体问题是什么可以问问AI,总之需要一个现成的工具库来完成这个操作,这个库去哪里找?去 body-parser
的源码看看它的实现。
可以去read文件找下对应的实现:
这里的 getBody 方法就是获取body的核心方法
raw-body
就是解决body的第三方包,自己可以组装一个简单的解析body的工具函数
handleHttp.ts
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
{
// 请求处理
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
const app = await NestFactory.create<NestExpressApplication>(
AppModule,
new ExpressAdapter(),
{ bodyParser: true },
);
然后再转发的时候,把被转换的body还原回去。
但是,如果有的边界情况没考虑到,就可能导致转发的数据异常,进而影响业务,这个就得不偿失了。
所以我还是建议使用上面的那种方法,虽然是麻烦,但是不会影响业务,因为formatBody
只是消费了body,并没有修改它。