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

2 comments:

  1. I'm getting the same error (when have web and webservices dependency)

    I need to consume a web service so I need both. Could you please tell me what is the fix?

    ReplyDelete
  2. Sorry for late response, to readers in future, you can try changing the MesseageDispatcherServlet's default path value by configuring it's property in application.properties/yml like below

    spring:
    web-services:
    path: /non-conflicting-services-path

    ReplyDelete