23

I'm digging myself in trying to send a POST request with a JSON payload to a remote server.

This GET curl command works fine:

curl -H "Accept:application/json" --user aaa@aaa.com:aaa "http://www.aaa.com:8080/aaa-project-rest/api/users/1" -i

And this POST one works fine too:

curl -H "Accept:application/json" -H "Content-Type: application/json" "http://www.aaa.com:8080/aaa-project-rest/api/users/login" -X POST -d "{ \"email\" : \"aaa@aaa.com\", \"password\" : \"aaa\" }" -i

And so I'm trying to mimic it in my Android app.

The app works fine on the first GET request but gives a 400 Bad Request on the second POST one.

Here is the code that works for the GET request:

  RestTemplate restTemplate = new RestTemplate();
  restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
  HttpHeaders httpHeaders = Common.createAuthenticationHeaders("aaa@aaa.com" + ":" + "aaa");
  User user = null;
  ResponseEntity<User> responseEntity = restTemplate.exchange("http://" + REST_HOST + ":8080/aaa-project-rest/api/users/" + 1L, HttpMethod.GET, new HttpEntity<Object>(httpHeaders), User.class);

Here is the source code for the POST request:

RestTemplate restTemplate = new RestTemplate();
restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
User user = null;
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
httpHeaders.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));
JSONObject jsonCredentials = new JSONObject();
jsonCredentials.put("email", REST_LOGIN);
jsonCredentials.put("password", REST_PASSWORD);
ResponseEntity<User> responseEntity = restTemplate.exchange("http://" + REST_HOST + ":" + REST_PORT + "/" + REST_APP + "/api/users/login",
        HttpMethod.POST, new HttpEntity<Object>(jsonCredentials, httpHeaders), User.class);

But it gives the message:

Could not write request: no suitable HttpMessageConverter found for request type [org.json.JSONObject] and content type [application/json]

Here is the Spring REST controller:

