30歳からのプログラミング

30歳無職から独学でプログラミングを開始した人間の記録。

フォーム送信時の HTTP リクエストについて

この記事では、フォーム送信時に実際にどのようなデータが送信されるのか、ファイルを送信するためにはどうすればよいのか、などを Deno や curl で確認しながら見ていく。

動作確認に使った実行環境やツールのバージョンは以下の通り。

  • Google Chrome 85.0.4183.121
  • Deno 1.4.4
  • curl 7.54.0

サーバを立てる

まず、リクエストを受けるためのサーバを Deno で立てる。

// server.ts
import {
  listenAndServe,
  ServerRequest,
} from "https://deno.land/std@0.74.0/http/mod.ts";
import { bold } from "https://deno.land/std@0.74.0/fmt/colors.ts";

const getRouting = async (req: ServerRequest) => {
  switch (req.url) {
    case "/": {
      const html = await Deno.readTextFile("./form.html");
      req.respond({
        status: 200,
        headers: new Headers({
          "content-type": "text/html",
        }),
        body: html,
      });
      break;
    }
    default:
      req.respond({
        status: 404,
        headers: new Headers({
          "content-type": "text/plain",
        }),
        body: "Not found\n",
      });
      break;
  }
};

const postRouting = (req: ServerRequest) => {
  switch (req.url) {
    case "/":
      req.respond({
        status: 200,
        headers: new Headers({
          "content-type": "text/plain",
        }),
        body: "Your post was successful\n",
      });
      break;
    default:
      req.respond({
        status: 404,
        headers: new Headers({
          "content-type": "text/plain",
        }),
        body: "Not found\n",
      });
      break;
  }
};

listenAndServe({ port: 8080 }, async (req: ServerRequest) => {
  console.log(`Request Method -> ${req.method}`);

  req.headers.forEach((value, key) => {
    console.log(`${bold(key)}: ${value}`);
  });

  const decoder = new TextDecoder("utf-8");
  const body = await Deno.readAll(req.body);
  console.log("Request Body");
  console.log(decoder.decode(body));

  switch (req.method) {
    case "GET":
      getRouting(req);
      break;
    case "POST":
      postRouting(req);
      break;
    default:
      req.respond({
        status: 405,
      });
  }
});

form.htmlの内容は以下の通り。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Form</title>
</head>
<body>
  <form method="POST" action="http://localhost:8080/">
    <label for="task">task: </label><input type="text" name="task" id="task"><br>
    <label for="priority">priority: </label><input type="text" name="priority" id="priority"><br>
    <p><input type="submit" value="submit"></p>
  </form>
</body>
</html>

deno run --allow-net --allow-read server.tsを実行すると、サーバが起動する。

URL エンコード

http://localhost:8080/にアクセスするとフォームが表示されるので、tasklearn JS & TSpriorityhighを入力して、送信してみる。
すると、以下の内容がログに表示される。

Request Method -> POST
host: localhost:8080
connection: keep-alive
content-length: 34
cache-control: max-age=0
upgrade-insecure-requests: 1
origin: http://localhost:8080
content-type: application/x-www-form-urlencoded
user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36
accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
sec-fetch-site: same-origin
sec-fetch-mode: navigate
sec-fetch-user: ?1
sec-fetch-dest: document
referer: http://localhost:8080/
accept-encoding: gzip, deflate, br
accept-language: ja,en-US;q=0.9,en;q=0.8
Request Body
task=learn+JS+%26+TS&priority=high

最後のtask=learn+JS+%26+TS&priority=highがリクエストボディ。
各項目はキーと値が=で結合され、複数の項目がある場合は&で結合されて、一つの文字列になっている。
キーと値に含まれる&%26に変換されるため、区切り文字の&と混同されることはない。
今回の例だとスペースも+に変換されている。

learn JS & TS
↓ エンコード
learn+JS+%26+TS

フォーム送信と同様のリクエストを curl で発行するには、-dもしくは--data-urlencodeを使う。
それぞれのフラグの後ろにキー="値"という形で、送信したいデータを指定する。

まずは-dで送信してみる。

