Search code examples
javajerseymultipartform-dataembedded-jetty

Embedded Jetty HttpServer not accepting mutipart/form-data


I'm working to migrate from Eclipse Jersey/Grizzly (2.33) to Eclipse/Jetty (10.0.6) for the embedded Http Server for our REST API, and I can't for the life of me get the multipart/form-data uploads working properly. I freely admit I am not versed in Jetty configuration, nor Jersey/Grizzly configuration, and I'm cobbling together the old code with the bare minimum of boilerplate from Jetty cookbooks.

At this point, I'd be thrilled to just get the server to accept the request. I can work on how to handle the files on my own. My primary goal at the moment is to not have to rewrite dozens of servlets/handlers right now (hence the use of the Jersey ServletContainer).

This is the server code:

    public static void start() throws Exception {
    
    httpConfig = new HttpConfiguration();

    HttpConnectionFactory http11 = new HttpConnectionFactory(httpConfig);
    
    server = new Server();
    ServerConnector connector = new ServerConnector(server, http11);
    connector.setPort((cmdOptions.port < 0 ? 9998 : cmdOptions.port));
    server.setConnectors(new Connector[] {connector});
    
    ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
    context.setContextPath("/");

    HandlerList handlers = new HandlerList();
    ServletHandler servletHandler = new ServletHandler();
    
    // Set up the resources.common package as the handlers for the servlet
    ServletHolder servletHolder = context.addServlet(ServletContainer.class, "/*");
    servletHolder.setInitOrder(0);
    servletHolder.setInitParameter("jersey.config.server.provider.packages", "resources.grizzly;resources.common");
    servletHandler.addServlet(servletHolder);
    
    // MultiPartConfig setup - to allow for ServletRequest.getParts() usage
    Path multipartTmpDir = Paths.get("target", "multipart-tmp");
    multipartTmpDir = CommonResFileManager.ensureDirExists(multipartTmpDir);

    String location = multipartTmpDir.toString();
    long maxFileSize = 10 * 1024 * 1024; // 10 MB
    long maxRequestSize = 10 * 1024 * 1024; // 10 MB
    int fileSizeThreshold = 64 * 1024; // 64 KB
    MultipartConfigElement multipartConfig = new MultipartConfigElement(location, maxFileSize, maxRequestSize, fileSizeThreshold);
    
    FilterHolder filterHolder;

    filterHolder = context.addFilter(resources.jetty.SecurityFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST));
    filterHolder.setAsyncSupported(true);

    CorsHandler corsHandler = new CorsHandler();
    corsHandler.setHandler(context);

    UploadHandler uploadHandler = new UploadHandler("/G/uploadFolder", multipartConfig, multipartTmpDir);
    
    handlers.addHandler(corsHandler);
    handlers.addHandler(uploadHandler);
    handlers.addHandler(servletHandler);
    
    server.setHandler(handlers);
    server.start();
}

The resources of interest are:

public class CommonResProject extends CommonResBase {

...

    @POST @Path("uploadFolder")
    @Produces(MediaType.APPLICATION_JSON)
    @Consumes(MediaType.MULTIPART_FORM_DATA)
    public String uploadFolder(final FormDataMultiPart multiPart)            
    {
        Collection<Part> parts = null;
        try {
            parts = ((HttpServletRequest)request).getParts();
        } catch (IOException | ServletException ex) {
            Logger.getLogger(CommonResProject.class.getName()).log(Level.SEVERE, null, ex);
        }
        if(parts != null){
            parts.stream().forEach(p -> System.out.println(p.getName() + " ["+p.getContentType()+"]: "+p.getSize()+" bytes"));
        }
        // projects is a POJO that actually does the fiddly bits with the uploaded files
        boolean retVal = projects.uploadFolder(getDB(), getUserId(), multiPart);
        return "{\"retVal\" : " + String.valueOf(retVal) + "}";
    }   

...

Which is extended by:

@Path("/GProject")
public class GProject extends CommonResProject
{
    public GProject()
    {
        super();
        resInterface = new GBaseRes();  // Must always do
    }
    
