Lazy Diary @ Hatena Blog

PowerShell / Java / miscellaneous things about software development, Tips & Gochas. CC BY-SA 4.0/Apache License 2.0

Spring Frameworkのアップロードファイルサイズ制限のテスト

spring.servlet.multipart.max-file-size=100を設定した状態で、以下のようなControllerに様々なmultipart/form-dataリクエストを送信して結果を比較した。

@PostMapping("/upload")
public String handleFileUpload(@RequestParam("file") MultipartFile file, Model model,
        RedirectAttributes redirectAttributes) {

    System.out.println(file.getOriginalFilename());

    return "redirect:/";
}

これにより、以下のような不明点の調査を行った。

  • 大きいファイルをアップロードした際にJavaヒープがあふれないか?
  • 大きいファイルをアップロードした際のエラーハンドリングを個別のControllerで行えるか?
  • 大きいファイルをアップロードした際に、アップロードが終わるまでエラー処理には入らないのか?
  • ファイルをアップロードした際に一時ファイルが作られる場合、一時ファイルの削除などハウスキープ処理は必要か? 必要な場合、対象のディレクトリはどこか?
  • ファイルアップロードを行う場合、リクエストに必須のヘッダは何か?

1. ファイルサイズ上限以下のファイルを送信した場合

リクエス

リクエストはChromeのAdvanced REST clientで作成した。

POST /upload HTTP/1.1
HOST: localhost:8080
content-type: multipart/form-data; boundary=----WebKitFormBoundaryqx0dK5IFp8lW2Hjr
content-length: 419

------WebKitFormBoundaryqx0dK5IFp8lW2Hjr
Content-Disposition: form-data; name="hoge"; filename="blob"
Content-Type: text/plain

piyo
------WebKitFormBoundaryqx0dK5IFp8lW2Hjr
Content-Disposition: form-data; name="file"; filename="100.txt"
Content-Type: text/plain

1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890
------WebKitFormBoundaryqx0dK5IFp8lW2Hjr--

結果

  • 正常に処理され、HTTPレスポンス200が返る。
  • Controllerのメソッドが実行される(100.txtがコンソールに出力される)。
  • OSのテンポラリディレクトリ以下にtomcat.8080.6089307547608376372\work\Tomcat\localhost\ROOT\upload_68a5f1c0_fdc2_45d4_8705_18cd81766440_00000023.tmpのような一時ファイルが作られて、そこにファイルの内容が格納される。ファイルの内容はJavaヒープ上には読みこまれない*1
  • 一時ファイルは、リクエストの処理が完了して200レスポンスが返ると削除される。

分かったこと

  • file-size-thresholdがデフォルトなら、大きいファイルをアップロードされてJavaのヒープが溢れるといったことはなさそう。
  • 一時ファイルはリクエストが処理されたらすぐに削除されるので、ハウスキープ処理は基本的に必要なさそう。
  • 一時ファイルが作られる先は、OSのテンポラリディレクトリの下のtomcat.【ポート番号】.【Tomcatを立ち上げるたびに決まる乱数?】\work\Tomcat\【ホスト名】\以下。ファイル削除が必要な場合はそこを削除する。

2. ファイルサイズ上限を超えるファイルを送信した場合

リクエス

リクエストはChromeのAdvanced REST clientで作成した。

POST /upload HTTP/1.1
HOST: localhost:8080
content-type: multipart/form-data; boundary=----WebKitFormBoundaryoCEYONcK9AYZ4G7B
content-length: 420

------WebKitFormBoundaryoCEYONcK9AYZ4G7B
Content-Disposition: form-data; name="hoge"; filename="blob"
Content-Type: text/plain

piyo
------WebKitFormBoundaryoCEYONcK9AYZ4G7B
Content-Disposition: form-data; name="file"; filename="101.txt"
Content-Type: text/plain

12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901
------WebKitFormBoundaryoCEYONcK9AYZ4G7B--

結果

  • エラーとなり、HTTPレスポンス500が返る。
  • Controllerのメソッドは実行されない(Controllerまで処理が届かない)。
  • 標準出力へ以下のスタックトレースが出力される。

