記憶力が無い

プログラミングと室内園芸と何か

HTTPのPOSTリクエストでコネクションがリセットされる問題について

久しぶりに投稿です。 いつの間にか就職して3カ月が経っていました。 いまはエンジニア見習いとして日々勉強中です。

はじめに

会社では開発の練習をしています。 tomcat上に簡単なWEBサービスを構築しています。 そこでぶち当たったある問題について軽くまとめます。


何が問題なのか

とりあえずコードを載せます。 このコードは次のサイトのものを拝借させていただきました。 Java/Tomcat/ファイルをアップロードするサンプル(Commons FileUplaod) - ちゃぱてぃ商店IT部 @ ウィキ - アットウィキ

アップロード画面表示用のプログラム
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
  <title>[Tomcat] ファイルアップロードサンプル。</title>
</head>
<body>
  <h1>[Tomcat] ファイルアップロードサンプル。</h1>
  <form method="POST" enctype="multipart/form-data" action="up">
  ファイル : <input type="file" name="upfile"><br/>
  メモ : <input type="text" name="comment"><br/>
  <br/>
  <input type="submit" value="Press"> ファイルをアップロードします!
  </form>
</body>
</html>
ファイル受信処理用プログラム
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.List;
 
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
 
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
 
/**
 * [Tomcat] ファイルアップロードサンプル。
 */
public class FileUploadSample extends HttpServlet {
  // (1) ファイルアップロードする時は、doPostメソッドを使います。
  @Override
  protected void doPost(HttpServletRequest request, HttpServletResponse response)
      throws ServletException, IOException {
    // (2) アップロードファイルを受け取る準備
    // ディスク領域を利用するアイテムファクトリーを作成
    DiskFileItemFactory factory = new DiskFileItemFactory();
 
    // tempディレクトリをアイテムファクトリーの一次領域に設定
    ServletContext servletContext = this.getServletConfig().getServletContext();
    factory.setRepository((File) servletContext.getAttribute("javax.servlet.context.tempdir"));
 
    // ServletFileUploadを作成
    ServletFileUpload upload = new ServletFileUpload(factory);
 
    try {
      // (3) リクエストをファイルアイテムのリストに変換
      List<FileItem> items = upload.parseRequest(request);
 
      // アップロードパス取得
      String upPath = servletContext.getRealPath("/") + "upload/";
      byte[] buff = new byte[1024];
      int size = 0;
 
      for (FileItem item : items) {
        // (4) アップロードファイルの処理
        if (!item.isFormField()) {
          // ファイルをuploadディレクトリに保存
          BufferedInputStream in = new BufferedInputStream(item.getInputStream());
          File f = new File(upPath + item.getName());
          BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(f));
          while ((size = in.read(buff)) > 0) {
            out.write(buff, 0, size);
          }
          out.close();
          in.close();
 
          // アップロードしたファイルへのリンクを表示
          response.getWriter().print("<a href='");
          response.getWriter().print(servletContext.getContextPath() + "/upload/" + item.getName());
          response.getWriter().print("'>" + item.getName() + "</a>");
 
          // (5) フォームフィールド(ファイル以外)の処理
        } else {
          // ここでは処理せず、直接requestからgetParamしてもいいと思います。
        }
      }
    } catch (FileUploadException e) {
      // 例外処理
    }
    response.getWriter().flush();
  }
}

これを実際に動かしてみます。(動かし方については今回は省略します) WEBブラウザからアクセスすると次のような画面が表示されます。

これでサーバーにファイルをアップロードできるようになりました。 ここで問題なのは、このままではいくらでも大きいファイルがアップロードできてしまうことです。

実際にテスト環境構築用に使用したcentosのイメージファイルをアップロードしてみます。

ボタンをクリックしてしばらくすると、次の画面が表示されます。

700MBほどのサイズのファイルでしたが、問題なくアップロードできました。

様々な理由からアップロードできるファイルに上限を付けたいという要求が出てくると思います。 アップロードする前にjavascript等でファイルサイズのチェックを入れるのもいいかもしれません。 でもサーバー側でもチェックは必要でしょう。

上のFileUploadSample.javaの37行目に次の1行を追加することで受信するファイルのサイズ制限をすることができます。 この例ではファイルサイズは1024*1024B=1MBに制限されます。

upload.setFileSizeMax(1024*1024);

アップロードするファイルのサイズが1MBを超えると、FileSizeLimitExceededExceptionが発生しそれをキャッチすることで「ファイルサイズが上限を超えているよ(413エラー)」メッセージを表示できるはず...でした。 しかし、やってみると次のような表示になりうまくいきません。 コネクションがリセットされてしまいます。 そう、それが問題なのです。


なぜそうなるのか

やったこととしては、tcpdumpをとりwiresharkでパケットの内容を追っていくという作業です。 わかったこととしては、setFileSizeMaxで指定したサイズ分程度のパケットを受診した後にサーバーが[FIN, ACK]を返しているということです。

ここからは私の推測ですが、

  • ブラウザはPOSTメソッドでリクエストを送信中にサーバーから他のメッセージを受け付けない
  • サーバーはsetFileSizeMaxで指定したサイズ分程度のパケットを受診した後にリクエストのストリームをクローズする
  • ブラウザはリクエストが送信しきる前にクローズされたため、レスポンスのパケットを受診することなく接続が切れたものと判断してtcpコネクションをクローズする

参考になったstack overflowの記事を示しておきます。 How to cancel HTTP upload from data events?


対策

どうにかして、「接続がリセットされました」メッセージではなく「ファイルサイズが上限を超えているよ(413エラー)」メッセージを表示するようにできないかプログラムをいろいろいじってみたものの、うまくできませんでした。 http/1.1プロトコルの仕様上仕方ないのかもしれません。(上のstack overflowの記事ではRFC 2616 § 8.2.2にブラウザの動作が違反しているからだと主張されているようですが。) なので、javascriptのFILE API等を使って送信を開始する前に確認を入れるようにする方法が合理的かと思います。


対策その2

ここで終わると「なんだ、結局javascriptに頼るのかよ」と言われそうなので、javascriptを使わない解決の方法にも言及しておこうと思います。 実はhttp/2プロトコルではtcpコネクションをクローズすることなくリクエストストリームだけをクローズすることができるようです。

つまり、ブラウザはリクエストストリームがクローズされた後もレスポンスを受け取ることができるそうです。

そしてtomcatはhttp/2プロトコルに対応しています。 これは試してみるしかない。ということで実際にやってみました。

結果から言うと「ファイルサイズが上限を超えているよ(413エラー)」メッセージを表示させることができました。 このことについてはtomcatのhttp/2対応手順とともに後日まとめたいと思います。


最後に

内容が間違ってる、日本語が間違っている等の指摘ありましたらお願いします。 また他に解決策があるようであればぜひ教えてください。

Copyright © 2017 ttk1