在 Express 中接收客户端提交的 JSON 或表单数据时,请求体解析是必经环节。body-parser 作为 Express 内置的解析中间件,默认把请求体大小限制在 100KB,如果客户端提交的数据超过这个阈值,服务端会直接抛出 413 Payload Too Large 错误。这个限制虽然避免了恶意大体积请求拖垮服务,但默认错误响应通常是 HTML 文本,对 API 调用方并不友好。本文围绕请求体大小限制与数据校验两条主线,结合可运行的中间件代码,帮助你在边界情况下返回结构化错误信息,并在数据进入业务逻辑之前完成完整校验。

一、请求体大小限制的底层机制与 limit 配置
Express 从 4.16 版本开始内置了 express.json、express.urlencoded、express.raw、express.text 四个解析中间件,它们都基于 body-parser 实现。这些中间件都会读取请求头中的 Content-Length 字段,并调用 raw-body 库把请求体读入内存。如果请求体长度超过 limit 选项所设置的值,raw-body 会触发 entity.too.large 错误,Express 将其包装为 413 状态码返回。默认情况下,四个解析器的 limit 均为 100kb,单位支持 b、kb、mb、gb 等,也可以直接传数字表示字节数。
配置非常简单。下面的代码把 JSON 请求体限制放宽到 1MB,并调整 urlencoded 解析器为 500KB。需要注意,limit 只是限制请求体大小,并不能限制请求头的大小,如果需要限制请求头,可以借助服务器层如 Nginx 的 client_header_buffer_size 或 Node.js 的 maxHeaderSize 配置。另外,当客户端使用分块传输编码且未提供 Content-Length 时,raw-body 会在数据累计到限制大小时主动中断连接,因此 limit 仍然有效。
const express = require('express');
const app = express();
// JSON 请求体最大 1MB
app.use(express.json({ limit: '1mb' }));
// 表单请求体最大 500KB,支持嵌套对象
app.use(express.urlencoded({ limit: '500kb', extended: true }));
app.post('/api/upload', function (req, res) {
res.json({ received: true, data: req.body });
});
app.listen(3000, function () {
console.log('Server running on port 3000');
});
为何不建议无限调大 limit?把限制放宽到 10MB 或 50MB 表面上看能解决大文件上传问题,但实际会显著增加内存占用。body-parser 将整个请求体读入内存后才解析,如果并发几十个请求都携带 10MB 数据,内存可能很快耗尽。对于大文件上传场景,建议使用 multer 等流式解析方案,或者走对象存储直传,而不是简单调大 body-parser 的 limit。
二、捕获 413 错误并统一响应结构
默认情况下,Express 收到超限的请求体后会走内置错误处理,返回一段 Content-Type 为 text/html 的响应,例如 PayloadTooLargeError: request entity too large。这个响应不适合前后端分离的接口设计。要让接口返回 JSON 结构,需要注册一个带有四个参数的错误处理中间件,并把它放在所有路由定义之后。
错误处理中间件的第一个参数是捕获到的错误对象。对于 body-parser 产生的超限错误,可以判断 err.type 是否等于 entity.too.large,或者更通用地判断 err.status 是否为 413。下面的示例同时处理了 JSON 解析失败的情况。JSON 语法错误时 err.type 通常为 entity.parse.failed,状态码为 400,也要一并转成统一格式。
const express = require('express');
const app = express();
app.use(express.json({ limit: '200kb' }));
app.post('/api/users', function (req, res) {
res.json({ ok: true, data: req.body });
});
// 统一错误处理中间件,必须放在路由之后
app.use(function (err, req, res, next) {
if (err.type === 'entity.too.large') {
return res.status(413).json({
code: 'PAYLOAD_TOO_LARGE',
message: '请求体超过 200KB 限制,请压缩数据后重试'
});
}
if (err instanceof SyntaxError && err.status === 400 && 'body' in err) {
return res.status(400).json({
code: 'INVALID_JSON',
message: 'JSON 格式错误,请检查请求体'
});
}
next(err);
});
app.listen(3000);
返回 413 状态码时,响应体已经是 JSON,前端可以在 axios 或 fetch 的 catch 中根据 response.data.code 做提示。另外,如果错误处理中间件内部出现新的错误,需要调用 next(err) 把错误继续抛给后续处理,避免请求挂起。对于权限、数据库等业务错误,也可以沿用这个统一错误处理模式,但需要额外判断错误类型。
三、请求体数据校验:express-validator 实战
大小限制只能挡住超大请求,不能保证请求体里的字段格式正确。例如接口要求 username 为 3 到 20 个字符,email 必须合法,age 必须为 0 到 120 的整数,这些规则需要在数据进入控制器之前完成校验。express-validator 是 Express 生态中最常用的校验中间件,底层基于验证库 validator.js,提供了链式 API,可直接生成中间件数组。
下面这段代码展示了用户注册接口的校验规则。先通过 body 函数提取请求体中的字段,然后依次调用 trim、isLength、isEmail、isInt、isArray 等验证器。withMessage 用于自定义错误提示。最后在真正的业务处理之前用一个自定义中间件收集 validationResult,如果存在错误就返回 422 状态码和结构化的错误数组。这样业务代码里拿到的 req.body 一定是已经通过基础校验的数据。
const express = require('express');
const { body, validationResult } = require('express-validator');
const app = express();
app.use(express.json());
const validateUser = [
body('username')
.trim()
.isLength({ min: 3, max: 20 })
.withMessage('用户名长度需在3到20之间'),
body('email')
.isEmail()
.withMessage('邮箱格式不正确')
.normalizeEmail(),
body('age')
.optional({ nullable: true })
.isInt({ min: 0, max: 120 })
.withMessage('年龄需为0-120的整数'),
body('roles')
.isArray({ min: 1 })
.withMessage('至少选择一个角色'),
body('address.city')
.notEmpty()
.withMessage('城市不能为空'),
function (req, res, next) {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(422).json({
code: 'VALIDATION_FAILED',
errors: errors.array()
});
}
next();
}
];
app.post('/api/users', validateUser, function (req, res) {
res.json({ ok: true, data: req.body });
});
app.listen(3000);
express-validator 的优势是侵入性小,可以直接作为路由级中间件插入,错误信息也能自由定制。不过对于嵌套层级较深的对象,链式调用会显得啰嗦,而且类型推导能力弱,在 TypeScript 项目中需要额外定义接口。因此如果项目已经使用 TypeScript,可以考虑下一节介绍的 Zod。
四、Joi 与 Zod 的替代方案及选型建议
如果不想为每个字段写中间件链,也可以使用 schema 校验库。Joi 是早期 Node.js 生态中最流行的对象校验工具,支持条件校验、自定义消息、异步校验等高级能力。Zod 则更现代,以静态类型推导为核心,非常适合 TypeScript 项目。两者都能在请求体进入业务层之前进行声明式校验,区别主要在 API 风格和类型支持上。
下面是 Joi 的示例。Joi 的 schema 用对象描述字段规则,validate 方法返回一个结果对象,通过判断 error 属性决定是否放行。Joi 的规则非常丰富,例如 when 可以做条件依赖,但包体积较大,且校验速度相对较慢,适合对校验规则要求非常复杂的场景。
const Joi = require('joi');
const userSchema = Joi.object({
username: Joi.string().min(3).max(20).required(),
email: Joi.string().email().required(),
age: Joi.number().integer().min(0).max(120).optional(),
roles: Joi.array().items(Joi.string()).min(1).required(),
address: Joi.object({
city: Joi.string().required()
}).required()
});
function validateBody(schema) {
return function (req, res, next) {
const { error, value } = schema.validate(req.body, { abortEarly: false });
if (error) {
return res.status(422).json({
code: 'VALIDATION_FAILED',
errors: error.details.map(function (item) {
return { field: item.path.join('.'), message: item.message };
})
});
}
req.body = value;
next();
};
}
// 使用
// app.post('/api/users', validateBody(userSchema), handler);
Zod 版本则更简洁,并且能通过 z.infer 自动推导出 TypeScript 类型。下面的示例定义了一个 userSchema,parse 方法在校验失败时会抛错,我们可以用 safeParse 方法替代以获取结果对象。在 Express 中一般用 safeParse 来返回结构化错误,而不是让异常冒泡到错误处理层。Zod 的包体积比 Joi 小,性能也更好,是当前新项目的优先选择。
const { z } = require('zod');
const userSchema = z.object({
username: z.string().min(3).max(20),
email: z.string().email(),
age: z.number().int().min(0).max(120).optional(),
roles: z.array(z.string()).min(1),
address: z.object({
city: z.string().min(1)
})
});
function validateBody(schema) {
return function (req, res, next) {
const result = schema.safeParse(req.body);
if (!result.success) {
return res.status(422).json({
code: 'VALIDATION_FAILED',
errors: result.error.issues.map(function (issue) {
return { path: issue.path.join('.'), message: issue.message };
})
});
}
req.body = result.data;
next();
};
}
// 使用
// app.post('/api/users', validateBody(userSchema), handler);
选型上没有绝对最优。如果团队以 JavaScript 为主且不想引入额外依赖,express-validator 最轻量直接;如果项目已经全面使用 TypeScript,Zod 能减少大量类型定义工作;如果业务中存在大量条件必填、字段依赖、动态对象等复杂规则,Joi 的 when、alternatives 等特性会更省心。无论选择哪种方案,都要把校验中间件放在大小限制中间件之后、业务路由处理之前,形成限制大小、解析请求体、校验数据、业务逻辑的清晰链路。