🔖

form submission時の改行コード変換を考慮する

2022/02/20に公開

フロントエンドでの文字数バリデーションは通るのに、バックエンドでの文字数バリデーションは通らないという問題に遭遇しました。
原因はブラウザによって行われる改行コードの正規化でした。

問題が起きる仕組み

form submission時に、改行が \r\n に正規化されてバックエンドへ送信されます[1]
一方、textareaの値を取得すると、改行は \n に正規化されます[2]
その結果、フロントエンドでは95文字として見えていたのに、改行が10個あるせいでバックエンドでは105文字として見えるという事象が発生します。

解決策

案1. バックエンドで改行を正規化

状況ごとに解決方法は違うでしょうが、シンプルな解決策の1つは、バックエンドで改行を \n に正規化することです。
フロントエンドでのバリデーション時と同じ状況を作り出します。

案2. form submissionを使わない

fetchを使う案も考えられます。FormDataはform submissionになってしまうので使わない。
application/json だけでバックエンドとやりとりするようなアプリケーションも多いでしょうから、これで解決するケースも多いでしょう。
ファイルのアップロード機能などで multipart/form-data を使う場合は、そこに限定して異なる対処を行えば良いでしょう。

実験

以下のコードで、form submission時の改行の正規化の振る舞いを確認します。
textareaに改行付きで文章を入力してsubmitすると、ブラウザのコンソールには \n で正規化された文字列が表示され、nodeの標準出力には \r\n で正規化された文字列が表示されます。
一方、案2の方法でリクエストを送ると、nodeの標準出力には改行コードが \n の文字列が表示されます。

index.mjs
import http from 'http';
import fs from 'fs';

async function parseBody(req) {
  let body = '';
  for await (const chunk of req) {
    body += chunk;
  }
  return body;
}

const server = http.createServer(async (req, res) => {
  if (req.method === 'POST') {
    const body = await parseBody(req);
    console.log(JSON.stringify(decodeURIComponent(body)));
  }

  const html = fs.readFileSync('index.html');
  res.setHeader('Content-Type', 'text/html');
  res.end(html);
});

server.listen(8000);
index.html
<!DOCTYPE html>
<html>
  <body>
    <form action="/" method="post" id="form">
      <textarea name="content" id="textarea"></textarea>
      <button type="submit">submit</button>
    </form>
    <button id="fetch">use fetch</button>

    <script>
      document.getElementById("textarea").value = "a\rb\rc";

      document.getElementById("form").addEventListener("submit", (e) => {
        e.preventDefault();

        const textarea = document.getElementById("textarea");
        console.log(JSON.stringify(textarea.value));

        e.target.submit();
      });

      document.getElementById("fetch").addEventListener("click", () => {
        const textarea = document.getElementById("textarea");
        console.log(JSON.stringify(textarea.value));

        fetch("/", {
          method: "POST",
          body: textarea.value,
        });
      });
    </script>
  </body>
</html>

補足

以前はブラウザごとに挙動が違った

WHATWGのブログ記事にform submission時の改行コードの取り扱いに関するものがあります。
ブラウザごとに改行の正規化タイミングが異なっていたため、結果も違うものになってしまっていたそうです。
仕様を変更して振る舞いの統一を図った結果、現在はブラウザごとの差異は無くなっているようです。
https://blog.whatwg.org/newline-normalizations-in-form-submission

文字数はlengthで数えないほうが良い

JavaScriptでは文字列の長さをlengthでとろうとすると、誤った値が取得される場合があります。Sosuke Suzukiさんの記事が参考になります。
https://zenn.dev/sosukesuzuki/articles/d21d69a5914a03

疑問点

form submissionするケースで、改行コードを正規化できない、またはできるけどかなり面倒な場合はどうするか。悩み中。

例えば、Spring Bootで文字列長のバリデーション付きのハンドラを書くと以下のようになります。 \r\n はそのまま2文字としてカウントされてしまうのですが、事前に \n に正規化しようとするとかなり面倒です。

public void post(@Size(max = 100) String content) {
脚注
  1. https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#multipart-form-data ↩︎

  2. https://html.spec.whatwg.org/multipage/form-elements.html#the-textarea-element ↩︎

Discussion