分かったこと

  • max-file-sizeによって発生したエラーは、Controllerではハンドリングできない。エラーメッセージをHTML中に含めて返したい、といった場合には使いにくそう。ファイルアップロードのリクエストはajaxで送るとかであれば大きな問題はないが……。

スタックトレース

org.apache.tomcat.util.http.fileupload.impl.FileSizeLimitExceededException: The field file exceeds its maximum permitted size of 100 bytes.
    at org.apache.tomcat.util.http.fileupload.impl.FileItemStreamImpl$1.raiseError(FileItemStreamImpl.java:114) ~[tomcat-coyote.jar:9.0.44]
    at org.apache.tomcat.util.http.fileupload.util.LimitedInputStream.checkLimit(LimitedInputStream.java:76) ~[tomcat-coyote.jar:9.0.44]
    at org.apache.tomcat.util.http.fileupload.util.LimitedInputStream.read(LimitedInputStream.java:135) ~[tomcat-coyote.jar:9.0.44]
    at java.base/java.io.FilterInputStream.read(FilterInputStream.java:107) ~[na:na]
    at org.apache.tomcat.util.http.fileupload.util.Streams.copy(Streams.java:98) ~[tomcat-coyote.jar:9.0.44]
    at org.apache.tomcat.util.http.fileupload.FileUploadBase.parseRequest(FileUploadBase.java:291) ~[tomcat-coyote.jar:9.0.44]
    at org.apache.catalina.connector.Request.parseParts(Request.java:2922) ~[catalina.jar:9.0.44]
    at org.apache.catalina.connector.Request.getParts(Request.java:2824) ~[catalina.jar:9.0.44]
    at org.apache.catalina.connector.RequestFacade.getParts(RequestFacade.java:1098) ~[catalina.jar:9.0.44]
    at org.springframework.web.multipart.support.StandardMultipartHttpServletRequest.parseRequest(StandardMultipartHttpServletRequest.java:95) ~[spring-web-5.3.9.jar:5.3.9]
    at org.springframework.web.multipart.support.StandardMultipartHttpServletRequest.<init>(StandardMultipartHttpServletRequest.java:88) ~[spring-web-5.3.9.jar:5.3.9]
    at org.springframework.web.multipart.support.StandardServletMultipartResolver.resolveMultipart(StandardServletMultipartResolver.java:122) ~[spring-web-5.3.9.jar:5.3.9]
    at org.springframework.web.servlet.DispatcherServlet.checkMultipart(DispatcherServlet.java:1202) ~[spring-webmvc-5.3.9.jar:5.3.9]
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1036) ~[spring-webmvc-5.3.9.jar:5.3.9]
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:963) ~[spring-webmvc-5.3.9.jar:5.3.9]
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006) ~[spring-webmvc-5.3.9.jar:5.3.9]
    at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:909) ~[spring-webmvc-5.3.9.jar:5.3.9]
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:652) ~[servlet-api.jar:4.0.FR]
    at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883) ~[spring-webmvc-5.3.9.jar:5.3.9]
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:733) ~[servlet-api.jar:4.0.FR]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:227) ~[catalina.jar:9.0.44]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[catalina.jar:9.0.44]
    at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53) ~[tomcat-websocket.jar:9.0.44]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[catalina.jar:9.0.44]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[catalina.jar:9.0.44]
    at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-5.3.9.jar:5.3.9]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.3.9.jar:5.3.9]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[catalina.jar:9.0.44]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[catalina.jar:9.0.44]
    at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[spring-web-5.3.9.jar:5.3.9]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.3.9.jar:5.3.9]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[catalina.jar:9.0.44]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[catalina.jar:9.0.44]
    at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[spring-web-5.3.9.jar:5.3.9]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.3.9.jar:5.3.9]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[catalina.jar:9.0.44]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[catalina.jar:9.0.44]
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202) ~[catalina.jar:9.0.44]
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97) ~[catalina.jar:9.0.44]
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:542) ~[catalina.jar:9.0.44]
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:143) ~[catalina.jar:9.0.44]
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92) ~[catalina.jar:9.0.44]
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78) ~[catalina.jar:9.0.44]
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:357) ~[catalina.jar:9.0.44]
    at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:374) ~[tomcat-coyote.jar:9.0.44]
    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65) ~[tomcat-coyote.jar:9.0.44]
    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:893) ~[tomcat-coyote.jar:9.0.44]
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1707) ~[tomcat-coyote.jar:9.0.44]
    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) ~[tomcat-coyote.jar:9.0.44]
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) ~[na:na]
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628) ~[na:na]
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) ~[tomcat-util.jar:9.0.44]
    at java.base/java.lang.Thread.run(Thread.java:834) ~[na:na]

