jetty.project icon indicating copy to clipboard operation
jetty.project copied to clipboard

Spring and Jetty ResourceServlet Issues

Open jringbox opened this issue 7 months ago • 8 comments
trafficstars

Jetty Version 12.0.16

Jetty Environment Spring Boot 3.4.3 Spring 6.2.3 Jakarta EE10

Java Version 17

Question Recently, I've been working on upgrade from Java 8 to 17. This involved Jetty needing to be upgraded from 9 to 12. My primary focus is to get my app working in a standalone Jetty server.

Everything went smooth on the upgrade but I'm having issues with ResourceServlet. I understand we shouldn't be using DefaultServlet since it gives a warning in the logs to that effect.

This is what I thought would work but didn't.

@SpringBootApplication
public class App extends SpringBootServletInitializer
{
	public static void main(String[] args) throws Exception
	{
		System.out.println("Starting DemoApplication...");
		SpringApplication.run(App.class, args);
	}

	@Bean
	public ServletRegistrationBean<ResourceServlet> resourceServletRegistration()
	{
		ServletRegistrationBean<ResourceServlet> registrationBean = new ServletRegistrationBean<>(new ResourceServlet(),
				"/resources/*");
		registrationBean.addInitParameter("resourceBase", "C:\\example\\resources");
		registrationBean.addInitParameter("dirAllowed", "true");
		return registrationBean;
	}
}

When I tried to use this, I got org.eclipse.jetty.ee10.webapp.WebAppContext$Servlet ApiContext is not org.eclipse.jetty.server.handler.ContextHandler$ScopedContext error.

Github coPilot says the error is a mismatch between the Jetty server's context handling and the way the servlet is being registered and goes on to say it's a conflict between Jetty's ServletContextHandler and the Jakarta Servlet API's ServletContext. But I'm not sure if this is a correct assessment or what this could imply.

I kept trying to rewrite the code several times and found I could get it working by removing the resourceServletRegistration Bean and implementing onStartup as follows.

	@Override
	public void onStartup(ServletContext servletContext) throws ServletException
	{
		System.out.println("onStartup starting");
		
		// Create a Jetty server and configure the context handler
		Server server = new Server(8080);
		ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
		context.setContextPath("/");
		server.setHandler(context);

		// Retrieve the ServletHolder bean and map it to the desired URL pattern
		ServletHolder resourceServletHolder = resourceServletHolder();
		context.addServlet(resourceServletHolder, "/resources/*");

		try
		{
			// Start the Jetty server
			server.start();
		}
		catch (Exception e)
		{
			throw new ServletException("Failed to start Jetty server", e);
		}

		super.onStartup(servletContext);
	}

However this does not work well with the rest of my annotated Beans in the class. I really don't want to create a new Server and assigning port and context path was not immediately available for me in my application properties.

What I want is to use the existing server context that is intializing/initialized and add this servlet resource to the existing servletContext. I kept hunting and I thought this would work for onStartup implementation.

	@Override
	public void onStartup(ServletContext servletContext) throws ServletException
	{
		System.out.println("onStartup starting");
		super.onStartup(servletContext);

		ServletRegistration.Dynamic resourceServlet = servletContext.addServlet("resourceServlet", ResourceServlet.class);

		// Set initialization parameters
		resourceServlet.setInitParameter("resourceBase", "C:\\example\\resources");

		// Map the servlet to a URL pattern
		resourceServlet.addMapping("/resources/*");
		System.out.println("Servlet dynamically registered with Jakarta Servlet API.");
	}

This did startup error-free but when I went to "localhost:8080/resources" to access a video file, I got nothing. No response in any jetty log. Web page just has ERR_CONNECTION_REFUSED. The servlet does not appear to be registered with the context at all.

What am I doing wrong here?

jringbox avatar Mar 27 '25 16:03 jringbox

Use a resourceBase that is a URI String.

So instead of C:\\example\\resources, try file://C:/example/resources (or some other thing like that). You can even do this to be extra sure of the conversion ..

import java.nio.file.Path;
import java.nio.file.Files;

Path path = Path.of("C:\\example\\resources");
if (!Files.isDirectory(path))
    throw new FileNotFoundException();
String baseUri = path.toUri().toASCIIString();

joakime avatar Mar 27 '25 16:03 joakime

