跳至主要內容
Skip to content

請求解析: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/jsonJSON 格式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=secret123

4.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: falsequerystring簡單鍵值對
extended: trueqs支援嵌套物件
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
});

總結

格式解析器使用場景
JSONexpress.json()API 請求
URL 編碼express.urlencoded()HTML 表單
Multipartmulter檔案上傳
Rawexpress.raw()二進位資料
Textexpress.text()純文字

> **安全提醒**:

  • 一定要設定 limit 防止 DoS
  • 上傳檔案要驗證類型和大小
  • 不要信任 Content-Type,自己驗證

進階挑戰

  1. 實作一個支援斷點續傳的檔案上傳功能。
  2. 實作一個 CSV 解析中介軟體,自動將 CSV 轉為 JSON。
  3. 研究如何安全地處理 ZIP 檔案上傳(防止 zip bomb)。

延伸閱讀與資源