3. リクエスト全体のContent-Lengthヘッダを省略し、ファイル側にContent-Lengthヘッダを指定した場合

リクエス

1.や2.のリクエストをもとに作成した以下のリクエストをtelnetで流しこんだ。

POST /upload HTTP/1.1
HOST: localhost:8080
content-type: multipart/form-data; boundary=----WebKitFormBoundaryoCEYONcK9AYZ4G7B

------WebKitFormBoundaryoCEYONcK9AYZ4G7B
Content-Disposition: form-data; name="hoge"; filename="blob"
Content-Type: text/plain

piyo
------WebKitFormBoundaryoCEYONcK9AYZ4G7B
Content-Disposition: form-data; name="file"; filename="101.txt"
Content-Type: text/plain
content-length: 100

12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901
------WebKitFormBoundaryoCEYONcK9AYZ4G7B--

結果

  • エラーとなり、HTTPレスポンス500が返る。
  • multipart/form-dataリクエストとして認識されない。
  • 標準出力へ以下のエラーが出力される。スタックトレースは出力されない。
2021-09-26 00:35:15.559  WARN 4920 --- [nio-8080-exec-8] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.multipart.support.MissingServletRequestPartException: Required request part 'file' is not present]

分かったこと

  • ファイルをアップロードする場合、リクエスト全体のContent-Lengthは必須。

4. リクエストの本当の長さより短かいContent-Lengthを指定した場合

リクエス

1.や2.のリクエストをもとに作成した以下のリクエストをtelnetで流しこんだ。

POST /upload HTTP/1.1
HOST: localhost:8080
content-type: multipart/form-data; boundary=----WebKitFormBoundaryqx0dK5IFp8lW2Hjr
content-length: 200

------WebKitFormBoundaryqx0dK5IFp8lW2Hjr
Content-Disposition: form-data; name="hoge"; filename="blob"
Content-Type: text/plain

piyo
------WebKitFormBoundaryqx0dK5IFp8lW2Hjr
Content-Disposition: form-data; name="file"; filename="100.txt"
Content-Type: text/plain

1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890
------WebKitFormBoundaryqx0dK5IFp8lW2Hjr--

結果

  • エラーとなり、HTTPレスポンス500が返る。
  • Controllerのメソッドは実行されない(Controllerまで処理が届かない)。
  • 6.や7.の例と異なり、telnetを実行しているコンソールに入力内容があふれることはない。
  • 標準出力へ以下のスタックトレースが出力される。

分かったこと

  • リクエスト全体のContent-Lengthを認識した上で、以降の内容の読み込み処理が行われている。

スタックトレース