I did see that being used in some example code. I made that change. No effect.

As I was looking, I noticed my example app that I'm trying to get working only included jakarta.servlet-api and jetty-ee10-servlet in my pom.xml. I needed spring-boot-starter-jetty which I added and excluded tomcat-embed-el since I'm not dealing with tomcat.

When I started my app, I got No Jetty ServletContextHandler, Jetty WebSocket SCI unavailable. I've seen this error before in my plethora of approaches of solving my resource servlet issue. What's causing this? Is there something else I'm missing in my implementation?

jringbox avatar Mar 27 '25 17:03 jringbox

Can you give a full stack trace on the exception "org.eclipse.jetty.ee10.webapp.WebAppContext$Servlet ApiContext is not org.eclipse.jetty.server.handler.ContextHandler$ScopedContext"

Also, before and after you start the server can you do a server.dumpStdErr(), and post the output here?

gregw avatar Mar 29 '25 04:03 gregw

Can you give a full stack trace on the exception "org.eclipse.jetty.ee10.webapp.WebAppContext$Servlet ApiContext is not org.eclipse.jetty.server.handler.ContextHandler$ScopedContext"

Also, before and after you start the server can you do a server.dumpStdErr(), and post the output here?

When I got that exception, I was using the first code I posted. There is no server variable. How do you propose I get the server instance to call dumpStdErr()? I tried to wire up a JettyServerCustomizer bean and implemented lifeCycleStarting and lifeCycleStarted but I got no output. Not sure what I did wrong here.

As fyi, in the second code, I introduced an implementation of onStartup which does not create the exception but I didn't want to create a offshoot server. Then in the third code, I rewrote onStartup but that code doesn't work. The addition of the onStartup implementation does not generate the org.eclipse.jetty.ee10.webapp.WebAppContext$Servlet ApiContext is not org.eclipse.jetty.server.handler.ContextHandler$ScopedContext exception.

jringbox avatar Mar 31 '25 15:03 jringbox

I would think this kind of thing should work on Spring Boot ...

import org.springframework.boot.context.embedded.jetty.JettyServerCustomizer

@Component
public class DumpStartup implements JettyServerCustomizer {
    @Override
    public void customize(Server server) {
        if (server.isStarting())
            server.setDumpAfterStart(true);
        else
            server.dumpStdErr();
    }
}

joakime avatar Mar 31 '25 20:03 joakime

No dice on that. All I have is SpringBootApplication annotation on the App and that DumpStartup customizer class you provided is in the same package. So, it should pick it up for component scanning. I must be fundamentally overlooking something I'm not setting up right in the simple Spring-Jetty example project I created because it should've picked up that component.

jringbox avatar Mar 31 '25 21:03 jringbox

I've been continuing to try and figure out my problem. I have found that if I run my initial SpringBootApplication App class that I posted via maven with mvn spring-boot:run, it works with with no issues. But this is all purely Spring logic.

The problem is when that same application is placed into a standalone Jetty server. That's when I get No Jetty ServletContextHandler, Jetty WebSocket SCI unavailable. So apparently, I'm dealing with container issues, embedded servlet container (like spring boot container) versus standalone servlet container (like Jetty).

Spring Boot I believe initializes an embedded Jetty server where spring boots auto config picks up my registration beans and programmatically registers everything. So Jetty is out of the picture and everything works from this context.

But now, I have introduced Jetty. Spring Boot is no longer in control and Jetty is in control. Jetty is relying on Jakarta EE 10 specifications for the server. My code extends SpringBootServletInitializer which implements WebApplicationInitializer. I believe WebApplicationInitializer is the programmatic approach where onStartup implementation is required rather than having a web.xml for registering web servlet. But I've already done that approach and that gave me No Jetty ServletContextHandler, Jetty WebSocket SCI unavailable error. Regardless, I tried using this implementation again because I think this implementation is close to the correct way it should be coded even though it doesn't work for some reason.

@SpringBootApplication
public class App extends SpringBootServletInitializer
{
	public static void main(String[] args) throws Exception
	{
		System.out.println("Starting DemoApplication...");
		SpringApplication.run(App.class, args);
	}

	@Override
	protected SpringApplicationBuilder configure(SpringApplicationBuilder application)
	{
		return application.sources(App.class);
	}

