Post View

ajax를 통한 파일업로드 시 간헐적인 MultipartException 발생

모바일에서 업로드 테스트 중, 갑자기 "파일 업로드 중 네트워크 오류가 발생했습니다." 라는 메시지가 나타났습니다.
분명 PC에서는 정상적으로 업로드가 되는걸 확인했는데 왜 모바일에서만 갑자기 이러는건지...

처음에는 모바일에서만 이런 오류가 발생하는줄 알고 개발자 도구를 사용하지 못해 굉장히 난감했습니다.
일단 테스트를 위해 개발환경에서 이클립스를 키고, 로컬 환경을 외부에서 접속할 수 있게 한 뒤 휴대폰으로 이미지를 올려보았는데, 업로드가 되네요...?

그럼 PC/Mobile간의 문제가 아니라 현재 운영을 하고있는 서버쪽 설정에 문제가 있는 것 같은데...
먼저 서버쪽 로그를 확인해봤습니다.

15-Apr-2020 16:42:47.368 SEVERE [https-openssl-apr-8443-exec-5] org.apache.catalina.core.StandardWrapperValve.invoke Servlet.service() for servlet [appServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.web.multipart.MultipartException: Failed to parse multipart servlet request; nested exception is org.apache.commons.fileupload.FileUploadBase$IOFileUploadException: Processing of multipart/form-data request failed. java.io.IOException: The socket [140,219,479,134,368] associated with this connection has been closed.] with root cause
    java.io.IOException: The socket [140,219,479,134,368] associated with this connection has been closed.
        at org.apache.tomcat.util.net.AprEndpoint$AprSocketWrapper.doWrite(AprEndpoint.java:2335)
        at org.apache.tomcat.util.net.SocketWrapperBase.doWrite(SocketWrapperBase.java:692)
        at org.apache.tomcat.util.net.SocketWrapperBase.flushBlocking(SocketWrapperBase.java:645)
        at org.apache.tomcat.util.net.SocketWrapperBase.flush(SocketWrapperBase.java:635)
        at org.apache.coyote.http2.Http2UpgradeHandler.writeWindowUpdate(Http2UpgradeHandler.java:810)
        at org.apache.coyote.http2.Stream$StreamInputBuffer.doRead(Stream.java:1185)
        at org.apache.coyote.Request.doRead(Request.java:581)
        at org.apache.catalina.connector.InputBuffer.realReadBytes(InputBuffer.java:344)
        at org.apache.catalina.connector.InputBuffer.checkByteBufferEof(InputBuffer.java:663)
        at org.apache.catalina.connector.InputBuffer.read(InputBuffer.java:370)
        at org.apache.catalina.connector.CoyoteInputStream.read(CoyoteInputStream.java:183)
        at java.io.FilterInputStream.read(FilterInputStream.java:133)
        at org.apache.commons.fileupload.util.LimitedInputStream.read(LimitedInputStream.java:132)
        at org.apache.commons.fileupload.MultipartStream$ItemInputStream.makeAvailable(MultipartStream.java:1027)
        at org.apache.commons.fileupload.MultipartStream$ItemInputStream.read(MultipartStream.java:931)
        at java.io.InputStream.read(InputStream.java:101)
        at org.apache.commons.fileupload.util.Streams.copy(Streams.java:98)
        at org.apache.commons.fileupload.util.Streams.copy(Streams.java:68)
        at org.apache.commons.fileupload.FileUploadBase.parseRequest(FileUploadBase.java:346)
        at org.apache.commons.fileupload.servlet.ServletFileUpload.parseRequest(ServletFileUpload.java:113)
        at org.springframework.web.multipart.commons.CommonsMultipartResolver.parseRequest(CommonsMultipartResolver.java:159)
        at org.springframework.web.multipart.commons.CommonsMultipartResolver.resolveMultipart(CommonsMultipartResolver.java:143)
        at org.springframework.web.servlet.DispatcherServlet.checkMultipart(DispatcherServlet.java:1178)
        at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1012)
        at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:943)
        at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)
        at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:909)
        at javax.servlet.http.HttpServlet.service(HttpServlet.java:660)
        at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)
        at javax.servlet.http.HttpServlet.service(HttpServlet.java:741)
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
        at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52)
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
        at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
        at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:320)
        at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.invoke(FilterSecurityInterceptor.java:126)
        at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.doFilter(FilterSecurityInterceptor.java:90)
        at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
        at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:118)
        at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
        at org.springframework.security.web.session.SessionManagementFilter.doFilter(SessionManagementFilter.java:137)
        at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
        at org.springframework.security.web.authentication.AnonymousAuthenticationFilter.doFilter(AnonymousAuthenticationFilter.java:111)
        at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
        at org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter.doFilter(SecurityContextHolderAwareRequestFilter.java:158)
        at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
        at org.springframework.security.web.savedrequest.RequestCacheAwareFilter.doFilter(RequestCacheAwareFilter.java:63)
        at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
        at org.springframework.security.web.authentication.www.BasicAuthenticationFilter.doFilterInternal(BasicAuthenticationFilter.java:155)
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
        at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
        at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:200)
        at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
        at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:116)
        at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
        at org.springframework.security.web.header.HeaderWriterFilter.doHeadersAfter(HeaderWriterFilter.java:92)
        at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:77)
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
        at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
        at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:56)
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
        at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
        at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:105)
        at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
        at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:215)
        at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:178)
        at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:358)
        at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:271)
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
        at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:199)
        at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96)
        at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:668)
        at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:139)
        at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:81)
        at org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:688)
        at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:87)
        at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343)
        at org.apache.coyote.http2.StreamProcessor.service(StreamProcessor.java:327)
        at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65)
        at org.apache.coyote.http2.StreamProcessor.process(StreamProcessor.java:69)
        at org.apache.coyote.http2.StreamRunnable.run(StreamRunnable.java:35)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
        at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
        at java.lang.Thread.run(Thread.java:748)

