2

I'm building a small application to serve as a client for some third party library here at work. The API states that a Webhookis needed to respond some asynchronous events, but all their methods have the very same signature, apart from a changing _method field between the calls. For example, I have a _method = ping, media, etc.

I'd like to have separate methods on my controller to respond for each one of these methods. If the app allowed me to specify different URLs for each method it would be easy to use Spring MVC's @RequestMapping for each one of them. But I have to specify a single endpoint to receive all calls.

Is there a way (for example using Spring's HttpMessageConverter or something like that) to map different controller methods based on what the Request Body is? I've already tried with @RequestBody, @RequestParam but didn't seem to find anything.

I really, really didn't want to use a bunch of case, switch methods on a front controller to dispatch actions based on my _method field that comes with my POST data, so I happen to believe someone had this problem before and solved it intelligently.

Thanks a lot!

Edit 1: Providing source code

@Controller
@RequestMapping("/webhooks")
public class WebhookController {

    @RequestMapping(method = RequestMethod.POST, params = {"_method=ping"})
    @ResponseBody
    public String ping(){
        return "pong";
    }

    @RequestMapping(method = RequestMethod.POST, params = {"_method=media"})
    @ResponseBody
    public String media(){
        return "media";
    }
}

This is the answer:

{
  "timestamp": 1440875190389,
  "status": 400,
  "error": "Bad Request",
  "exception": "org.springframework.web.bind.UnsatisfiedServletRequestParameterException",
  "message": "Parameter conditions \"_method=ping\" not met for actual request parameters: ",
  "path": "/webhooks"
}
Community
  • 1
  • 1
Gustavo Ramos
  • 1,324
  • 1
  • 12
  • 23

2 Answers2

2

Right, I got it working. The answer is a bit tricky so I wanted to register it here should anyone have such problem.

@Neil McGuigan pointed me on the right direction on his comment but I didn't pay attention at first. The main culprit here is a very, very, very bad API design on our remote application's side.

_method is a field used to specify non-standard HTTP verbs such as PUT, PATCH, DELETE, TRACE and so on. This field is filtered by HiddenHttpMethodFilter and the HttpServletRequest is wrapped with this 'new' method. You can see at the file's source how it works.

As I wanted this _method field to get thru the filter without modifying the whole request (and causing the errors because there's no such verb as pingor message on `RequestMethod) I firstly had to deactivate the filter. This could be done by two ways:

  1. I could stop Spring Boot from automagically configuring Spring MVC, skipping WebMvcAutoConfiguration from being loaded when the ApplicationContext was loaded. As you can imagine this is a BIG, BIG, BIIIIG NO because, well, things could happen.

  2. I could use a FilterRegistrationBean to disable the bad filter. Pretty simple and straightforward, this was the method I chose to use:

    @Bean
    public FilterRegistrationBean registration(HiddenHttpMethodFilter filter) {
        FilterRegistrationBean registration = new FilterRegistrationBean(filter);
        registration.setEnabled(false);
        return registration;
    }
    

Last but not least, I decided to give HiddenHttpMethodFilter a little extension to somehow improve how the requests were getting thru. The Java EE Spec is pretty clear on the Servlet Spec Commandments where it states:

Thou should not alter your request on your side. You must respect the sender (something like that)

Though I agree with this, for the sake of my mental stability I decided to alter it anyway. To achieve this, we can use a simple HttpServletRequestWrapper, override the chosen methods and filter the original request with the wrapped part. I ended up doing something like this:

public class WhatoolsHiddenHttpMethodFilter extends OrderedHiddenHttpMethodFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String paramValue = request.getParameter(OrderedHiddenHttpMethodFilter.DEFAULT_METHOD_PARAM);
        if("POST".equals(request.getMethod()) && StringUtils.hasLength(paramValue)) {
            String method = paramValue.toUpperCase(Locale.ENGLISH);
            List<String> whatoolsMethods = Arrays.asList("ping", "message", "carbon", "media", "media_carbon", "ack");
            if(whatoolsMethods.contains(paramValue)){
                WhatoolsHiddenHttpMethodFilter.HttpMethodRequestWrapper wrapper = new WhatoolsHiddenHttpMethodFilter
                        .HttpMethodRequestWrapper(request, "POST", paramValue);
                filterChain.doFilter(wrapper, response);
            } else {
                WhatoolsHiddenHttpMethodFilter.HttpMethodRequestWrapper wrapper = new WhatoolsHiddenHttpMethodFilter
                        .HttpMethodRequestWrapper(request, method, null);
                filterChain.doFilter(wrapper, response);
            }
        } else {
            filterChain.doFilter(request, response);
        }
    }

    private static class HttpMethodRequestWrapper extends HttpServletRequestWrapper {
        private final String method;

        private final String whatoolsMethod;

        public HttpMethodRequestWrapper(HttpServletRequest request, String method, String whatoolsMethod) {
            super(request);
            this.method = method;
            this.whatoolsMethod = whatoolsMethod;
        }

        @Override
        public String getMethod() {
            return this.method;
        }

        @Override
        public String getHeader(String name) {
            if("x-whatools-method".equals(name)){
                return this.whatoolsMethod;
            }
            return super.getHeader(name);
        }

        @Override
        public Enumeration<String> getHeaderNames() {
            List<String> names = Collections.list(super.getHeaderNames());
            if(this.whatoolsMethod != null){
                names.add("x-whatools-method");
            }
            return Collections.enumeration(names);
        }
    }
}

So, what this does is to wrap the request with a new x-whatools-method header when the header is in my whatoolsMethods list. With this, I can easily use @RequestMapping's headers property and map the requests to the correct controller methdods.

Back to the initial question, I'm almost sure (well, 99,95% should be completely sure but let's not risk it) the params property on @RequestMapping works only for request parameters on GET URIs, e.g http://foo.bar/?baz=42. It won't work filtering parameters sent on the request's body.

Thanks Neil for your guidance, even if small! I hope this helps someone.

Gustavo Ramos
  • 1,324
  • 1
  • 12
  • 23
1

You can use params in a request mapping:

@RequestMapping(value="/foo", params={"_method=ping"})

Assuming these are post parameters that is

params DOES work for POST, I promise you

Here's my controller:

@Controller
@RequestMapping("/test1")
public class ParamTestController {

    @RequestMapping(method = RequestMethod.POST)
    @ResponseBody String getA(){
        return "A";
    }

    @RequestMapping(method = RequestMethod.POST, params = {"b"})
    @ResponseBody String getB(){
        return "B";
    }
}

Here's my test:

enter image description here

Neil McGuigan
  • 46,580
  • 12
  • 123
  • 152