org.apache.tomcat.util.http.fileupload.MultipartStream$MalformedStreamException: Stream ended unexpectedly
    at org.apache.tomcat.util.http.fileupload.MultipartStream$ItemInputStream.makeAvailable(MultipartStream.java:981) ~[tomcat-coyote.jar:9.0.44]
    at org.apache.tomcat.util.http.fileupload.MultipartStream$ItemInputStream.read(MultipartStream.java:879) ~[tomcat-coyote.jar:9.0.44]
    at java.base/java.io.FilterInputStream.read(FilterInputStream.java:133) ~[na:na]
    at org.apache.tomcat.util.http.fileupload.util.LimitedInputStream.read(LimitedInputStream.java:132) ~[tomcat-coyote.jar:9.0.44]
    at java.base/java.io.FilterInputStream.read(FilterInputStream.java:107) ~[na:na]
    at org.apache.tomcat.util.http.fileupload.util.Streams.copy(Streams.java:98) ~[tomcat-coyote.jar:9.0.44]
    at org.apache.tomcat.util.http.fileupload.FileUploadBase.parseRequest(FileUploadBase.java:291) ~[tomcat-coyote.jar:9.0.44]
    at org.apache.catalina.connector.Request.parseParts(Request.java:2922) ~[catalina.jar:9.0.44]
    at org.apache.catalina.connector.Request.getParts(Request.java:2824) ~[catalina.jar:9.0.44]
    at org.apache.catalina.connector.RequestFacade.getParts(RequestFacade.java:1098) ~[catalina.jar:9.0.44]
    at org.springframework.web.multipart.support.StandardMultipartHttpServletRequest.parseRequest(StandardMultipartHttpServletRequest.java:95) ~[spring-web-5.3.9.jar:5.3.9]
    at org.springframework.web.multipart.support.StandardMultipartHttpServletRequest.<init>(StandardMultipartHttpServletRequest.java:88) ~[spring-web-5.3.9.jar:5.3.9]
    at org.springframework.web.multipart.support.StandardServletMultipartResolver.resolveMultipart(StandardServletMultipartResolver.java:122) ~[spring-web-5.3.9.jar:5.3.9]
    at org.springframework.web.servlet.DispatcherServlet.checkMultipart(DispatcherServlet.java:1202) ~[spring-webmvc-5.3.9.jar:5.3.9]
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1036) ~[spring-webmvc-5.3.9.jar:5.3.9]
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:963) ~[spring-webmvc-5.3.9.jar:5.3.9]
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006) ~[spring-webmvc-5.3.9.jar:5.3.9]
    at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:909) ~[spring-webmvc-5.3.9.jar:5.3.9]
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:652) ~[servlet-api.jar:4.0.FR]
    at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883) ~[spring-webmvc-5.3.9.jar:5.3.9]
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:733) ~[servlet-api.jar:4.0.FR]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:227) ~[catalina.jar:9.0.44]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[catalina.jar:9.0.44]
    at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53) ~[tomcat-websocket.jar:9.0.44]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[catalina.jar:9.0.44]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[catalina.jar:9.0.44]
    at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-5.3.9.jar:5.3.9]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.3.9.jar:5.3.9]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[catalina.jar:9.0.44]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[catalina.jar:9.0.44]
    at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[spring-web-5.3.9.jar:5.3.9]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.3.9.jar:5.3.9]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[catalina.jar:9.0.44]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[catalina.jar:9.0.44]
    at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[spring-web-5.3.9.jar:5.3.9]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.3.9.jar:5.3.9]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[catalina.jar:9.0.44]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[catalina.jar:9.0.44]
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202) ~[catalina.jar:9.0.44]
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97) ~[catalina.jar:9.0.44]
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:542) ~[catalina.jar:9.0.44]
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:143) ~[catalina.jar:9.0.44]
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92) ~[catalina.jar:9.0.44]
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78) ~[catalina.jar:9.0.44]
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:357) ~[catalina.jar:9.0.44]
    at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:374) ~[tomcat-coyote.jar:9.0.44]
    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65) ~[tomcat-coyote.jar:9.0.44]
    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:893) ~[tomcat-coyote.jar:9.0.44]
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1707) ~[tomcat-coyote.jar:9.0.44]
    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) ~[tomcat-coyote.jar:9.0.44]
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) ~[na:na]
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628) ~[na:na]
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) ~[tomcat-util.jar:9.0.44]
    at java.base/java.lang.Thread.run(Thread.java:834) ~[na:na]

5. リクエスト全体とファイルの両方にContent-Lengthを指定した場合

リクエス

1.や2.のリクエストをもとに作成した以下のリクエストをtelnetで流しこんだ。

POST /upload HTTP/1.1
HOST: localhost:8080
content-type: multipart/form-data; boundary=----WebKitFormBoundaryqx0dK5IFp8lW2Hjr
content-length: 441

------WebKitFormBoundaryqx0dK5IFp8lW2Hjr
Content-Disposition: form-data; name="hoge"; filename="blob"
Content-Type: text/plain

piyo
------WebKitFormBoundaryqx0dK5IFp8lW2Hjr
Content-Disposition: form-data; name="file"; filename="100.txt"
Content-Type: text/plain
content-length: 100

1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890
------WebKitFormBoundaryqx0dK5IFp8lW2Hjr--