@RequestMapping(value = RESTConstants.SLASH + RESTConstants.LOGIN, method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public ResponseEntity<UserResource> login(@Valid @RequestBody CredentialsResource credentialsResource, UriComponentsBuilder builder) {
    HttpHeaders responseHeaders = new HttpHeaders();
    User user = credentialsService.checkPassword(credentialsResource);
    userService.clearReadablePassword(user);
    if (user == null) {
        return new ResponseEntity<UserResource>(responseHeaders, HttpStatus.NOT_FOUND);
    } else {
        tokenAuthenticationService.addTokenToResponseHeader(responseHeaders, credentialsResource.getEmail());
        responseHeaders.setLocation(builder.path(RESTConstants.SLASH + RESTConstants.USERS + RESTConstants.SLASH + "{id}").buildAndExpand(user.getId()).toUri());
        UserResource createdUserResource = userResourceAssembler.toResource(user);
        ResponseEntity<UserResource> responseEntity = new ResponseEntity<UserResource>(createdUserResource, responseHeaders, HttpStatus.CREATED);
        return responseEntity;
    }
}

@RequestMapping(value = RESTConstants.SLASH + "{id}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public ResponseEntity<UserResource> findById(@PathVariable Long id, UriComponentsBuilder builder) {
    HttpHeaders responseHeaders = new HttpHeaders();
    User user = userService.findById(id);
    if (user == null) {
        return new ResponseEntity<UserResource>(responseHeaders, HttpStatus.NOT_FOUND);
    } else {
        UserResource userResource = userResourceAssembler.toResource(user);
        responseHeaders.setLocation(builder.path(RESTConstants.SLASH + RESTConstants.USERS + RESTConstants.SLASH + "{id}").buildAndExpand(user.getId()).toUri());
        ResponseEntity<UserResource> responseEntity = new ResponseEntity<UserResource>(userResource, responseHeaders, HttpStatus.OK);
        return responseEntity;
    }
}

The CredentialsResource class code:

public class CredentialsResource extends ResourceSupport {

    @NotEmpty
    @Email
    private String email;
    @NotEmpty
    private String password;

    public CredentialsResource() {
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

}
Stephane
  • 11,836
  • 25
  • 112
  • 175
  • Can you show definition of the REST Controller on the server? – Sergey Yamshchikov Mar 05 '15 at 08:16
  • @SergeyYamshchikov I added the controller. Also I notice now the response is a 401 Unauthorized – Stephane Mar 05 '15 at 08:50
  • The curl request is users/1 but the android is users/login – Javi Mollá Mar 05 '15 at 08:50
  • For the unauthorized part, you're not sending the user/pass on the headers – Javi Mollá Mar 05 '15 at 08:50
  • @JavierMollá Spot on. I just also noticed it. I changed the example curl request to match it to the application request. – Stephane Mar 05 '15 at 08:52
  • @JavierMollá Do you see anything wrong in the ways I try passing the header ? – Stephane Mar 05 '15 at 08:53
  • Sorry guys. There was a typo I introduced in the application request which triggered the 401. Now that it is corrected the response is a beautiful 200. – Stephane Mar 05 '15 at 08:55
  • I was messing up my question with two different requests. I cleaned up the whole test and it hopefully now makes sense to the new reader. – Stephane Mar 05 '15 at 09:40
  • What I undestand from your requests is that /users/1 works with basic authentication but /users/login does not (You're not passing credentials in the same way). Is it right? – Javi Mollá Mar 05 '15 at 10:16
  • @JavierMollá Exactly. The login url request passes a json object with the credentials, and not in the headers. The demo curl shows this to work. – Stephane Mar 05 '15 at 10:18
  • Can you post CredentialsResource class code? – Javi Mollá Mar 05 '15 at 10:22
  • I posted the CredentialsResource class code. – Stephane Mar 05 '15 at 10:37
  • I wonder why the app request makes the server look up for a single string constructor, when the example curl one does not. – Stephane Mar 05 '15 at 10:39
  • At first, this was a REST Angular app and I'm tried adding an Android client to it. I first do a login request and then the server creates a JWT JSON Web Token which is used in subsequent requests. You reckon I could move this: tokenAuthenticationService.addTokenToResponseHeader(responseHeaders, credentialsResource.getEmail()); out of the login controller and into the security filter ? Then I would not need the login controller any longer.. I wonder what is best, when doing a JWT, if it is to use the login controller or a security filter. Is it a good design to mix Basic auth with JWT ? – Stephane Mar 05 '15 at 10:49
  • Answer with explanation: https://stackoverflow.com/a/56366641/6346531 – aksh1618 Sep 11 '19 at 16:47

5 Answers5

29

Quite late to reply, though I've just hitten the same problem and took me some time to solve it. So, I think I'd better share it and keep track of my solution.

Actually, the exception thrown is totally misleading. Turned out the problem is not that the MappingJackson2HttpMessageConverter didn't know how to marshall my object -which sounded strange, being JSON-, but a configuration of the underlying ObjectMapper.

What I did is to disable the property SerializationFeature.FAIL_ON_EMPTY_BEANS like that

restTemplate = new RestTemplate();
MappingJackson2HttpMessageConverter jsonHttpMessageConverter = new MappingJackson2HttpMessageConverter();
jsonHttpMessageConverter.getObjectMapper().configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
restTemplate.getMessageConverters().add(jsonHttpMessageConverter);

and everything started working as expected.

Stefano Cazzola
  • 1,597
  • 1
  • 20
  • 36
  • 9
    I encountered the same issue and found it to be because of the absence of getters and setters to the POJO that I was trying to "jsonify". After adding the getters and setters, the default MappingJackson2HttpMessageConverter() worked just fine. – Vivek Sethi Apr 30 '18 at 10:26
  • Thanks @VivekSethi, my issue was missing getters and setters too. – Steve Jul 04 '18 at 21:34
  • For me was also missing getters, thanks @VivekSethi. – czdepski Sep 05 '19 at 15:32
9

I had to do a few things to get this working.

First I had to convert the JSONObject to a string, as in:

HttpEntity<String> entityCredentials = new HttpEntity<String>(jsonCredentials.toString(), httpHeaders);

The reason being that there is no mapping message converter for the JSONObject class while there is one for the String class.

Second I had to pass a true value to the RestTemplate constructor. Failing to do so, I would get a 400 Bad Request.

RestTemplate restTemplate = new RestTemplate(true);

The true value tells the rest template to use the default converters. If someone knows why this is so, I'd be happy to know more about.

Third I removed the unneeded Jackson converter:

restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());

With these things done, the request works just fine.

Here is the full code:

RestTemplate restTemplate = new RestTemplate(true);    
User user = null;
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setContentType(MediaType.APPLICATION_JSON);
httpHeaders.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));
try {
    JSONObject jsonCredentials = new JSONObject();
    jsonCredentials.put("email", REST_LOGIN);
    jsonCredentials.put("password", REST_PASSWORD);
    Log.e(Constants.APP_NAME, ">>>>>>>>>>>>>>>> JSON credentials " + jsonCredentials.toString());
    HttpEntity<String> entityCredentials = new HttpEntity<String>(jsonCredentials.toString(), httpHeaders);
    ResponseEntity<User> responseEntity = restTemplate.exchange("http://" + REST_HOST + ":" + REST_PORT + "/" + REST_APP + "/api/users/login",
            HttpMethod.POST, entityCredentials, User.class);
    if (responseEntity != null) {
        user = responseEntity.getBody();
    }
    return user;
} catch (Exception e) {
    Log.e(Constants.APP_NAME, ">>>>>>>>>>>>>>>> " + e.getLocalizedMessage());
}
return null;