운영 서버에서 "tail -f logfile.log" 명령을 통해 로그를 보면서 휴대폰으로 이미지 업로드를 시도해보니 MultipartException이 발생했네요.
해당 내용을 구글에서 "ckeditor mobile nested exception is org.springframework.web.multipart.MultipartException: Failed to parse multipart servlet request"로 검색해서 원인과 해결방법을 찾아보았습니다. 

검색된 글을 보니 Tomcat의 context.xml 수정, web.xml filter 등록, headers = ("content-type=multipart/*") 입력 등 20 ~ 30개의 글을 봤지만 전혀 문제가 해결되지 않았는데,
글 중에 비슷한 현상이 있는 글(https://okky.kr/article/685136)을 발견했습니다.
여기서 말하기를 저용량은 문제가 없지만, 파일크기가 일정 이상 넘어가면 오류가 발생한다고 합니다.

여러 이미지를 올려보면서 테스트 해보니 모바일에서도 모든 이미지의 업로드가 문제가 발생하는게 아니라 약 500kb 이상의 이미지만 문제가 발생합니다.
마찬가지로 PC에서도 이미지 용량이 500kb를 넘어가면 문제가 발생하더라구요...
안타깝게도 아까 그 글에는 해결 방법이 안적혀있더라구요.

다시한번 오류메시지를 읽어보니, 소켓이 종료되었다고 나와있는데 왜 종료된지를 알아야 할 것 같아서  다시 검색 을 해봤습니다.
"The socket [?] associated with this connection has been closed. "로 검색!
 * [?](브라켓) 부분의 값 "140,219,479,134,368"을 "?"로 바꿔서 검색 한 이유는 구글 검색 시 ?를 입력하면 해당 부분에 어떤 값이 들어오더라도 검색됩니다.  

검색 최상단에 오류메시지와 비슷한 제목의 글(https://bz.apache.org/bugzilla/show_bug.cgi?id=63690)이 있어서 읽어봤습니다.
글 내용을 보니 해당 문제는 HTTP/2 버전이 활성화 되어  있을 때 발생하며 Tomcat의 server.xml 설정 중 를 주석처리 하면 해당 이슈가 사라진다고 합니다.

해당 설정은 HTTP의 버전을 HTTP/2로 업그레이드 시키는 설정인데, 해당 설정을 주석처리 하면 HTTP/1.1 버전으로 작동한다고 하네요.
일단 한번 작업해보았습니다.

주석 처리 후 다시 Tomcat을 재시작합니다.

모바일과 PC에서 테스트를 해보니 정상적으로 업로드 되네요!

해결책을 찾았던 글(https://bz.apache.org/bugzilla/show_bug.cgi?id=63690)을 더 살펴보니 이 이슈는 HTTP/2의 overheadDataThreadhold 관련 이슈라고 하네요.
server.xml의 설정은 HTTPS 적용 시에 기본값으로 적혀있는 부분이어서 신경을 안썼었는데,
로컬 환경에서는 HTTPS가 적용안되어 있으니 해당 설정이 없었고, HTTP/1.1로 작동되어 같은 문제가 발생되지 않았었네요.

일단은 HTTP/1.1로 작동되어도 큰 문제가 없기도 하고, 현재로써는 서버 설정보다는 개발 코드가 중요하다고 생각하여 여기에서 마무리 하도록 하겠습니다.
해당 이슈에 대해서는 추후에 추가로 작업해보도록 하겠습니다.

Comments