前言

前陣子開發時,遇到 Node.js 上傳檔案到某個 file storage 時的問題,後來深入研究了一下上傳檔案時的 Content-Type: multipart/form-data,順手記錄一下,也透過常用套件 multer, busboy 的原始碼來驗證這些機制的存在。

何謂 boundary?

網路世界中,HTTP 協定定義了 Content-Type 標頭檔來規範資料傳遞的格式, 其中表單(Form)提交是很常見的情境,而表單提交的 Content-Type 主要有三種,並寫在 form 的 enctype 屬性裏頭:

  • application/x-www-form-urlencoded
  • multipart/form-data
  • text/plain

第一種 application/x-www-form-urlencoded 會被表單轉換成 url 帶上 query params 的格式,例如: ?name=Madi&age=20,使用 & 符號作為參數的分隔符(Delimiter)。

第二種 multipart/form-data 就是本篇文章要探討的主角,他也有類似 application/x-www-form-urlencoded 的分隔符 &,但是是使用 boundary 作為分隔符,在上傳檔案的 Request Header 都可以看見它的存在,而主要目的就是透過這個分隔符來區分不同格式的資料,例如圖片、檔案、影片…等等。

boundary 可以是任意符合 ascii-7 的字元,可以自己定義他的值,但開頭需要兩個 hyphen(連字號-),且長度須限制於 70 字元以內。當送出請求的 Content-Type 是 multipart/form-data時,強制規定必須帶上 boundary才能成功派送,但若是使用瀏覽器來發送,他會自己幫我們帶上一組 boundary。

例如,底下寫個簡單的 fetch,帶上 local 的某個文字檔

<script>
  const formData = new FormData();
  formData.append("name", "This is content of name");
  formData.append(
    "file",
    new File(["This is content of file"], "data.txt", { type: "text/plain" })
  );

  fetch("/upload", {
    method: "POST",
    body: formData,
  });
</script>

執行後,打開瀏覽器的 Network 頁籤就可以看到 Request Header 的 Content-Type 有 boundary 的蹤跡

boundary-content-type

打開 Payload 頁籤後可以看到表單提送的內容

boundary

其中檔案內容會以 binary 方式傳送 (這邊之所以看的到內容是因為我點選了瀏覽器上的 View Source 按鈕) 但每份內容之間是用 boundary 作為分隔的符號,而 server 就會依照該格式來解析傳遞來的檔案,完成以一筆 HTTP 請求傳遞多個不同格式的資料的需求。

Node.js 常用套件

前端上傳檔案時因為是夾帶在 form 內,瀏覽器會幫我們把這塊處理掉,但如果是在 Node.js 後端來上傳檔案到某個 file storage,就會無法使用原生的 fetch 和 FormData 物件(Node 18 版以前),這時候就會去 install 第三方實作的相關套件,例如: node-fetch, FormData, multer以及 busboy,但是使用上我們也不用自己處理 boundary 這個分割符,因為套件底層也都實做了這些機制了。

舉例來說:

FormData 原始碼的 這行

FormData.prototype._generateBoundary = function () {
  // This generates a 50 character boundary similar to those used by Firefox.
  // They are optimized for boyer-moore parsing.
  var boundary = "--------------------------";
  for (var i = 0; i < 24; i++) {
    boundary += Math.floor(Math.random() * 10).toString(16);
  }

  this._boundary = boundary;
};

node-fetch 原始碼的 這行

if (body instanceof FormData) {
  return `multipart/form-data; boundary=${request[INTERNALS].boundary}`;
}

multer 是 Nodejs 中上傳檔案很熱門的套件,其中的 這行 也看出 boundary 被處理掉了

req.headers = {
  "content-type": "multipart/form-data; boundary=" + form.getBoundary(),
  "content-length": length,
};

另一個用於處理檔案上傳的套件 busboy,其中的 這行 也把 boundary 處理掉了

function createMultipartBuffers(boundary, sizes) {
  const bufs = [];
  for (let i = 0; i < sizes.length; ++i) {
    const mb = sizes[i] * 1024 * 1024;
    bufs.push(
      Buffer.from(
        [
          `--${boundary}`,
          `content-disposition: form-data; name="field${i + 1}"`,
          "",
          "0".repeat(mb),
          "",
        ].join("\r\n")
      )
    );
  }
  bufs.push(Buffer.from([`--${boundary}--`, ""].join("\r\n")));
  return bufs;
}

總結

總結來說,boundary 是 HTTP 協定標頭檔 Content-Type 是multipart/form-data時傳遞不同格式的資料使用的一種分隔符,算是網際網路溝通上的內規,以便前後端在處理檔案時有個依據,知道如何解析這些資料流。這次透過研究檔案傳遞的流程來了解平常用套件時底層做的事情。