jsonspring-mvcsmartgwtsame-origin-policy

SmartGWT RestDataSource to Spring REST Controller GET returns 415 Error


This problem has been bugging me all day, and I have spent a lot of time here, and on Google trying to find the right answer and trying lots of different fixes.

I have a Spring MVC Controller which is defined here:

@RequestMapping(value = "/searchAndCount", method = RequestMethod.GET, produces = "application/json", headers =
{ "Accept=application/json", "Content-Type=application/json" }, consumes = "application/json")
public @ResponseBody
RequestResults<?> searchAndCount(@RequestBody SearchInvoiceDTO searchInvoiceDto)
{
      RequestResults<?> requestResults = invoiceApprovalService.searchAndCount(searchInvoiceDto);
    return requestResults;
}

I know that with most gets, simple parameters can get sent back, but in this case, I found it better to put all my search criteria in one object and send that over. Which is why I am doing a @RequestBody.

Per previous fixes, I made sure that this has both headers that may be needed to accept JSON output.

The JSON String looks like: String s1 = "{\"userId\":3, \"ddUserId\":301010651, \"customerCode\":\"QA\", \"customerId\":8}"; And yes, I have used the Jackson ObjectMapper tool verify this code will map correctly from this String to an Object, and vice-versa. When I look at the POJO, it does implement Serializable, and it does have a default constructor.

The Junit test works awesome and does return data:

MockHttpServletRequestBuilder requestBuilder =
        MockMvcRequestBuilders.get("/invoices/searchAndCount").contentType(MediaType.APPLICATION_JSON)
        .content(test);

    this.mockMvc.perform(requestBuilder).andDo(print());

This does make the call to the Controller, I can see from the output what the headers are, and I can see that I actually get back real data which is great. So I feel there is not much more I can do from he controller side.

The real call to the controller comes from the SmartGWT RestDataSource.

The RequestMethod is defined in the datasource, here is the init method:

private InvoiceDataSource(String id)
{
    setID(id);
    setClientOnly(false);

    // set up FETCH to use GET requests
    OperationBinding fetch = new OperationBinding();
    fetch.setOperationType(DSOperationType.FETCH);
    fetch.setDataProtocol(DSProtocol.POSTMESSAGE);
    fetch.setDataFormat(DSDataFormat.JSON);
    DSRequest fetchProps = new DSRequest();
    fetchProps.setHttpMethod("GET");
    fetch.setRequestProperties(fetchProps);

    // set up ADD to use POST requests
    OperationBinding add = new OperationBinding();
    add.setOperationType(DSOperationType.ADD);
    add.setDataProtocol(DSProtocol.POSTMESSAGE);
    // ===========================================
    DSRequest addProps = new DSRequest();
    addProps.setHttpMethod("POST");
    add.setRequestProperties(addProps);

    // set up UPDATE to use PUT
    OperationBinding update = new OperationBinding();
    update.setOperationType(DSOperationType.UPDATE);
    update.setDataProtocol(DSProtocol.POSTMESSAGE);
    // ===========================================
    DSRequest updateProps = new DSRequest();
    updateProps.setHttpMethod("PUT");
    // updateProps.setContentType("application/json");
    update.setRequestProperties(updateProps);

    // set up REMOVE to use DELETE
    OperationBinding remove = new OperationBinding();
    remove.setOperationType(DSOperationType.REMOVE);
    DSRequest removeProps = new DSRequest();
    removeProps.setHttpMethod("DELETE");
    remove.setRequestProperties(removeProps);

    // apply all the operational bindings
    setOperationBindings(fetch, add, update, remove);

    init();
}

The Fetch is set to POSTMESSAGE which seems to be the best way to pass data using transformReponse.

@Override
protected Object transformRequest(DSRequest dsRequest)
{
    // gets the correct URL - (web-app-root)/rest/invoices/searchAndCount
    postProcessTransform(dsRequest);

    System.out.println("InvoiceDataSource: transformRequest: START");
    dsRequest.setContentType("application/json");
    JavaScriptObject jso = dsRequest.getData();

    // setting more headers, but this doesn't seem to change anything
    dsRequest.setAttribute("Access-Control-Allow-Origin", "*");
    dsRequest.setAttribute("Content-Type", "application/json");
    dsRequest.setAttribute("Accept", "application/json");

    String s1 = JSON.encode(jso);
    System.out.println("InvoiceDataSource: transformRequest: FINISH: s1=" + s1);
    return s1;
}

Since I know I have he correct URL, I also know I am spitting out that the variable "s1" also has the correct JSON data, and again I did test that JSON to make sure it would hit the controller correctly.

I also do have dependencies for Jackson as defined in the pom.xml file. I also have the message converters set in the springmvc-servlet.xml file. If these were not correct, the unit test would not work. However, if you need to see the pom.xml file or the springmvc-servlet.xml file, please let me know.

I have been researching and trying lots of things all day long now, and so far ... no luck. I hope I have provided enough information, but if you need more, please let me know. Ultimately, I hope I can tweak my SmartGWT RestDataSource to pass in the correct data to this controller to actually get data out of it.

UPDATE: When I run this with Jetty in Eclipse, I am using Firefox 23.0.1 to open to my web-app. Within the Console in Eclipse, here is what I can see:

