5

I'm trying to call a method in Spring (3.2.0) via AJAX using the following jQuery 1.6.

function updateRoleEnabled(id)
{
    $.ajax({
            datatype:"json",                        
            type: "PUT",
            url: "/wagafashion/ajax/UpdateUserRole.htm",
            data: "id="+id+"&t="+new Date().getTime(),
            success: function(response)
            {
            },
            error: function(e)
            {
                alert('Error: ' + e);
            }
    });
}

It attempts to invoke the following method in Spring.

@RequestMapping(value=("ajax/UpdateUserRole"), method=RequestMethod.PUT)
public @ResponseBody void updateUserRole(@RequestParam(value=("id")) String id)
{
    System.out.println("id = "+id);
}

FireFox responds with the following error.

HTTP Status 405 - Request method 'GET' not supported

type Status report

message Request method 'GET' not supported

description The specified HTTP method is not allowed for the requested resource (Request method 'GET' not supported).

Apache Tomcat/6.0.26

It works with the GET and POST methods and JSON (with Jackson-2.1.1) also works fine in other parts of the application.


If you need to see the dispatcher-servlet.xml file, the full contents is as follows.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:p="http://www.springframework.org/schema/p"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:tx="http://www.springframework.org/schema/tx"

       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc"

       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
       http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.2.xsd
       http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.2.xsd
       http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.2.xsd
       http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.2.xsd">

    <context:component-scan base-package="controller" />
    <context:component-scan base-package="validatorbeans" />

    <mvc:annotation-driven content-negotiation-manager="contentNegotiationManager" >
        <mvc:message-converters register-defaults="false">
        <bean id="jacksonMessageConverter" p:supportedMediaTypes="application/json" class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter"/>
    </mvc:message-converters>
    </mvc:annotation-driven>

    <bean id="contentNegotiationManager" class="org.springframework.web.accept.ContentNegotiationManagerFactoryBean">
        <property name="favorPathExtension" value="false" />
        <property name="favorParameter" value="false" />
        <property name="ignoreAcceptHeader" value="false" />
        <property name="mediaTypes" >
            <value>
                atom=application/atom+xml
                html=text/html
                json=application/json
                *=*/*
            </value>
        </property>
    </bean>

    <bean class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping" />
    <bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter"/>
    <bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
        <property name="exceptionMappings">
            <props>
                <prop key="org.springframework.web.multipart.MaxUploadSizeExceededException">
                    fileUploadingFailure
                </prop>
            </props>
        </property>
    </bean>
    <bean id="urlMapping" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <property name="mappings">
            <props>
                <prop key="index.htm">indexController</prop>
            </props>
        </property>
    </bean>

    <bean id="viewResolver"
          class="org.springframework.web.servlet.view.InternalResourceViewResolver"
          p:prefix="/WEB-INF/jsp/"
          p:suffix=".jsp" />

    <bean name="indexController"
          class="org.springframework.web.servlet.mvc.ParameterizableViewController"
          p:viewName="index" />
</beans>

How to make HTTP methods other than GET and POST work in Spring 3.2?


EDIT:

Based on the comment below, the following is my entire web.xml file.

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>
            /WEB-INF/applicationContext.xml
            /WEB-INF/spring-security.xml
        </param-value>
    </context-param>
    <filter>
        <filter-name>springSecurityFilterChain</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    </filter>

    <filter-mapping>
        <filter-name>springSecurityFilterChain</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>



    <filter>
        <filter-name>NoCacheFilter</filter-name>
        <filter-class>filter.NoCacheFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>NoCacheFilter</filter-name>
        <url-pattern>/admin_side/*</url-pattern>
    </filter-mapping>


    <filter>
        <filter-name>FileUploadFilter</filter-name>
        <filter-class>com.ckfinder.connector.FileUploadFilter</filter-class>
        <init-param>
            <param-name>sessionCookieName</param-name>
            <param-value>JSESSIONID</param-value>
        </init-param>
        <init-param>
            <param-name>sessionParameterName</param-name>
            <param-value>jsessionid</param-value>
        </init-param>
    </filter>


    <filter-mapping>
        <filter-name>FileUploadFilter</filter-name>
        <url-pattern>
                    /ckfinder/core/connector/java/connector.java
     </url-pattern>
    </filter-mapping>

    <filter>
        <filter-name>multipartFilter</filter-name>
        <filter-class>org.springframework.web.multipart.support.MultipartFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>multipartFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>


    <filter>
        <filter-name>httpMethodFilter</filter-name>
        <filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>httpMethodFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>


    <filter>
        <filter-name>openSessionInViewFilter</filter-name>
        <filter-class>org.springframework.orm.hibernate3.support.OpenSessionInViewFilter</filter-class>
        <init-param>
            <param-name>singleSession</param-name>
            <param-value>false</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>openSessionInViewFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>





    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    <listener>
        <description>ServletContextListener</description>
        <listener-class>listener.UnregisterDatabaseDrivers</listener-class>
    </listener>
    <servlet>
        <servlet-name>ConnectorServlet</servlet-name>
        <servlet-class>com.ckfinder.connector.ConnectorServlet</servlet-class>
        <init-param>
            <param-name>XMLConfig</param-name>
            <param-value>/WEB-INF/config.xml</param-value>
        </init-param>
        <init-param>
            <param-name>debug</param-name>
            <param-value>false</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>ConnectorServlet</servlet-name>
        <url-pattern>
                    /ckfinder/core/connector/java/connector.java
            </url-pattern>
    </servlet-mapping>


     <listener>
        <listener-class>
          org.springframework.security.web.session.HttpSessionEventPublisher
        </listener-class>
      </listener>




    <error-page>
        <description>Missing login</description>
        <error-code>401</error-code>
        <location>/WEB-INF/jsp/admin_side/ErrorPage.jsp</location>
    </error-page>

    <error-page>
        <description>Forbidden directory listing</description>
        <error-code>403</error-code>
        <location>/WEB-INF/jsp/admin_side/ErrorPage.jsp</location>
    </error-page>

    <error-page>
        <description>Missing page</description>
        <error-code>404</error-code>
        <location>/WEB-INF/jsp/admin_side/ErrorPage.jsp</location>
    </error-page>

    <error-page>
        <description>Uncaught exception</description>
        <error-code>500</error-code>
        <location>/WEB-INF/jsp/admin_side/ErrorPage.jsp</location>
    </error-page>

    <error-page>
        <description>Unsupported servlet method</description>
        <error-code>503</error-code>
        <location>/WEB-INF/jsp/admin_side/ErrorPage.jsp</location>
    </error-page>



    <servlet>
        <servlet-name>dispatcher</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>2</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>dispatcher</servlet-name>
        <url-pattern>*.htm</url-pattern>
    </servlet-mapping>
    <session-config>
        <session-timeout>
            30
        </session-timeout>
    </session-config>
    <welcome-file-list>
        <welcome-file>redirect.jsp</welcome-file>
    </welcome-file-list>
</web-app>
Tiny
  • 27,221
  • 105
  • 339
  • 599
  • To clarify further, I have `org.springframework.web.filter.HiddenHttpMethodFilter` configured in my web.xml file and Servlet works with other HTTP methods (including `GET` and `POST`). The problem in question was also working fine with the Spring framework 3.0.2 with no configurations at all. – Tiny Jan 19 '13 at 16:13
  • Are you sure that your browser really issues a `PUT` request? – axtavt Jan 19 '13 at 17:28
  • @axtavt -The browser doesn't issue a `PUT` request but jQuery (with an appropriate library) does it obviously and the `PUT` request works very well with the Spring framework 3.0.2 which I was working earlier with (I have just tried it out with the Spring version 3.0.2 prior to this comment). – Tiny Jan 19 '13 at 17:39
  • Can you post your Web.xml? – Jean-Philippe Bond Jan 19 '13 at 18:02
  • @Jean-Philippe Bond - Posted the entire `web.xml` file. – Tiny Jan 20 '13 at 04:27
  • Can I assume that in your POM you updated ALL relevant Spring dependencies, and that the outdated references such as `spring-context-3.0.xsd` are just an oversight? (`PUT` with 3.2 is just working fine for me, though I did hear someone complain about the same.) – Arjan Jan 23 '13 at 22:02
  • @Arjan - I'm not working with Maven projects and therefore, I don't have a `pom.xml` file in my application. The `PUT` method (i.e other than `GET` and `POST`) works with Spring 3.2 for me too (with ``) but it doesn't appear to be the case with AJAX in Spring 3.2. Additionally, I have updated the namespaces in `dispatcher-servlet.xml` and `application-context.xml` files according to the latest version. I have also upgraded Jackson from 1.9.8 to 2.1.1. The same problem is still reflected though. – Tiny Jan 24 '13 at 05:26
  • You know that with `` your browser does NOT need to send a `PUT`, right? So, you're not using that in jQuery, right? (You should either have jQuery send use just `PUT`, or have it use `GET` together with `_method=put`.) Did you peek into the HTTP requests? If it's indeed actually a GET, then the problem might be in Spring 3.2's `HiddenHttpMethodFilter` then. – Arjan Jan 24 '13 at 07:38
  • 1
    @ Arjan - Going through the stuff in my application carefully, I came to know that there was nothing wrong with the configuration. I just suddenly realized that I was using `@RequestParam` instead of using `@PathVariable` in the method parameter in the controller class. I have changed the `@RequestMapping` to `@RequestMapping(value=("ajax/{id}/UpdateUserRole"), method=RequestMethod.PUT)` and the method parameter to `updateUserRole(@PathVariable(value=("id")) String id)` and the requesting URL in the jQuery code to accommodate the `@PathVariable` and the whole approach worked as intended. – Tiny Jan 24 '13 at 09:41
  • 1
    I really apologize to all who wasted their time meaninglessly in reading this question. – Tiny Jan 24 '13 at 09:42

1 Answers1

2

Unless one is using only path parameters, processing a regular HTTP PUT needs some more work.

Since Spring 3.1, HttpPutFormContentFilter can be used to make @RequestParam work for application/x-www-form-urlencoded data:

Filter that makes form encoded data available through the ServletRequest.getParameter*() family of methods during HTTP PUT requests.

The Servlet spec requires form data to be available for HTTP POST but not for HTTP PUT requests. This filter intercepts HTTP PUT requests where content type is 'application/x-www-form-urlencoded', reads form encoded content from the body of the request, and wraps the ServletRequest in order to make the form data available as request parameters just like it is for HTTP POST requests.

However: this filter consumes the request's input stream, making it unavailable for converters such as FormHttpMessageConverter, like used for @RequestBody MultiValueMap<String, String> or HttpEntity<MultiValueMap<String, String>>. As a result, once you have configured the above filter in your application, you will get "IOException: stream closed" when invoking methods that use other converters that also expect raw application/x-www-form-urlencoded PUT data.


Alternatively one can do everything manually, using @RequestBody or HttpEntity<?>:

@RequestMapping(value="ajax/UpdateUserRole", method=RequestMethod.PUT,
    produces = MediaType.TEXT_PLAIN_VALUE)
public @ResponseBody String updateUserRole(
    @RequestBody final MultiValueMap<String, String> data,
    final HttpServletResponse response) {
  Map<String, String> params = data.toSingleValueMap();
  String id = params.get("id");
  String a = params.get("a");
  String b = params.get("b");
  if(id == null || a == null || b == null) {
    response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
    return null;
  }
  return "id = " + id;
}

See also an example using WebDataBinder, or use:

public ResponseEntity<String> updateUserRole(
    final HttpEntity<MultiValueMap<String, String>> entity) {
  Map<String, String> params = entity.getBody().toSingleValueMap();
  String id = params.get("id");
  ...

Note that for testing, using MockMvc's mockMvc.perform(put(url).param(name, value)) would actually also work with the code form the question, even though it would fail in a servlet container. But MockMvc is not running in such servlet container, hence is fooling you a bit.

MockMvc's .param(name, value) also works nicely with HttpPutFormContentFilter. But when using MockMvc to test @RequestBody or HttpEntity<?>, one also needs to create any application/x-www-form-urlencoded PUT content manually. Like:

mockMvc.perform(put(url).content("id=" + URLEncoder.encode(id, "UTF-8")
  + "&a=" + URLEncoder.encode(a, "UTF-8") + "&b=" + ...)

To be able to simply use .param(name, value), just like for GET and POST, one could define:

public static RequestPostProcessor convertParameters() {
  return new RequestPostProcessor() {
    @Override
    public MockHttpServletRequest postProcessRequest(
        final MockHttpServletRequest request) {
      if ("PUT".equalsIgnoreCase(request.getMethod()) {
        Map<String, String[]> params = request.getParameterMap();
        if (params != null) {
          StringBuilder content = new StringBuilder();
          for (Entry<String, String[]> es : params.entrySet()) {
            for (String value : es.getValue()) {
              try {
                content.append(URLEncoder.encode(es.getKey(), "UTF-8"))
                  .append("=")
                  .append(URLEncoder.encode(value, "UTF-8"))
                  .append("&");
              }
              catch (UnsupportedEncodingException e) {
                throw new IllegalArgumentException("UTF-8 not supported");
              }
            }
          }
          request.setParameters(new HashMap<String, String[]>());
          request.setContent(content.toString().getBytes());
          request.setContentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE);
        }
      }
      return request;
    }
  };
}

...and then use .with(convertParameters()) next to .param(name, value):

mockMvc.perform(put(url)
    .with(convertParameters())
    .param("id", id).param("a", a).param("b", b) ...)

Given all the above, simply using HttpPutFormContentFilter for application/x-www-form-urlencoded data really makes life easier.

When the browser is not sending application/x-www-form-urlencoded data, but things such as JSON, then trying to map to MultiValueMap will yield 415 Unsupported Media Type. Instead, use something like @RequestBody MyDTO data or HttpEntity<MyDTO> entity as explained in Parsing JSON in Spring MVC using Jackson JSON.

Community
  • 1
  • 1
Arjan
  • 22,808
  • 11
  • 61
  • 71
  • Wow! I didn't know that `@RequestBody` is used for this purpose. I will see it later in its appropriate place. Thank you for this point. – Tiny Jan 29 '13 at 14:07
  • When I tried the same thing as you mentioned, I have ended up with this error - `415 Unsupported Media Type`. In the request headers, I can see `Accept:*/*`. Could you please tell me the reason? I was planing to go with a separate question. – Tiny Jan 29 '13 at 15:17
  • Interesting, I guess even for `Accept:*/*` you need to configure content negotiation. Maybe simply using `@RequestMapping(..., produces = MediaType.APPLICATION_JSON_VALUE)` is enough already, if you have `com.fasterxml.jackson.core` version 2 or higher on your classpath. Or maybe `produces = MediaType.TEXT_PLAIN_VALUE`. Otherwise, see [17.15.4 Configuring Content Negotiation](http://static.springsource.org/spring-framework/docs/3.2.x/spring-framework-reference/html/mvc.html#mvc-config-content-negotiation) in the Spring documentation. – Arjan Jan 29 '13 at 16:59
  • Also note that I copied your example code into my answer. However, that `void` example is not returning any content. *Maybe* it is capturing the output of `System.out.println`, but I'd simply return a true object. – Arjan Jan 29 '13 at 17:30
  • I have tried with `produces=org.springframework.http.MediaType.APPLICATION_JSON_VALUE` but that made no difference. I have `contentNegotiationManager` configured in my `dispatcher-servlet.xml` file something like [this](http://stackoverflow.com/a/14102773/1037210). – Tiny Jan 29 '13 at 18:13
  • And yes I have `com.fasterxml.jackson.core` (Jackson 2.1.1) on the classpath (annotation and databind too). – Tiny Jan 29 '13 at 18:19
  • Configuration looks fine... Still then: what if you add `` or remove `register-defaults="false"` from ``? Meanwhile I've changed the code sample a bit (and added some notes about MockMvc). Are you actually `return`ing something in your test? – Arjan Jan 29 '13 at 19:22
  • I'm trying this at another place of my application and returning a message as a String. I have gone with a new question [here](http://stackoverflow.com/q/14590640/1391249). The new question might be simple enough to look into it. – Tiny Jan 29 '13 at 19:25
  • @Tiny, please don't edit for code formatting preferences. Your edit just introduced scroll bars in regular browsers. – Arjan Mar 01 '14 at 10:42