Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SpringMVC: Inconsistent mapping behavior depending on url extension

I have a RESTful spring based endpoint to get assets stored in a db to a javascript editor. The relevant parts boil down to:

@RestController
@RequestMapping(ThemeEndpoint.ENDPOINT_NAME)
public class ThemeEndpoint {

public static final String ENDPOINT_NAME = "/themes"; 

    @RequestMapping(value="/{id}/css/{assetName:.*}", method=RequestMethod.GET)
    public Asset getCssItem(
        @PathVariable("id") ThemeId id, 
        @PathVariable("assetName") String name) {
    CssThemeAsset themeAsset = themeService.getCssAsset(
                id, ThemeAssetId.fromString(name));
    Asset asset = new Asset();
    asset.name = themeAsset.getName();
    asset.text = themeAsset.getContent();
    return asset;
}

This works as expected for urls like

http://localhost:8080/app-url/rest/themes/ac18a080-a2f1-11e3-84f4-600308a0bd14/css/main.less

but fails as soon as i change the extension to .css.

After some debugging i'm quite sure the request is not even mapped if i use an url like

http://localhost:8080/app-url/rest/themes/ac18a080-a2f1-11e3-84f4-600308a0bd14/css/main.css

With a high log level i can see that the mapping is catched by spring:

org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping 
    - Mapped "{[/themes/{id}/css/{assetName:.*}],methods=[GET],params=[],headers=[],
                consumes=[],produces=[application/json],custom=[]}" 
      onto public xxx.endpoint.ThemeEndpoint$Asset
          xxx.endpoint.ThemeEndpoint.getCssItem(
              net.lacho.svc.themes.api.ThemeId,java.lang.String)   

and with a non .css extension the controller is called:

Found 1 matching mapping(s) for [/themes/ac18a080-a2f1-11e3-84f4-600308a0bd14/css/main.less]
  : [{[/themes/{id}/css/{assetName:.*}],methods=[GET],params=[],headers=[],
        consumes=[],produces=[application/json],custom=[]}]

but as soon as i use .css as extension - bang:

Looking up handler method for path /themes/ac18a080-a2f1-11e3-84f4-600308a0bd14/css/test.css
org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver - 
    Resolving exception from handler [null]:  
org.springframework.web.HttpMediaTypeNotAcceptableException: 
    Could not find acceptable representation

web.xmland MVC-Config as requested:

web.xml:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns="http://java.sun.com/xml/ns/javaee"
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee 
        http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
    version="3.0"
    metadata-complete="false">

<welcome-file-list>
    <welcome-file>index</welcome-file>
</welcome-file-list>

</web-app>

WebApplicationInitializer:

package net.lacho.opcenter.bootstrap;


public class WebApplicationBootstrapper implements WebApplicationInitializer {


    @Override
    public void onStartup(ServletContext container) throws ServletException {

        AnnotationConfigWebApplicationContext rootContext = new AnnotationConfigWebApplicationContext();
        rootContext.setConfigLocation(ApplicationConfig.class.getName());

        container.addListener(new ContextLoaderListener(rootContext));

        registerRestDispatcher(container);
        registerDefaultDispatcher(container);

        container.addFilter("CharacterEncodingFilter", UTF8EncodingFilter.class).addMappingForUrlPatterns(null,  true,  "/*");
        container.addFilter("headSupportFilter", HeadSupportFilter.class).addMappingForUrlPatterns(null,  true,  "/*");

        DelegatingFilterProxy shallowFrontendContextFilterProxy = new DelegatingFilterProxy("shallowFrontendContextProviderLocalFilter");
        shallowFrontendContextFilterProxy.setTargetFilterLifecycle(true);
        FilterRegistration.Dynamic shallowFrontendFilter = container.addFilter("ShallowFrontendContextFilter", shallowFrontendContextFilterProxy);
        shallowFrontendFilter.setInitParameter("ignoreNullClient", "true");
        shallowFrontendFilter.addMappingForUrlPatterns(null,  true,  "/*");

        container.addFilter("springSecurityFilterChain", new DelegatingFilterProxy("springSecurityFilterChain"))
            .addMappingForUrlPatterns(null,  true,  "/*");

        container.addFilter("FrontendContextFilter", new DelegatingFilterProxy("frontendContextProviderLocalFilter"))
            .addMappingForUrlPatterns(null,  true,  "/*");

        container.addFilter("hiddenHttpMethodFilter", new HiddenHttpMethodFilter()).addMappingForUrlPatterns(null,  true,  "/rest/*");;
    }

