Node.js GET/POST请求
查询参数、表单体、JSON 体、文件上传,以及两者的边界。
Key PointsGET 的参数在 URL 里,POST 的参数在请求体里——这是唯一的本质区别。
GET 取参数
JavaScript
const http = require('node:http');
const server = http.createServer((req, res) => {
const url = new URL(req.url, `http://${req.headers.host}`);
// /search?q=node&page=2
const q = url.searchParams.get('q') || '';
const page = Number(url.searchParams.get('page') || 1);
// 同名多值:?tag=a&tag=b
const tags = url.searchParams.getAll('tag');
res.setHeader('Content-Type', 'application/json; charset=utf-8');
res.end(JSON.stringify({ q, page, tags }));
});
server.listen(3000);Output
$ curl "http://127.0.0.1:3000/search?q=node&page=2&tag=a&tag=b"
{"q":"node","page":2,"tags":["a","b"]}POST 读请求体
JavaScript
function readBody(req, limit = 1024 * 1024) {
return new Promise((resolve, reject) => {
let size = 0;
const chunks = [];
req.on('data', (c) => {
size += c.length;
if (size > limit) {
reject(new Error('请求体过大'));
req.destroy();
return;
}
chunks.push(c);
});
req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
req.on('error', reject);
});
}
const server = http.createServer(async (req, res) => {
if (req.method === 'POST' && req.url === '/login') {
const raw = await readBody(req);
const ct = req.headers['content-type'] || '';
let data;
if (ct.includes('application/json')) {
data = JSON.parse(raw);
} else {
data = Object.fromEntries(new URLSearchParams(raw));
}
res.setHeader('Content-Type', 'application/json; charset=utf-8');
return res.end(JSON.stringify({ got: data }));
}
res.writeHead(404).end();
});Output
$ curl -X POST http://127.0.0.1:3000/login \
-H "Content-Type: application/json" \
-d '{"user":"tom"}'
{"got":{"user":"tom"}}
$ curl -X POST http://127.0.0.1:3000/login -d "user=tom&pwd=1"
{"got":{"user":"tom","pwd":"1"}}Always cap the body size. Without a limit a crafted 10 GB POST exhausts memory. Also set it at the reverse proxy (nginx
client_max_body_size).GET 还是 POST
| Aspect | GET | POST |
|---|---|---|
| 参数位置 | URL 查询串 | 请求体 |
| 可缓存 | 是 | 默认否 |
| 可收藏/分享 | 是 | 否 |
| 幂等 | 是 | 否 |
| 长度限制 | 浏览器约 2KB | 服务端配置决定 |
| 浏览器历史 | 留在历史里 | 不保留 |
Read with GET, mutate with POST/PUT/PATCH/DELETE. Making "delete" a GET link lets browser prefetch and crawlers trigger it — a real source of incidents.
文件上传
JavaScript
// 原生 multipart 解析很麻烦,用 multer
const multer = require('multer');
const upload = multer({ dest: 'uploads/', limits: { fileSize: 5 * 1024 * 1024 } });
app.post('/upload', upload.single('file'), (req, res) => {
res.json({ name: req.file.originalname, size: req.file.size });
});GET 取参数
JavaScript
const http = require('node:http');
const server = http.createServer((req, res) => {
const url = new URL(req.url, `http://${req.headers.host}`);
// /search?q=node&page=2
const q = url.searchParams.get('q') || '';
const page = Number(url.searchParams.get('page') || 1);
// 同名多值:?tag=a&tag=b
const tags = url.searchParams.getAll('tag');
res.setHeader('Content-Type', 'application/json; charset=utf-8');
res.end(JSON.stringify({ q, page, tags }));
});
server.listen(3000);Output
$ curl "http://127.0.0.1:3000/search?q=node&page=2&tag=a&tag=b"
{"q":"node","page":2,"tags":["a","b"]}POST 读请求体
JavaScript
function readBody(req, limit = 1024 * 1024) {
return new Promise((resolve, reject) => {
let size = 0;
const chunks = [];
req.on('data', (c) => {
size += c.length;
if (size > limit) {
reject(new Error('请求体过大'));
req.destroy();
return;
}
chunks.push(c);
});
req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
req.on('error', reject);
});
}
const server = http.createServer(async (req, res) => {
if (req.method === 'POST' && req.url === '/login') {
const raw = await readBody(req);
const ct = req.headers['content-type'] || '';
let data;
if (ct.includes('application/json')) {
data = JSON.parse(raw);
} else {
data = Object.fromEntries(new URLSearchParams(raw));
}
res.setHeader('Content-Type', 'application/json; charset=utf-8');
return res.end(JSON.stringify({ got: data }));
}
res.writeHead(404).end();
});Output
$ curl -X POST http://127.0.0.1:3000/login \
-H "Content-Type: application/json" \
-d '{"user":"tom"}'
{"got":{"user":"tom"}}
$ curl -X POST http://127.0.0.1:3000/login -d "user=tom&pwd=1"
{"got":{"user":"tom","pwd":"1"}}必须限制请求体大小。不设上限时,一个构造的 10GB POST 就能把内存打满。生产环境在反向代理层(nginx
client_max_body_size)也要设。GET 还是 POST
| 对比项 | GET | POST |
|---|---|---|
| 参数位置 | URL 查询串 | 请求体 |
| 可缓存 | 是 | 默认否 |
| 可收藏/分享 | 是 | 否 |
| 幂等 | 是 | 否 |
| 长度限制 | 浏览器约 2KB | 服务端配置决定 |
| 浏览器历史 | 留在历史里 | 不保留 |
查询用 GET,改数据用 POST/PUT/PATCH/DELETE。把「删除」做成 GET 链接,会被浏览器预取和爬虫误触发——这是真实事故的来源。
文件上传
JavaScript
// 原生 multipart 解析很麻烦,用 multer
const multer = require('multer');
const upload = multer({ dest: 'uploads/', limits: { fileSize: 5 * 1024 * 1024 } });
app.post('/upload', upload.single('file'), (req, res) => {
res.json({ name: req.file.originalname, size: req.file.size });
});