	@Override
	public void onStartup(ServletContext servletContext) throws ServletException
	{
		System.out.println("onStartup starting");
		super.onStartup(servletContext);

		// Configure the ResourceServlet programmatically
		ServletRegistration.Dynamic resourceServlet = servletContext.addServlet("ResourceServlet", ResourceServlet.class);

		Path path = Path.of("C:\\example\\resources");
		String baseUri = path.toUri().toASCIIString();

		resourceServlet.setInitParameter("resourceBase", baseUri);
		resourceServlet.setInitParameter("dirAllowed", "true");
		resourceServlet.addMapping("/resources/*");
	}
}

application.yml

server:
    port: 8282
    contextPath: /example

Also, my pom.xml includes the following.

  • spring-boot-starter-web:3.4.3 (excluding spring-boot-starter-tomcat)
  • spring-boot-starter-jetty:3.4.3 (excluding spring-boot-starter-tomcat)
  • jetty-ee10-servlet:12.0.16
  • jakarta.servlet-api:6.0.0

With this code, I did try to debug to where the exception is thrown. I found that before the exception was thrown ServletContextHandler.getServletContextHandler received org.eclipse.jetty.ee10.webapp.WebAppContext for servletContext variable which is not an instance of ServletContextApi. This is why the exception was thrown. Why is this a "WebAppContext" and not a handler of some sort? Why is Jetty receiving a WebAppContext object?

As fyi, I think the reason my JettyServerCustomizer or using DumpStartup wired bean to get the server dump has not succeeded is because it's failing before it reaches that part of the workflow.

Also, I should mention that for ResourceServlet, I have seen an example out there for Jetty 12, https://github.com/jetty/jetty-examples/blob/12.0.x/embedded/ee10-file-server/src/main/java/examples/ServletFileServerMultipleLocations.java. This is a good example but not the example I want, esp with it literally creating a new server.

Can anybody create a simple example of a ResourceServlet using Spring Boot 3.4 that works on a Jetty 12 standalone server or fix the code I have posted to work in a Jetty 12 standalone server?

jringbox avatar Apr 01 '25 21:04 jringbox

I worked with someone on this issue I was having and we came up with the answer. The problem I was having stems from line 491 below from ResourceServlet.doGet in jetty-ee10-servlet-12.0.16 artifact as shown below.

488 // If the servlet response has been wrapped and has been written to,
489 // then the servlet response must be wrapped as a core response
490 // otherwise we can use the core response directly.
491 boolean useServletResponse = !(httpServletResponse instanceof ServletApiResponse) || servletContextResponse.isWritingOrStreaming();

The useServletResponse is returning true which results in UnknownLengthHttpContent exception to be thrown later in the method. With this UnknownLengthHttpContent wrapping our response we ran into an issue in Chrome (Firefox is less strict) where we were not able to play the videos inline in the browser due to an "HTTP 416 Requested Range Not Satisfiable" error. This was due to spring security wrapping the servlet request and then triggering the UnknownLengthHttpContent wrapping of the response. Thus the Chrome browser did not get the range request headers it was expecting and threw the error.

This is the code that is the main problem with using Jetty resource servlet sitting behind Spring Security / Spring Boot.

In spring-security-web-6.4.3 FilterChainProxy class, the doFilterInternal is wrapping of HttpServletRequest and HttpServletResponse by the Spring Security firewall, which is expected. The problem was the fact we are serving content that is not static and not in src/main/resources location. Spring Security is designed to work seamlessly with this static resource handling which is not what we were doing. To serve files from disk outside your deployment using Spring (not Jetty’s ResourceServlet), we had to implement a Spring @RestController or @Controller that streams files from disk. After doing this, then Spring Security handled the security as it normally would.

I'm not sure if this is the correct approach to code or if ResourceServlet.doGet has a bug related to useServletResponse assignment and UnknownLengthHttpContent throwing.

jringbox avatar Jun 04 '25 21:06 jringbox

@jringbox it would be helpful to get some kind of stack trace for this, along with the response headers. Note that the UnknownContentLengthHttpContent only comes into play if the application has already called getOutputStream() or getWriter() before it lands in the ResourceServlet. Do you have some filter that is doing that, or is Spring calling one of those methods?

janbartel avatar Nov 02 '25 21:11 janbartel