    public void registerRestDispatcher(ServletContext container) {
        AnnotationConfigWebApplicationContext restDispatcherContext = new AnnotationConfigWebApplicationContext();
        restDispatcherContext.register(RestCommonsMvcConfig.class);

        ServletRegistration.Dynamic restDispatcher = container.addServlet("rest-dispatcher", new DispatcherServlet(restDispatcherContext));
        restDispatcher.setLoadOnStartup(1);
        restDispatcher.addMapping("/rest/*");
    }

    public void registerDefaultDispatcher(ServletContext container) {
        AnnotationConfigWebApplicationContext dispatcherContext = new AnnotationConfigWebApplicationContext();
        dispatcherContext.register(MvcConfig.class);

        ServletRegistration.Dynamic dispatcher = container.addServlet("backend-dispatcher", new DispatcherServlet(dispatcherContext));
        dispatcher.setLoadOnStartup(1);
        dispatcher.addMapping("/*", "/index");
    }

}

MVC-config:

package net.lacho.opcenter.bootstrap;



@Configuration
@EnableWebMvc
@ComponentScan(basePackages = {"net.lacho.opcenter.ui"} )
public class MvcConfig extends WebMvcConfigurerAdapter {

    ... many lines removed, containing interceptors and velocity-config ...

@Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry
            .addResourceHandler("/_r/_s/**")
            .addResourceLocations("classpath:/static-resources/")
            .setCachePeriod(365 * 86400);
        registry
            .addResourceHandler("/_r/_d/**")
            .addResourceLocations("classpath:/static-uncached-resources/");
    }

    @Override
    public void configureDefaultServletHandling(
            DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
    }

Any idea anyone?

like image 224
Dirk Lachowski Avatar asked Mar 11 '14 14:03

Dirk Lachowski


2 Answers

It's not a bug, it's a feature...

As @axtavt and @rhinds have supposed, something is messing around which the content type. The browsers sends a correct Accept: application/json but spring ignores this and uses the extension of the url (aarrgh). From the docs:

16.16.4 Configuring Content Negotiation

You can configure how Spring MVC determines the requested media types from the client for
request mapping as well as for content negotiation purposes. The available options are to 
check the file extension in the request URI, the "Accept" header, a request parameter, as 
well as to fall back on a default content type. By default, file extension in the request 
URI is checked first and the "Accept" header is checked next.

The solution is quite simple as you can disable this "feature":

@Configuration
@EnableWebMvc
public class RestCommonsMvcConfig extends WebMvcConfigurerAdapter {

    @Override
    public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
        configurer.favorPathExtension(false);
    }

}

See also Spring does not ignore file extension for xml-config.

Related

  • Spring MVC @PathVariable with dot (.) is getting truncated
like image 148
Dirk Lachowski Avatar answered Nov 06 '22 02:11

Dirk Lachowski


I had required an extra step in my special case. I would like to add an extra information to @dirk-lachowski's great comment in case someone else needs. In my case I am importing an extension from WebMvcConfigurationSupport class

@Configuration
@EnableWebMvc
@Import({ SearchWebMvcConfigurationSupport.class })
public class SearchSpringConfiguration extends WebMvcConfigurerAdapter {
    @Override
    public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
        configurer.favorPathExtension(false);
    }
}

If you need to override requestMappingHandlerMapping method then you also have to add m.setUseSuffixPatternMatch(false);

@Configuration
public class SearchWebMvcConfigurationSupport extends WebMvcConfigurationSupport {
    @Override
    @Bean
    public RequestMappingHandlerMapping requestMappingHandlerMapping() {
        RequestMappingHandlerMapping m = super.requestMappingHandlerMapping();
        m.setAlwaysUseFullPath(true); // This makes your endpoints receive full path, including servlet mapping (you probably will not need this)
        m.setUseSuffixPatternMatch(false);
        return m;
    }
}
like image 1
Gokhan Celikkaya Avatar answered Nov 06 '22 03:11

Gokhan Celikkaya