結果

  • 正常に処理され、HTTPレスポンス200が返る。
  • Controllerのメソッドが実行される(100.txtがコンソールに出力される)。

分かったこと

  • multipartのファイル部分に個別にContent-Lengthを指定しても問題ない。

6. ファイルのContent-Lengthをmax-file-sizeより長い値にした場合

リクエス

1.や2.のリクエストをもとに作成した以下のリクエストをtelnetで流しこんだ。ファイル本体の長さはmax-file-sizeを超えず、Content-Lengthだけがmax-file-sizeを超えた値になっている。

POST /upload HTTP/1.1
HOST: localhost:8080
content-type: multipart/form-data; boundary=----WebKitFormBoundaryqx0dK5IFp8lW2Hjr
content-length: 441

------WebKitFormBoundaryqx0dK5IFp8lW2Hjr
Content-Disposition: form-data; name="hoge"; filename="blob"
Content-Type: text/plain

piyo
------WebKitFormBoundaryqx0dK5IFp8lW2Hjr
Content-Disposition: form-data; name="file"; filename="100.txt"
Content-Type: text/plain
content-length: 101

123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789
------WebKitFormBoundaryqx0dK5IFp8lW2Hjr--

結果

  • エラーとなり、HTTPレスポンス500が返る。
  • Controllerのメソッドは実行されない(Controllerまで処理が届かない)。
  • リクエストのうち「45678901234567890123456789012345678901234567890123456789012345678901234567890123456789」はサーバへ送信されず、telnetを実行しているコンソールの標準出力にあふれる。
  • 標準出力へ以下のスタックトレースが出力される。

分かったこと

  • multipartのファイル部分に個別に指定したContent-Lengthを認識した上で以降の読み込み処理行われている。
  • リクエストを最後まで読んでからContent-Lengthを確認するのではなく、Content-Lengthを読み込んだ時点でmax-file-sizeのエラー処理に入っている。
  • ファイル個別のContent-Lengthの行が送られた後、すこし間があいてからエラー処理が行われるのは、リクエストを読み込むStreamのバッファのせい?

スタックトレース

org.apache.tomcat.util.http.fileupload.impl.FileSizeLimitExceededException: The field file exceeds its maximum permitted size of 100 bytes.
    at org.apache.tomcat.util.http.fileupload.impl.FileItemStreamImpl.<init>(FileItemStreamImpl.java:95) ~[tomcat-coyote.jar:9.0.44]
    at org.apache.tomcat.util.http.fileupload.impl.FileItemIteratorImpl.findNextItem(FileItemIteratorImpl.java:247) ~[tomcat-coyote.jar:9.0.44]
    at org.apache.tomcat.util.http.fileupload.impl.FileItemIteratorImpl.hasNext(FileItemIteratorImpl.java:297) ~[tomcat-coyote.jar:9.0.44]
    at org.apache.tomcat.util.http.fileupload.FileUploadBase.parseRequest(FileUploadBase.java:283) ~[tomcat-coyote.jar:9.0.44]
    at org.apache.catalina.connector.Request.parseParts(Request.java:2922) ~[catalina.jar:9.0.44]
    at org.apache.catalina.connector.Request.getParts(Request.java:2824) ~[catalina.jar:9.0.44]
    at org.apache.catalina.connector.RequestFacade.getParts(RequestFacade.java:1098) ~[catalina.jar:9.0.44]
    at org.springframework.web.multipart.support.StandardMultipartHttpServletRequest.parseRequest(StandardMultipartHttpServletRequest.java:95) ~[spring-web-5.3.9.jar:5.3.9]
    at org.springframework.web.multipart.support.StandardMultipartHttpServletRequest.<init>(StandardMultipartHttpServletRequest.java:88) ~[spring-web-5.3.9.jar:5.3.9]
    at org.springframework.web.multipart.support.StandardServletMultipartResolver.resolveMultipart(StandardServletMultipartResolver.java:122) ~[spring-web-5.3.9.jar:5.3.9]
    at org.springframework.web.servlet.DispatcherServlet.checkMultipart(DispatcherServlet.java:1202) ~[spring-webmvc-5.3.9.jar:5.3.9]
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1036) ~[spring-webmvc-5.3.9.jar:5.3.9]
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:963) ~[spring-webmvc-5.3.9.jar:5.3.9]
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006) ~[spring-webmvc-5.3.9.jar:5.3.9]
    at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:909) ~[spring-webmvc-5.3.9.jar:5.3.9]
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:652) ~[servlet-api.jar:4.0.FR]
    at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883) ~[spring-webmvc-5.3.9.jar:5.3.9]
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:733) ~[servlet-api.jar:4.0.FR]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:227) ~[catalina.jar:9.0.44]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[catalina.jar:9.0.44]
    at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53) ~[tomcat-websocket.jar:9.0.44]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[catalina.jar:9.0.44]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[catalina.jar:9.0.44]
    at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-5.3.9.jar:5.3.9]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.3.9.jar:5.3.9]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[catalina.jar:9.0.44]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[catalina.jar:9.0.44]
    at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[spring-web-5.3.9.jar:5.3.9]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.3.9.jar:5.3.9]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[catalina.jar:9.0.44]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[catalina.jar:9.0.44]
    at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[spring-web-5.3.9.jar:5.3.9]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.3.9.jar:5.3.9]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[catalina.jar:9.0.44]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[catalina.jar:9.0.44]
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202) ~[catalina.jar:9.0.44]
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97) ~[catalina.jar:9.0.44]
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:542) ~[catalina.jar:9.0.44]
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:143) ~[catalina.jar:9.0.44]
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92) ~[catalina.jar:9.0.44]
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78) ~[catalina.jar:9.0.44]
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:357) ~[catalina.jar:9.0.44]
    at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:374) ~[tomcat-coyote.jar:9.0.44]
    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65) ~[tomcat-coyote.jar:9.0.44]
    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:893) ~[tomcat-coyote.jar:9.0.44]
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1707) ~[tomcat-coyote.jar:9.0.44]
    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) ~[tomcat-coyote.jar:9.0.44]
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) ~[na:na]
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628) ~[na:na]
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) ~[tomcat-util.jar:9.0.44]
    at java.base/java.lang.Thread.run(Thread.java:834) ~[na:na]