$ curl -d task="learn JS & TS" -d priority="high" http://localhost:8080
Request Method -> POST
host: localhost:8080
user-agent: curl/7.54.0
accept: */*
content-length: 32
content-type: application/x-www-form-urlencoded
Request Body
task=learn JS & TS&priority=high

無事に POST メソッドのリクエストを発行できた。
だがよく見てみると、スペースや&がエンコードされず、そのまま送信されてしまっている。

フォームでの送信と同様に自動的にエンコードしてもらいたい場合は、-dの代わりに--data-urlencodeを使う。

$ curl --data-urlencode task="learn JS & TS" --data-urlencode priority="high" http://localhost:8080
Request Method -> POST
host: localhost:8080
user-agent: curl/7.54.0
accept: */*
content-length: 40
content-type: application/x-www-form-urlencoded
Request Body
task=learn%20JS%20%26%20TS&priority=high

&%26にエンコードされているのはフォームの例と同じだが、スペースは+ではなく%20になっている。

これは、encodeURIComponentと同じ挙動。

console.log(encodeURIComponent("learn JS & TS")) // "learn%20JS%20%26%20TS"

このように、クライアントによってエンコードのロジックが異なる。

multipart/form-data

ここまでの例では、リクエストヘッダのcontent-typeは全てapplication/x-www-form-urlencodedだった。
この他に、multipart/form-dataもある。

form要素のenctype属性にmultipart/form-dataを指定すると、content-typemultipart/form-dataになる。

@@ -5,7 +5,7 @@
   <title>Form</title>
 </head>
 <body>
-  <form method="POST" action="http://localhost:8080/">
+  <form method="POST" action="http://localhost:8080/" enctype="multipart/form-data">
     <label for="task">task: </label><input type="text" name="task" id="task"><br>
     <label for="priority">priority: </label><input type="text" name="priority" id="priority"><br>
     <p><input type="submit" value="submit"></p>

先程と同じ値をフォームに入力して、送信してみる。
すると、リクエストは以下のような内容になった。

(中略)
content-type: multipart/form-data; boundary=----WebKitFormBoundaryKqKCxRXPgWo8hEBw
(中略)
Request Body
------WebKitFormBoundaryKqKCxRXPgWo8hEBw
Content-Disposition: form-data; name="task"

learn JS & TS
------WebKitFormBoundaryKqKCxRXPgWo8hEBw
Content-Disposition: form-data; name="priority"

high
------WebKitFormBoundaryKqKCxRXPgWo8hEBw--

content-typeboundary属性が設定されており、その値は----WebKitFormBoundaryKqKCxRXPgWo8hEBwになっている。
そしてボディでは、この文字列で、各項目を区切っている。

curl で同様のことを行うには、-dの代わりに-Fを使えばよい。

$ curl -F task="learn JS & TS" -F priority="high" http://localhost:8080

ファイルを送信したい場合は、multipart/form-dataを使う必要がある。
application/x-www-form-urlencodedだと、ファイルの情報を送信することができない。

例えば、フォームの内容を以下のように変える。

  <form method="POST" action="http://localhost:8080/">
    <input type="file" name="item" id="item"><br>
    <p><input type="submit" value="submit"></p>
  </form>

そして、以下の内容のtest.txtを送信する。

a
b

そうすると、リクエストボディは以下のようになる。

item=test.txt

name属性の値=ファイル名という情報しか存在せず、ファイルの内容を取得できない。

multipart/form-dataを使えば、ファイルの内容を取得できるようになる。

-  <form method="POST" action="http://localhost:8080/">
+  <form method="POST" action="http://localhost:8080/" enctype="multipart/form-data">
     <input type="file" name="item" id="item"><br>
     <p><input type="submit" value="submit"></p>
   </form>
------WebKitFormBoundaryIFek0BpUCfkWvtrd
Content-Disposition: form-data; name="item"; filename="test.txt"
Content-Type: text/plain

a
b

------WebKitFormBoundaryIFek0BpUCfkWvtrd--

ファイルの内容の他、そのファイル自身のContent-Typeなども取得できている。

curl で-Fオプションを使いながらファイル送信を行うには、attachment-file=@ファイルパスとすればよい。

curl -F attachment-file=@test.txt http://localhost:8080/

参考資料