    public static void processParts(HttpServletRequest request, HttpServletResponse response, java.nio.file.Path outputDir) throws ServletException, IOException
    {
        response.setContentType("text/plain");
        response.setCharacterEncoding("utf-8");

        PrintWriter out = response.getWriter();

        for (Part part : request.getParts())
        {
            out.printf("Got Part[%s].size=%s%n", part.getName(), part.getSize());
            out.printf("Got Part[%s].contentType=%s%n", part.getName(), part.getContentType());
            out.printf("Got Part[%s].submittedFileName=%s%n", part.getName(), part.getSubmittedFileName());
            String filename = part.getSubmittedFileName();
            if (StringUtil.isNotBlank(filename))
            {
                // ensure we don't have "/" and ".." in the raw form.
                filename = URLEncoder.encode(filename, "utf-8");

                java.nio.file.Path outputFile = outputDir.resolve(filename);
                try (InputStream inputStream = part.getInputStream();
                     OutputStream outputStream = Files.newOutputStream(outputFile, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING))
                {
                    IO.copy(inputStream, outputStream);
                    out.printf("Saved Part[%s] to %s%n", part.getName(), outputFile.toString());
                }
            }
        }
    }
    
    public static ServletContextHandler newServletUploadHandler(MultipartConfigElement multipartConfig, java.nio.file.Path outputDir) throws IOException
    {
        ServletContextHandler context = new ServletContextHandler();

        SaveUploadServlet saveUploadServlet = new SaveUploadServlet(outputDir);
        ServletHolder servletHolder = new ServletHolder(saveUploadServlet);
        servletHolder.getRegistration().setMultipartConfig(multipartConfig);

        context.addServlet(servletHolder, "/uploadFolder");

        return context;
    }   
    
    public static class SaveUploadServlet extends HttpServlet
    {
        private final java.nio.file.Path outputDir;

        public SaveUploadServlet(java.nio.file.Path outputDir) throws IOException
        {
            this.outputDir = outputDir.resolve("servlet");
            ensureDirExists(this.outputDir);
        }

        @Override
        protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
        {
            processParts(request, response, outputDir);
        }
    }
    
    
    public static class UploadHandler extends AbstractHandler
    {
        private final String contextPath;
        private final MultipartConfigElement multipartConfig;
        private final java.nio.file.Path outputDir;

        public UploadHandler(String contextPath, MultipartConfigElement multipartConfig, java.nio.file.Path outputDir) throws IOException
        {
            super();
            this.contextPath = contextPath;
            this.multipartConfig = multipartConfig;
            this.outputDir = outputDir.resolve("handler");
            CommonResFileManager.ensureDirExists(this.outputDir);
        }