7. max-file-sizeより大きいファイルを送信した際にエラーが返るタイミング

リクエス

1.や2.のリクエストをもとに作成した以下のリクエストをtelnetで流しこんだ。

POST /upload HTTP/1.1
HOST: localhost:8080
content-type: multipart/form-data; boundary=----WebKitFormBoundaryqx0dK5IFp8lW2Hjr
content-length: 479

------WebKitFormBoundaryqx0dK5IFp8lW2Hjr
Content-Disposition: form-data; name="hoge"; filename="blob"
Content-Type: text/plain

piyo
------WebKitFormBoundaryqx0dK5IFp8lW2Hjr
Content-Disposition: form-data; name="file"; filename="100.txt"
Content-Type: text/plain

1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890............................................................
------WebKitFormBoundaryqx0dK5IFp8lW2Hjr--

結果

  • エラーとなり、HTTPレスポンス500が返る。
  • Controllerのメソッドは実行されない(Controllerまで処理が届かない)。
  • リクエストのうち「「...(改行)------WebKitFormBoundaryqx0dK5IFp8lW2Hjr--」はサーバへ送信されず、telnetを実行しているコンソールの標準出力にあふれる。
  • 標準出力へ以下のスタックトレースが出力される。

分かったこと

  • ファイル個別のContent-Lengthがない場合でも、リクエストを最後まで読んでからファイルの長さを確認するのではなく、max-file-sizeを超える量を読み込んだ時点でエラー処理に入っている。
  • max-file-sizeを超える量ぴったりではなく、さらに少しリクエストを読み込んでからエラー処理が行われるのは、リクエストを読み込むStreamのバッファのせい?

スタックトレース