I suspect there could be a way to explicitly use a Jackson converter and skip the true value in the rest template constructor, but this is just a guess.

Stephane
  • 11,836
  • 25
  • 112
  • 175
  • 1
    does your User class implement serializable, has public getters and setters and a public empty default constructor? Because spring boot should than automatically configure & use Jackson and serialize/deserialize your DTO class fine. – Gewure Mar 02 '19 at 16:41
  • Good question but I left that project years ago and haven't touch native Android coding since. – Stephane Mar 04 '19 at 09:25
  • maybe you remember ;) – Gewure Mar 04 '19 at 19:47
1

The Exception "no suitable HTTPMessageConverter found" is thrown, if not JSON implementation is present in the class path. Have a look at RestTemplate source code:

public RestTemplate() {
    this.messageConverters.add(new ByteArrayHttpMessageConverter());
    this.messageConverters.add(new StringHttpMessageConverter());
    this.messageConverters.add(new ResourceHttpMessageConverter());
    this.messageConverters.add(new SourceHttpMessageConverter<Source>());
    this.messageConverters.add(new AllEncompassingFormHttpMessageConverter());

    if (romePresent) {
        this.messageConverters.add(new AtomFeedHttpMessageConverter());
        this.messageConverters.add(new RssChannelHttpMessageConverter());
    }

    if (jackson2XmlPresent) {
        this.messageConverters.add(new MappingJackson2XmlHttpMessageConverter());
    }
    else if (jaxb2Present) {
        this.messageConverters.add(new Jaxb2RootElementHttpMessageConverter());
    }

    if (jackson2Present) {
        this.messageConverters.add(new MappingJackson2HttpMessageConverter());
    }
    else if (gsonPresent) {
        this.messageConverters.add(new GsonHttpMessageConverter());
    }
}

Add a JSON implementation to your class path. E. g.:

implementation "com.fasterxml.jackson.core:jackson-databind:2.9.0"
andy
  • 1,035
  • 1
  • 13
  • 35
0

I know... is too late. But here I am.

I fixed this sending a Java Object instead a JSONObject, something like this:

JSONObject jsonCredentials = new JSONObject();
jsonCredentials.put("email", REST_LOGIN);
jsonCredentials.put("password", REST_PASSWORD);

// Generic Object. It might be a DTO.
Object jsonCredentialsObj = JsonUtils.stringToObject(jsonCredentials.toString(), Object.class);

ResponseEntity<User> responseEntity = restTemplate.exchange("http://" + REST_HOST + ":" + REST_PORT + "/" + REST_APP + "/api/users/login",
        HttpMethod.POST, new HttpEntity<Object>(jsonCredentialsObj, httpHeaders), User.class);

Where JsonUtils.stringToObject is my own convert utility.

0

Spring's RestTemplate does not know how to serialize the Android org.json.JSONObject class. (org.json.JSONObject is not available in "normal"/desktop Java)

RestTemplate for Android supports

Jackson JSON Processor, Jackson 2.x, and Google Gson.

per: https://docs.spring.io/autorepo/docs/spring-android/1.0.1.RELEASE/reference/html/rest-template.html

With Jackson on the classpath RestTemplate should understand how to serialize Java beans.

CredentialsResource cr = new CredentialsResource();
cr.setEmail(...);
cr.setPassword(...);

HttpEntity<CredentialsResource> entityCredentials = new HttpEntity<CredentialsResource>(cr, httpHeaders);

P.S. It would be pretty easy to write your own JSONObjectHttpMessageConverter if you wanted to carry on using JSONObject, your converter should just have to call .toString()

teknopaul
  • 6,505
  • 2
  • 30
  • 24