        @Override
        public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
        {
            if (!target.startsWith(contextPath))
            {
                // not meant for us, skip it.
                return;
            }

            if (!request.getMethod().equalsIgnoreCase("POST"))
            {
                response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
                return;
            }

            // Ensure request knows about MultiPartConfigElement setup.
            request.setAttribute(Request.__MULTIPART_CONFIG_ELEMENT, multipartConfig);
            // Process the request
            processParts(request, response, outputDir);
            //baseRequest.setHandled(true);
        }
    }
}

And the whole thing generates the following stacktrace when I try to upload a set of files:

2021-09-13 12:58:17 SEVERE - resources.common.ResponseExceptionMapper toResponse -- HTTP 415 Unsupported Media Type
javax.ws.rs.NotSupportedException: HTTP 415 Unsupported Media Type
    at org.glassfish.jersey.server.spi.internal.ParameterValueHelper.getParameterValues(ParameterValueHelper.java:75)
    at org.glassfish.jersey.server.model.internal.JavaResourceMethodDispatcherProvider$AbstractMethodParamInvoker.getParamValues(JavaResourceMethodDispatcherProvider.java:109)
    at org.glassfish.jersey.server.model.internal.JavaResourceMethodDispatcherProvider$TypeOutInvoker.doDispatch(JavaResourceMethodDispatcherProvider.java:219)
    at org.glassfish.jersey.server.model.internal.AbstractJavaResourceMethodDispatcher.dispatch(AbstractJavaResourceMethodDispatcher.java:79)
    at org.glassfish.jersey.server.model.ResourceMethodInvoker.invoke(ResourceMethodInvoker.java:475)
    at org.glassfish.jersey.server.model.ResourceMethodInvoker.apply(ResourceMethodInvoker.java:397)
    at org.glassfish.jersey.server.model.ResourceMethodInvoker.apply(ResourceMethodInvoker.java:81)
    at org.glassfish.jersey.server.ServerRuntime$1.run(ServerRuntime.java:255)
    at org.glassfish.jersey.internal.Errors$1.call(Errors.java:248)
    at org.glassfish.jersey.internal.Errors$1.call(Errors.java:244)
    at org.glassfish.jersey.internal.Errors.process(Errors.java:292)
    at org.glassfish.jersey.internal.Errors.process(Errors.java:274)
    at org.glassfish.jersey.internal.Errors.process(Errors.java:244)
    at org.glassfish.jersey.process.internal.RequestScope.runInScope(RequestScope.java:265)
    at org.glassfish.jersey.server.ServerRuntime.process(ServerRuntime.java:234)
    at org.glassfish.jersey.server.ApplicationHandler.handle(ApplicationHandler.java:680)
    at org.glassfish.jersey.servlet.WebComponent.serviceImpl(WebComponent.java:394)
    at org.glassfish.jersey.servlet.WebComponent.service(WebComponent.java:346)
    at org.glassfish.jersey.servlet.ServletContainer.service(ServletContainer.java:366)
    at org.glassfish.jersey.servlet.ServletContainer.service(ServletContainer.java:319)
    at org.glassfish.jersey.servlet.ServletContainer.service(ServletContainer.java:205)
    at org.eclipse.jetty.servlet.ServletHolder.handle(ServletHolder.java:764)
    at org.eclipse.jetty.servlet.ServletHandler$ChainEnd.doFilter(ServletHandler.java:1619)
    at resources.jetty.SecurityFilter.doFilter(SecurityFilter.java:232)
    at org.eclipse.jetty.servlet.FilterHolder.doFilter(FilterHolder.java:202)
    at org.eclipse.jetty.servlet.ServletHandler$Chain.doFilter(ServletHandler.java:1594)
    at org.eclipse.jetty.websocket.servlet.WebSocketUpgradeFilter.doFilter(WebSocketUpgradeFilter.java:164)
    at org.eclipse.jetty.servlet.FilterHolder.doFilter(FilterHolder.java:202)
    at org.eclipse.jetty.servlet.ServletHandler$Chain.doFilter(ServletHandler.java:1594)
    at org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:506)
    at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:221)
    at org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:1571)
    at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:221)
    at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1372)
    at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:176)
    at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:463)
    at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:1544)
    at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:174)
    at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1294)
    at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:129)
    at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:122)
    at resources.jetty.CorsHandler.handle(CorsHandler.java:30)
    at org.eclipse.jetty.server.handler.HandlerList.handle(HandlerList.java:51)
    at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:122)
    at org.eclipse.jetty.server.Server.handle(Server.java:562)
    at org.eclipse.jetty.server.HttpChannel.lambda$handle$0(HttpChannel.java:406)
    at org.eclipse.jetty.server.HttpChannel.dispatch(HttpChannel.java:663)
    at org.eclipse.jetty.server.HttpChannel.handle(HttpChannel.java:398)
    at org.eclipse.jetty.server.HttpConnection.onFillable(HttpConnection.java:282)
    at org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:319)
    at org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:100)
    at org.eclipse.jetty.io.SocketChannelEndPoint$1.run(SocketChannelEndPoint.java:101)
    at org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy.runTask(AdaptiveExecutionStrategy.java:412)
    at org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy.consumeTask(AdaptiveExecutionStrategy.java:381)
    at org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy.tryProduce(AdaptiveExecutionStrategy.java:268)
    at org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy.lambda$new$0(AdaptiveExecutionStrategy.java:138)
    at org.eclipse.jetty.util.thread.ReservedThreadExecutor$ReservedThread.run(ReservedThreadExecutor.java:378)
    at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:894)
    at org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.run(QueuedThreadPool.java:1038)
    at java.base/java.lang.Thread.run(Thread.java:832)
