Monday, 25 June 2018

Spring Boot /services endpoint weirdness (405 Method Not Allowed)

This was an error that I encountered a few years back and it's still alive and well to trap programmers who use this feature unknowingly.
I had following simple code in which first method /services_1 worked and /services didn't work. It simply returns 405 Method Not Allowed status with empty response.
@RestController
@RestController
class TestRest {
    @GetMapping("/services")
    public String test() {
        return "Hello World!";
    }
    @GetMapping("/services_1")
    public String test2() {
        return "Hello World!";
    }
}
So I started looking around and found that I had extra dependency in POM for web services, which was not being used and /services endpoint worked if I removed it. Following is the dependency in question
<dependency>
    <groupid>org.springframework.boot</groupid>
    <artifactid>spring-boot-starter-web-services</artifactid>
</dependency>
This is a dependency used for SOAP web services support in Spring Boot. But why would Spring stop /services from being accessed in presence of this dependency? I was sure at this point that there was probably an extra servlet loaded by Spring which was hogging all requests to /services and thus intended REST method was not being called.
After debugging further I decided to list all the Servlets registered by Spring Boot and print there url-mappings to find out offending servlet using following code
@Autowired
private List<ServletRegistrationBean> servlets;
...
for(ServletRegistrationBean servlet: servlets) {
    System.out.println(servlet.getServletName() + ": " + Arrays.toString(servlet.getUrlMappings().toArray()));
}
I know it's crude but it works. Following is the output that I got,
dispatcherServlet: [/]
messageDispatcherServlet: [/services/*]
So there's the extra messageDispatcherServlet that I didn't need and was overriding /services method declared by my REST class.
Now why does it happen? It's because messageDispatcherServlet's default url-mapping is done to /services.
It's done in following property configuration class
package org.springframework.boot.autoconfigure.webservices;
@ConfigurationProperties(prefix = "spring.webservices")
public class WebServicesProperties {
    /**
     * Path that serves as the base URI for the services.
     */
    private String path = "/services";
and used in following class
class: org.springframework.boot.autoconfigure.webservices.WebServicesAutoConfiguration

public class WebServicesAutoConfiguration {
    private final WebServicesProperties properties;

    @Bean
    public ServletRegistrationBean messageDispatcherServlet(
            ApplicationContext applicationContext) {
        MessageDispatcherServlet servlet = new MessageDispatcherServlet();
        servlet.setApplicationContext(applicationContext);
        String path = this.properties.getPath();
        String urlMapping = (path.endsWith("/") ? path + "*" : path + "/*");
        ServletRegistrationBean registration = new ServletRegistrationBean<>(
                servlet, urlMapping);
So that took a while to figure out, I must say a message or an error from Spring would have been nice and helpful to figure out what was going on.

...till next time