[WARN] 415 - GET /rest/invoices/searchAndCount (127.0.0.1) 1440 bytes
Request headers
  Host: 127.0.0.1:8888
  User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:23.0) Gecko/20100101 Firefox/23.0
  Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
  Accept-Language: en-US,en;q=0.5
  Accept-Encoding: gzip, deflate
  Cookie: GLog=%7B%0D%20%20%20%20left%3A22%2C%20%0D%20%20%20%20top%3A11%2C%20%0D%20%20%20%20width%3A705%2C%20%0D%20%20%20%20height%3A855%2C%20%0D%20%20%20%20priorityDefaults%3A%7B%0D%20%20%20%20%20%20%20%20Log%3A4%0D%20%20%20%20%7D%2C%20%0D%20%20%20%20defaultPriority%3A3%2C%20%0D%20%20%20%20trackRPC%3Atrue%0D%7D
   Connection: keep-alive
   If-Modified-Since: Thu, 01 Jan 1970 00:00:00 GMT
 Response headers
  Content-Type: text/html; charset=iso-8859-1
  Content-Length: 1440
  Accept: application/json

Notice that the Request header: Accept: text/html,application/xhtml+xml,application/xml;q=0.9,/;q=0.8 does not show application/json Also, the Request header: "Content-Type" is not present

When I use Chrome, the result is:

[WARN] 415 - GET /rest/invoices/searchAndCount (127.0.0.1) 1440 bytes
Request headers
   Host: 127.0.0.1:8888
   Connection: keep-alive
   Accept: */*
   User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.57 Safari/537.36
   DNT: 1
   Accept-Encoding: gzip,deflate,sdch
   Accept-Language: en-US,en;q=0.8
   If-Modified-Since: Thu, 01 Jan 1970 00:00:00 GMT
   Cache-Control: max-age=0
Response headers
   Content-Type: text/html; charset=iso-8859-1
   Content-Length: 1440
   Accept: application/json

When I run from the JUnit test, there is always the "Content-Type:application/json" present. So, it seems that although I tell the SmartGWT RestDataSource that I am using JSON in several places ... the web-services call is not creating the right header.

UPDATE: I added the following code to the SmartGWT RestDataSource transformRequest method:

    Map<String, String> httpHeaders = new HashMap<String, String>();
    httpHeaders.put("Accept", "*/*");
    httpHeaders.put("Content-Type", "application/json");
    dsRequest.setHttpHeaders(httpHeaders);

I can add the "Accept" Request-Header and I still got the 415 Unsupported Media error Message. When I add the "Content-Type" Request-Header, then I get a 400 BAD REQUEST error message.

I get this in the console now:

[WARN] 400 - GET /rest/invoices/searchAndCount (127.0.0.1) 1418 bytes
Request headers
   Host: 127.0.0.1:8888
   User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:23.0) Gecko/20100101 Firefox/23.0
   Accept: */*
   Accept-Language: en-US,en;q=0.5
   Accept-Encoding: gzip, deflate
   Connection: keep-alive
   If-Modified-Since: Thu, 01 Jan 1970 00:00:00 GMT
   Content-Type: application/json
Response headers
   Content-Type: text/html; charset=iso-8859-1
   Content-Length: 1418

Solution

  • This took a lot of work, but I finally figured it out.

    The verb GET doesn' work for my needs ... I am an idiot. No amount of work is going to make a GET work with sending my parameters as an object in the request data. So let me show the code that I had to change to make this work.

    The SmartGWT RestDataSource, I changed; fetchProps.setHttpMethod("GET"); to fetchProps.setHttpMethod("POST"); an I still kept: fetch.setDataProtocol(DSProtocol.POSTMESSAGE);

        // set up FETCH to use GET requests
        OperationBinding fetch = new OperationBinding();
        fetch.setOperationType(DSOperationType.FETCH);
        fetch.setDataProtocol(DSProtocol.POSTMESSAGE);
        fetch.setDataFormat(DSDataFormat.JSON);
        // ===========================================
        DSRequest fetchProps = new DSRequest();
        fetchProps.setHttpMethod("POST");
        fetchProps.setContentType("application/json");
        fetch.setRequestProperties(fetchProps);
    

    Also in the same RestDataSource, in the "transformRequest" method, I added:

        Map<String, String> httpHeaders = new HashMap<String, String>();
        httpHeaders.put("Content-Type", "application/json");
        httpHeaders.put("Accept", "application/json");
        dsRequest.setHttpHeaders(httpHeaders);
    

    This made sure that whatever browser I was using, these two headers are being manually set.

    The Spring MVC Controller resides in the same web-app, so for now, this avoids any SOP cross-site domain issues until I can test that out. In the meantime, the header for my control looks like:

    @RequestMapping(value = "/searchAndCount", method = RequestMethod.POST, headers =
    { "Accept=application/json", "Content-Type=application/json" })
    public @ResponseBody
    ArrayList<?> searchAndCountPOST(@RequestBody SearchInvoiceDTO searchInvoiceDto)
    

    This work great to get called, and it returned my data. The original unit test I had was doing a MockMVc.get which worked and through me off. It must have recognized data coming through and changed the GET to a POST to make it work. My unit test was changed to a POST now, and that also works well.

    I hope all this work pays off for someone else!