Caused by: org.glassfish.jersey.message.internal.MessageBodyProviderNotFoundException: MessageBodyReader not found for media type=multipart/form-data;boundary=----WebKitFormBoundary4K5nPFIDDwLPZAnk, type=class org.glassfish.jersey.media.multipart.FormDataMultiPart, genericType=class org.glassfish.jersey.media.multipart.FormDataMultiPart.
    at org.glassfish.jersey.message.internal.ReaderInterceptorExecutor$TerminalReaderInterceptor.aroundReadFrom(ReaderInterceptorExecutor.java:208)
    at org.glassfish.jersey.message.internal.ReaderInterceptorExecutor.proceed(ReaderInterceptorExecutor.java:132)
    at org.glassfish.jersey.server.internal.MappableExceptionWrapperInterceptor.aroundReadFrom(MappableExceptionWrapperInterceptor.java:49)
    at org.glassfish.jersey.message.internal.ReaderInterceptorExecutor.proceed(ReaderInterceptorExecutor.java:132)
    at org.glassfish.jersey.message.internal.MessageBodyFactory.readFrom(MessageBodyFactory.java:1072)
    at org.glassfish.jersey.message.internal.InboundMessageContext.readEntity(InboundMessageContext.java:885)
    at org.glassfish.jersey.server.ContainerRequest.readEntity(ContainerRequest.java:282)
    at org.glassfish.jersey.server.internal.inject.EntityParamValueParamProvider$EntityValueSupplier.apply(EntityParamValueParamProvider.java:73)
    at org.glassfish.jersey.server.internal.inject.EntityParamValueParamProvider$EntityValueSupplier.apply(EntityParamValueParamProvider.java:56)
    at org.glassfish.jersey.server.spi.internal.ParamValueFactoryWithSource.apply(ParamValueFactoryWithSource.java:50)
    at org.glassfish.jersey.server.spi.internal.ParameterValueHelper.getParameterValues(ParameterValueHelper.java:68)

Solution

  • First, don't use ServletHandler directly like that.

    Only use ServletContetHandler and ServletHolder to configure what you need.

    ServletHandler is an internal class not meant to be used directly like that. Especially with all of the configuration you are attempting to force on it.

    Next, convert UploadHandler to a normal/formal HttpServlet and add it to the ServletContextHandler properly (you can even use the same url-pattern as you are currently). The ServletContext is important here (for multipart), and your raw/naked UploadHandler is not actually handling multipart like you think it is.

    The stacktrace indicates that you are not using Jetty for multipart at the point in time where the stacktrace is generated, which means it bypassed the UploadHandler and Jersey itself is attempting to handle the multipart content. This probably means you have the specify the MultiPartConfigElement on the Jersey servlet instead.

    
    ServletHolder servletHolder = context.addServlet(ServletContainer.class, "/*");
    servletHolder.setInitOrder(0);
    servletHolder.setInitParameter("jersey.config.server.provider.packages",
       "resources.grizzly;resources.common");
    
    Path multipartTmpDir = Paths.get("target", "multipart-tmp");
    multipartTmpDir = CommonResFileManager.ensureDirExists(multipartTmpDir);
    
    String location = multipartTmpDir.toString();
    long maxFileSize = 10 * 1024 * 1024; // 10 MB
    long maxRequestSize = 10 * 1024 * 1024; // 10 MB
    int fileSizeThreshold = 64 * 1024; // 64 KB
    MultipartConfigElement multipartConfig = new MultipartConfigElement(location,
       maxFileSize, maxRequestSize, fileSizeThreshold);
    
    servletHolder.getRegistration().setMultipartConfig(multipartConfig);