請求解析:Body Parser 的秘密
當客戶端發送 POST/PUT 請求時,資料通常放在 Body 中。如何正確解析這些資料?本篇將揭開 Body Parser 的神秘面紗。
一、 為什麼需要 Body Parser?
1.1 原始 HTTP Body
HTTP 請求的 Body 是原始位元組流:
http
POST /api/users HTTP/1.1
Host: example.com
Content-Type: application/json
Content-Length: 27
{"name":"John","age":30}Node.js 收到的是流,不是物件:
javascript
const server = http.createServer((req, res) => {
console.log(req.body); // undefined! Node.js 不會自動解析 Body
});1.2 手動解析
javascript
const server = http.createServer((req, res) => {
let body = "";
req.on("data", (chunk) => {
body += chunk.toString();
});
req.on("end", () => {
const data = JSON.parse(body); // 手動解析
console.log(data); // { name: 'John', age: 30 }
});
});問題:每個路由都要重複這段程式碼!
二、 Content-Type 決定解析方式
2.1 常見 Content-Type
| Content-Type | 說明 | 範例 |
|---|---|---|
application/json | JSON 格式 | API 請求 |
application/x-www-form-urlencoded | 表單格式 | HTML Form |
multipart/form-data | 多部份格式 | 檔案上傳 |
text/plain | 純文字 | 簡單文字 |
application/octet-stream | 二進位 | 檔案下載 |
2.2 Express 內建解析器
javascript
const express = require("express");
const app = express();
// JSON 解析
app.use(express.json());
// URL 編碼(表單)解析
app.use(express.urlencoded({ extended: true }));
app.post("/api/users", (req, res) => {
console.log(req.body); // 已解析!
});三、 JSON 解析
3.1 原理
javascript
// express.json() 的簡化實現
function jsonParser(req, res, next) {
if (req.headers["content-type"] !== "application/json") {
return next();
}
let body = "";
req.on("data", (chunk) => {
body += chunk.toString();
});
req.on("end", () => {
try {
req.body = JSON.parse(body);
next();
} catch (err) {
res.status(400).json({ error: "Invalid JSON" });
}
});
}3.2 選項
javascript
app.use(
express.json({
limit: "1mb", // 限制 Body 大小
strict: true, // 只接受陣列或物件
type: "application/json", // 指定 Content-Type
})
);3.3 安全考慮
javascript
app.use(
express.json({
limit: "100kb", // 防止大量資料攻擊
verify: (req, res, buf) => {
// 可在解析前驗證原始資料
req.rawBody = buf;
},
})
);四、 URL 編碼解析
4.1 格式
HTML 表單的預設格式:
http
POST /login HTTP/1.1
Content-Type: application/x-www-form-urlencoded
username=john&password=secret1234.2 解析
javascript
app.use(express.urlencoded({ extended: true }));
app.post("/login", (req, res) => {
console.log(req.body);
// { username: 'john', password: 'secret123' }
});4.3 extended 選項
| 選項 | 解析器 | 特性 |
|---|---|---|
extended: false | querystring | 簡單鍵值對 |
extended: true | qs | 支援嵌套物件 |
javascript
// extended: true 支援
// user[name]=John&user[age]=30
// => { user: { name: 'John', age: '30' } }五、 Multipart 解析(檔案上傳)
5.1 格式
http
POST /upload HTTP/1.1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary
------WebKitFormBoundary
Content-Disposition: form-data; name="file"; filename="photo.jpg"
Content-Type: image/jpeg
[二進位資料]
------WebKitFormBoundary
Content-Disposition: form-data; name="description"
My photo
------WebKitFormBoundary--5.2 使用 Multer
Express 不內建 multipart 解析,需用第三方套件:
javascript
const multer = require("multer");
// 記憶體儲存
const upload = multer({ storage: multer.memoryStorage() });
// 或檔案儲存
const diskUpload = multer({
dest: "uploads/",
limits: { fileSize: 5 * 1024 * 1024 }, // 5MB
});
// 單一檔案
app.post("/upload", upload.single("file"), (req, res) => {
console.log(req.file); // 檔案資訊
console.log(req.body); // 其他欄位
res.json({ success: true });
});
// 多個檔案
app.post("/gallery", upload.array("photos", 10), (req, res) => {
console.log(req.files); // 檔案陣列
});5.3 自訂儲存
javascript
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, "uploads/");
},
filename: (req, file, cb) => {
const uniqueName = `${Date.now()}-${file.originalname}`;
cb(null, uniqueName);
},
});
const upload = multer({
storage,
fileFilter: (req, file, cb) => {
const allowed = ["image/jpeg", "image/png", "image/gif"];
if (allowed.includes(file.mimetype)) {
cb(null, true);
} else {
cb(new Error("Invalid file type"));
}
},
});六、 串流處理
6.1 大檔案處理
不要把整個檔案載入記憶體:
javascript
const fs = require("fs");
const path = require("path");
const Busboy = require("busboy");
app.post("/upload-large", (req, res) => {
const busboy = Busboy({ headers: req.headers });
busboy.on("file", (name, file, info) => {
const saveTo = path.join("uploads", info.filename);
const writeStream = fs.createWriteStream(saveTo);
file.pipe(writeStream);
file.on("end", () => {
console.log(`File [${info.filename}] done`);
});
});
busboy.on("finish", () => {
res.json({ success: true });
});
req.pipe(busboy);
});6.2 進度追蹤
javascript
app.post("/upload", (req, res) => {
const totalSize = parseInt(req.headers["content-length"], 10);
let uploaded = 0;
req.on("data", (chunk) => {
uploaded += chunk.length;
const progress = ((uploaded / totalSize) * 100).toFixed(1);
console.log(`Progress: ${progress}%`);
});
// ... 處理上傳
});七、 錯誤處理
7.1 解析錯誤
javascript
app.use(express.json());
// 處理 JSON 解析錯誤
app.use((err, req, res, next) => {
if (err instanceof SyntaxError && "body" in err) {
return res.status(400).json({ error: "Invalid JSON" });
}
next(err);
});7.2 大小限制
javascript
app.use(express.json({ limit: "10kb" }));
app.use((err, req, res, next) => {
if (err.type === "entity.too.large") {
return res.status(413).json({ error: "Payload too large" });
}
next(err);
});7.3 Multer 錯誤
javascript
app.post("/upload", (req, res, next) => {
upload.single("file")(req, res, (err) => {
if (err instanceof multer.MulterError) {
if (err.code === "LIMIT_FILE_SIZE") {
return res.status(400).json({ error: "File too large" });
}
return res.status(400).json({ error: err.message });
} else if (err) {
return res.status(400).json({ error: err.message });
}
// 正常處理
res.json({ file: req.file });
});
});八、 安全最佳實踐
8.1 限制大小
javascript
// 全域限制
app.use(express.json({ limit: "100kb" }));
app.use(express.urlencoded({ limit: "100kb", extended: true }));
// 特定路由不同限制
app.post("/api/import", express.json({ limit: "10mb" }), handler);8.2 驗證 Content-Type
javascript
function requireJSON(req, res, next) {
if (!req.is("application/json")) {
return res.status(415).json({
error: "Content-Type must be application/json",
});
}
next();
}
app.post("/api/data", requireJSON, (req, res) => {
// ...
});8.3 檔案類型驗證
javascript
const upload = multer({
fileFilter: (req, file, cb) => {
// 不要只信任 mimetype,檢查副檔名
const allowed = [".jpg", ".jpeg", ".png", ".gif"];
const ext = path.extname(file.originalname).toLowerCase();
if (!allowed.includes(ext)) {
return cb(new Error("Invalid file extension"));
}
cb(null, true);
},
});九、 自定義解析器
9.1 XML 解析
javascript
const xml2js = require("xml2js");
function xmlParser(req, res, next) {
if (!req.is("application/xml") && !req.is("text/xml")) {
return next();
}
let body = "";
req.on("data", (chunk) => (body += chunk));
req.on("end", async () => {
try {
req.body = await xml2js.parseStringPromise(body);
next();
} catch (err) {
res.status(400).json({ error: "Invalid XML" });
}
});
}
app.use(xmlParser);9.2 原始 Body
javascript
app.use(express.raw({ type: "application/octet-stream" }));
app.post("/binary", (req, res) => {
console.log(req.body); // Buffer
});總結
| 格式 | 解析器 | 使用場景 |
|---|---|---|
| JSON | express.json() | API 請求 |
| URL 編碼 | express.urlencoded() | HTML 表單 |
| Multipart | multer | 檔案上傳 |
| Raw | express.raw() | 二進位資料 |
| Text | express.text() | 純文字 |
> **安全提醒**:
- 一定要設定
limit防止 DoS - 上傳檔案要驗證類型和大小
- 不要信任
Content-Type,自己驗證
進階挑戰
- 實作一個支援斷點續傳的檔案上傳功能。
- 實作一個 CSV 解析中介軟體,自動將 CSV 轉為 JSON。
- 研究如何安全地處理 ZIP 檔案上傳(防止 zip bomb)。