Firefox and Safari error out when download files greater than 1MB
Jetty version(s)
12.1.4
Jetty Environment
ee10
HTTP version
1.1
Java version/vendor (use: java -version)
openjdk version "25" 2025-09-16 LTS
OpenJDK Runtime Environment Corretto-25.0.0.36.2 (build 25+36-LTS)
OpenJDK 64-Bit Server VM Corretto-25.0.0.36.2 (build 25+36-LTS, mixed mode, sharing)
OS type/version Linux, Debian, 13
Description
In my ee10 environment, I'm using a ResourceHandler; this has been working great, however, in Firefox and Safari, if I attempt to download a .ZIP file that is above 1MB (1209991 exactly), it fails -- Firefox provides the NS_ERROR_NET_PARTIAL_TRANSFER. There is no error message logged on the Jetty side.
How to reproduce?
I have tried setting the minimum mapped file size and using the ByteBufferPool.NON_POOLING to no avail. Unclear what other information would be needed -- it is a manually configured ResourceHandler.
Please advise.
Are you using HTTP/1.1 or HTTP/2?
Knowing exactly how you configured your ResourceHandler is going to be necessary to figure out what the problem might be.
It's even better if you can provide us with a reproducer.
Hi--
Firefox and Safari are using HTTP/1.0.
Here's a minimal-ish example of what I am working with -- you'll need to fix the TODO to include a path -- then in that path, I include a file above 1MB -- in the real system, it is usually not reproducible on the localhost or local network, but this one appears to reproduce the problem.
package com.fxdevelopment.engine.jetty;
import jakarta.servlet.MultipartConfigElement;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.eclipse.jetty.ee10.servlet.ServletContextHandler;
import org.eclipse.jetty.ee10.servlet.ServletHolder;
import org.eclipse.jetty.http.CompressedContentFormat;
import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.http.UriCompliance;
import org.eclipse.jetty.http.content.ResourceHttpContentFactory;
import org.eclipse.jetty.server.ForwardedRequestCustomizer;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.handler.ResourceHandler;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import java.io.IOException;
public class Test extends HttpServlet implements JettyServletContainer {
private final ResourceHandler staticResourceHandler;
public Test( ResourceHandler staticResourceHandler ) {
this.staticResourceHandler = staticResourceHandler;
}
@Override
public void service( HttpServletRequest servletRequest, HttpServletResponse servletResponse ) throws ServletException, IOException {
Request coreRequest = org.eclipse.jetty.ee10.servlet.ServletCoreRequest.wrap( servletRequest );
Response coreResponse = org.eclipse.jetty.ee10.servlet.ServletCoreResponse.wrap( coreRequest, servletResponse, false );
try {
staticResourceHandler.handle( coreRequest, coreResponse, Callback.NOOP );
} catch ( Exception e ) {
throw new RuntimeException( e );
}
}
@Override
public void addServlet( HttpServlet servlet, SiteMapMakerServlet.Rate addToSiteMap, double priority, String... paths ) {
}
public static void main( String[] args ) throws Exception {
QueuedThreadPool threadPool = new QueuedThreadPool();
threadPool.setMinThreads( 100 );
threadPool.setMaxThreads( 500 );
// Configure the server
Server server = new Server( threadPool );
HttpConfiguration httpConfig = new HttpConfiguration();
httpConfig.setSecureScheme( "https" );
httpConfig.setSecurePort( 8084 );
httpConfig.setOutputBufferSize( 32 * 1024 );
httpConfig.setSendServerVersion( false );
httpConfig.setSendXPoweredBy( false );
httpConfig.addCustomizer( new ForwardedRequestCustomizer() );
// https://javadoc.jetty.org/jetty-12.1/org/eclipse/jetty/http/UriCompliance.Violation.html
UriCompliance specViolatingCompliance = UriCompliance.from( "RFC3986,AMBIGUOUS_PATH_ENCODING,AMBIGUOUS_PATH_PARAMETER,AMBIGUOUS_PATH_SEPARATOR,AMBIGUOUS_EMPTY_SEGMENT,AMBIGUOUS_PATH_SEGMENT,FRAGMENT" );
httpConfig.setUriCompliance( specViolatingCompliance );
httpConfig.setRedirectUriCompliance( specViolatingCompliance );
ResourceHandler staticResourceHandler = new ResourceHandler();
staticResourceHandler.setEtags( true );
staticResourceHandler.setDirAllowed( false );
MimeTypes mimeTypes = new MimeTypes();
staticResourceHandler.setMimeTypes( mimeTypes );
staticResourceHandler.setCacheControl( "max-age=" + ( ( 60 * 60 * 24 * 7 ) ) + ",public,stale-while-revalidate=30,stale-if-error=86400" ); // caching for 1 week in ms
String staticContentPath = //TODO: path to some data...
staticResourceHandler.setBaseResourceAsString( staticContentPath );
ResourceHttpContentFactory contentFactory = new ResourceHttpContentFactory( staticResourceHandler.getBaseResource(), mimeTypes, null );
staticResourceHandler.getResourceService().setHttpContentFactory( contentFactory );
staticResourceHandler.setPrecompressedFormats(
new CompressedContentFormat( "br", ".br" ),
new CompressedContentFormat( "gzip", ".gz" )
);
staticResourceHandler.setServer( server );
ServletContextHandler context = new ServletContextHandler( ServletContextHandler.SESSIONS );
// context.setErrorHandler( errorPageHandler );
context.setContextPath( "/" );
ServletHolder servletHolder = new ServletHolder( new Test( staticResourceHandler ) );
context.addServlet( servletHolder, "/*" );
servletHolder.getRegistration().setMultipartConfig(
new MultipartConfigElement(
System.getProperty("java.io.tmpdir"),
10 * 1024 * 1024L, // maxFileSize
20 * 1024 * 1024L, // maxRequestSize
5 * 1024 * 1024 // fileSizeThreshold
)
);
server.getServer().setHandler( context );
ServerConnector connector = new ServerConnector( server, null, null, null, -1, Runtime.getRuntime().availableProcessors(), new HttpConnectionFactory( httpConfig ) );
connector.setPort( 8081 );
connector.setIdleTimeout( 120000 );
connector.setAcceptQueueSize( 200 );
server.addConnector( connector );
server.start();
server.join();
}
}
Thanks.
Eric
I believe I managed to reproduce your problem.
In a nutshell, the problem is that you are calling an async API (staticResourceHandler.handle( coreRequest, coreResponse, Callback.NOOP )) from a blocking one (service()) so the async call has until the service() method returns and the request gets finalized by Jetty to upload its data. The quick fix is to block in service() until staticResourceHandler.handle() is done:
@Override
public void service(HttpServletRequest servletRequest, HttpServletResponse servletResponse) throws ServletException, IOException
{
Request coreRequest = org.eclipse.jetty.ee10.servlet.ServletCoreRequest.wrap(servletRequest);
Response coreResponse = org.eclipse.jetty.ee10.servlet.ServletCoreResponse.wrap(coreRequest, servletResponse, false);
try
{
try (Blocker.Callback callback = Blocker.callback())
{
staticResourceHandler.handle(coreRequest, coreResponse, callback);
callback.block(); // block until the above call is done
}
}
catch (Exception e)
{
throw new RuntimeException(e);
}
}
As a better fix, you should never mix Servlet and Core API usages in this way. If you want to serve static resources from the servlet context, it's better to use a ResourceServlet instead. But if you prefer to use the Core ResourceHandler, simply add it to a ContextHandler then combine it with the ServletContextHandler in a ContextHandlerCollection:
ResourceHandler staticResourceHandler = new ResourceHandler();
// configure the staticResourceHandler
ContextHandler staticContextHandler = new ContextHandler(staticResourceHandler, "/static");
ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
context.setContextPath("/");
// add servlets to the context
server.setHandler(new ContextHandlerCollection(context, staticContextHandler));