org.apache.tomcat.util.http.fileupload.impl.FileSizeLimitExceededException: The field file exceeds its maximum permitted size of 100 bytes.
    at org.apache.tomcat.util.http.fileupload.impl.FileItemStreamImpl$1.raiseError(FileItemStreamImpl.java:114) ~[tomcat-coyote.jar:9.0.44]
    at org.apache.tomcat.util.http.fileupload.util.LimitedInputStream.checkLimit(LimitedInputStream.java:76) ~[tomcat-coyote.jar:9.0.44]
    at org.apache.tomcat.util.http.fileupload.util.LimitedInputStream.read(LimitedInputStream.java:135) ~[tomcat-coyote.jar:9.0.44]
    at java.base/java.io.FilterInputStream.read(FilterInputStream.java:107) ~[na:na]
    at org.apache.tomcat.util.http.fileupload.util.Streams.copy(Streams.java:98) ~[tomcat-coyote.jar:9.0.44]
    at org.apache.tomcat.util.http.fileupload.FileUploadBase.parseRequest(FileUploadBase.java:291) ~[tomcat-coyote.jar:9.0.44]
    at org.apache.catalina.connector.Request.parseParts(Request.java:2922) ~[catalina.jar:9.0.44]
    at org.apache.catalina.connector.Request.getParts(Request.java:2824) ~[catalina.jar:9.0.44]
    at org.apache.catalina.connector.RequestFacade.getParts(RequestFacade.java:1098) ~[catalina.jar:9.0.44]
    at org.springframework.web.multipart.support.StandardMultipartHttpServletRequest.parseRequest(StandardMultipartHttpServletRequest.java:95) ~[spring-web-5.3.9.jar:5.3.9]
    at org.springframework.web.multipart.support.StandardMultipartHttpServletRequest.<init>(StandardMultipartHttpServletRequest.java:88) ~[spring-web-5.3.9.jar:5.3.9]
    at org.springframework.web.multipart.support.StandardServletMultipartResolver.resolveMultipart(StandardServletMultipartResolver.java:122) ~[spring-web-5.3.9.jar:5.3.9]
    at org.springframework.web.servlet.DispatcherServlet.checkMultipart(DispatcherServlet.java:1202) ~[spring-webmvc-5.3.9.jar:5.3.9]
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1036) ~[spring-webmvc-5.3.9.jar:5.3.9]
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:963) ~[spring-webmvc-5.3.9.jar:5.3.9]
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006) ~[spring-webmvc-5.3.9.jar:5.3.9]
    at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:909) ~[spring-webmvc-5.3.9.jar:5.3.9]
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:652) ~[servlet-api.jar:4.0.FR]
    at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883) ~[spring-webmvc-5.3.9.jar:5.3.9]
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:733) ~[servlet-api.jar:4.0.FR]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:227) ~[catalina.jar:9.0.44]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[catalina.jar:9.0.44]
    at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53) ~[tomcat-websocket.jar:9.0.44]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[catalina.jar:9.0.44]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[catalina.jar:9.0.44]
    at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-5.3.9.jar:5.3.9]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.3.9.jar:5.3.9]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[catalina.jar:9.0.44]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[catalina.jar:9.0.44]
    at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[spring-web-5.3.9.jar:5.3.9]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.3.9.jar:5.3.9]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[catalina.jar:9.0.44]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[catalina.jar:9.0.44]
    at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[spring-web-5.3.9.jar:5.3.9]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.3.9.jar:5.3.9]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[catalina.jar:9.0.44]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[catalina.jar:9.0.44]
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202) ~[catalina.jar:9.0.44]
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97) ~[catalina.jar:9.0.44]
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:542) ~[catalina.jar:9.0.44]
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:143) ~[catalina.jar:9.0.44]
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92) ~[catalina.jar:9.0.44]
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78) ~[catalina.jar:9.0.44]
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:357) ~[catalina.jar:9.0.44]
    at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:374) ~[tomcat-coyote.jar:9.0.44]
    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65) ~[tomcat-coyote.jar:9.0.44]
    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:893) ~[tomcat-coyote.jar:9.0.44]
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1707) ~[tomcat-coyote.jar:9.0.44]
    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) ~[tomcat-coyote.jar:9.0.44]
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) ~[na:na]
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628) ~[na:na]
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) ~[tomcat-util.jar:9.0.44]
    at java.base/java.lang.Thread.run(Thread.java:834) ~[na:na]

*1:spring.servlet.multipart.file-size-thresholdの値を超えたファイルは一時ファイルに出力される。デフォルト値は0なので、すべてのファイルが一時ファイルに保存される。