Spring Frameworkのmax-file-sizeによるアップロードファイルサイズ制限のまとめ
application.properties
にspring.servlet.multipart.max-file-size=100
のように設定することでサイズ制限設定可能。- Spring Frameworkのファイルアップロード機能は、Webコンテナが持っているmultipartリクエストの処理機能をそのまま使う*1。ファイルサイズ上限のチェックに引っかかったときのスタックトレースを見ると、Tomcatでは
org.apache.tomcat.util.http.fileupload
パッケージが使われている。これにはcommons-fileuploadからコピーされてきたコードが使われている*2。 - Webコンテナのmultipartリクエストのサイズ制限機能は、
web.xml
からも設定できる*3。プログラム内でも指定はできるが、指定をするためのMultipartConfigElement
はSpringアプリケーション1つに対して1個のようなので*4、Controllerごと・URLごとのファイルサイズ制限は行えない模様*5。 - URLごとのファイルサイズ制限を指定するアノテーションをググると
@MultipartConfig
がある。これはJavaEE仕様(javax.servlet.annotation.MultipartConfig
)なので、少なくともSpringだけでは利用できない模様*6。
- Spring Frameworkのファイルアップロード機能は、Webコンテナが持っているmultipartリクエストの処理機能をそのまま使う*1。ファイルサイズ上限のチェックに引っかかったときのスタックトレースを見ると、Tomcatでは
max-file-size
によって発生したエラーは、Controllerではハンドリングできない。URLごとに個別のファイルサイズ制限をしたい、またはファイルサイズ上限エラーのハンドリングをビジネスロジックの中で行い、ビジネスロジックのエラーとしてレスポンスに含めたいという場合は、MultipartFile#getSize()
を呼び出して自分で判定を行う必要がある*7。例外としてはSpring Frameworkがorg.springframework.web.multipart.MaxUploadSizeExceededException
を提供しているのでこれを使えばよい。application.properties
のfile-size-threshold
がデフォルトなら、アップロードされたファイルは常に一時ファイルへ格納されJavaヒープ上には格納されないので、大きいファイルをアップロードされてJavaのヒープが溢れるといったことはなさそう。- 一時ファイルはリクエストが処理されたらすぐに削除されるので、ハウスキープ処理は基本的に必要なさそう。
- 一時ファイルが作られる先は、OSのテンポラリディレクトリの下の
tomcat.【ポート番号】.【Tomcatを立ち上げるたびに決まる乱数?】\work\Tomcat\【ホスト名】\
以下。ファイル削除が必要な場合はそこを削除する。
01: POST /upload HTTP/1.1 02: HOST: localhost:8080 03: content-type: multipart/form-data; boundary=----WebKitFormBoundaryqx0dK5IFp8lW2Hjr 04: content-length: 500 05: 06: ------WebKitFormBoundaryqx0dK5IFp8lW2Hjr 07: Content-Disposition: form-data; name="hoge"; filename="blob" 08: Content-Type: text/plain 09: 10: piyo 11: ------WebKitFormBoundaryqx0dK5IFp8lW2Hjr 12: Content-Disposition: form-data; name="file"; filename="100.txt" 13: Content-Type: text/plain 14: Content-Length: 150 15: 16: 1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890............................................................ 17: ------WebKitFormBoundaryqx0dK5IFp8lW2Hjr--
- 上記のようなリクエストがあった場合、以下のように処理が行われる。
- 4行目の
Content-Length
は必須。これがなかった場合、multipartリクエストとして扱われない(ファイル部分は無視されてしまう)。 - 4行目の
Content-Length
からファイル部分のサイズを逆算するなどはしない。 - 14行目の
Content-Length
はあってもなくてもよい。ChromeのAdvanced REST Clientの場合、14行目のContent-Length
は付与されない。 - 14行目の
Content-Length
がある場合、これを読み込んだ時点でmax-file-size
との比較が行われる。パスしなかった場合は即エラーとなり、以降のデータは読み込まれない。 - 14行目の
Content-Length
がない場合は、16行目以降の内容を読み込み始める。max-file-size
を超える内容を読み込んだ時点で即エラーとなる。エラーとなった場合、以降のデータは読み込まれない。 - 14行目の
Content-Length
があって、かつmax-file-size
よりも小さい場合は、Content-Length
がない場合と同じ処理になる。 - 上記で「即エラーとなる」というのは、101バイト目を読み込んだ時点でエラーとなるわけではなく、もう少し(数十バイト?)読み込んでからエラーが返る。
- 4行目の
*1:https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-multipart-resolver-standard
*2:https://tomcat.apache.org/tomcat-8.5-doc/api/org/apache/tomcat/util/http/fileupload/package-summary.html
*3:https://stackoverflow.com/a/19145563/3902663
*4:https://docs.spring.io/spring-framework/docs/5.3.10/javadoc-api/org/springframework/web/multipart/support/StandardServletMultipartResolver.html
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--
結果
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]
Validate memory consumption in a JUnit test case
For load test
If you want to run a load test in a JUnit test case and get maximum memory consumption or GC count as a Java variable, the way to do it doesn't seem to be.
For memory consumption of whole Java VM
If you want to run some tests and measure memory consumption of the whole Java VM, you can use Runtime#totalMemory()
and Runtime#freeMemory()
like this *1:
static long initialMemoryConsumption; @BeforeAll static void recordInitialMemoryConsumption() { initialMemoryConsumption = getMemoryConsumption(); } @AfterAll static void measureWholeMemoryConsumption() { System.out.println(getMemoryConsumption() - initialMemoryConsumption); } static long getMemoryConsumption() { Runtime runtime = Runtime.getRuntime(); runtime.gc(); return runtime.totalMemory() - runtime.freeMemory(); }
For memory consumption of an object
If you want to measure rough memory consumption of a specific object in a test (for example, validate memory consumption is and not , or validate all the caches are invalidated), you can serialize the object into a byte array and measure the length of it.
static int getMemoryConsumption(Serializable obj) throws IOException { try ( ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream out = new ObjectOutputStream(bos); ) { out.writeObject(obj); out.flush(); byte[] bytes = bos.toByteArray(); return bytes.length; } } @Test void memoryConsumptionTest() throws IOException { ... assertTrue(getMemoryConsumption(someObject) < 500); }
- You cannot use
Runtime#totalMemory()
andRuntime#freeMemory()
for this purpose, because the whole memory consumption of Java VM reduces in a test case (memory consumption could seem like a minus value). - The measured object should implement the
Serializable
interface. If it doesn't, you have to add the interface at runtime with the Java instrumentation API and a library like Javassist *2. - The actual length of the byte array (
< 500
in the above example) should be measured in the test environment before writing a test case.