xebia-functional / appsly-android-rest

An Android client library for RESTful based web services.
209 stars 51 forks source link

Problem with POST method #43

Closed joshid closed 10 years ago

joshid commented 10 years ago

I'm having issues trying to post. Using Fetcher returns HTTP 200 response but with appsly-rest I have a 415 HTTP Unsupported Data Type error.

I've used Charles to monitor HTTP traffic and don't see nothing wrong.

This is code affected by error:

API Rest

@POST("/mensajeria/docente/{canal}/{codCentro}/{codPeriodo}") void postMensajeriaDocente(@Header("Authorization") String token, @Header("Accept") String accept, @Path("canal") String canal, @Path("codCentro") String codCentro, @Path("codPeriodo") String codPeriodo, @Body WSMensaje mensaje, Callback<List> callback);

Body class

public class WSMensaje implements Serializable {

@JsonProperty("ASUNTO")
private String mensajeAsunto;
@JsonProperty("TEXTO")
private String mensajeTexto;
@JsonProperty("DESTINATARIOS")
private List<String> mensajeDestinatarios; 

@JsonCreator
public WSMensaje(@JsonProperty("ASUNTO") String mensajeAsunto,
                 @JsonProperty("TEXTO") String mensajeTexto,
                 @JsonProperty("DESTINATARIOS") List<String> mensajeDestinatarios) {

    this.mensajeAsunto = mensajeAsunto;
    this.mensajeTexto = mensajeTexto;
    this.mensajeDestinatarios = mensajeDestinatarios;

}

// Getters and Setters
...

API Call

List managerList = new ArrayList(); managerList.add("123456");

WSMensaje msg = new WSMensaje("test", "test", managerList);

restApi.postMensajeriaDocente(bearerToken, "application/json", msgChannel, centerCode, periodCode, msg, new Callback<List>() {

  @Override
  public void onResponse(Response<List<WSMensajeResultado>> response) {
         ....
  }

});

raulraja commented 10 years ago

What happens when you get rid of all those @Header types? The default content type should already be application/json. Also what kind of messages do you see in the LogCat when you filter by appsly. For authorization or any default headers you can set those when you create the RestClient instance. Can you also share the code you are using to initialize the RestClient?

raulraja commented 10 years ago

Also can you share the request body you are using in fetcher and the server Response? The mappings may be wrong.

joshid commented 10 years ago

fetcher

joshid commented 10 years ago

The init of RestClient:

MiradorAPI restApi; RestClient client;

client = RestClientFactory.defaultClient(getActivity().getApplicationContext()); restApi = RestServiceFactory.getService(SERVER_BASE_URL, MiradorAPI.class, client);

joshid commented 10 years ago

I'm only getting 415 HTTP Error in PUT/POST methods. GET method runs fine. That's why I think the point is in Body class mapping (or maybe in its constructor). I've seen sometimes it's needed a dummy constructor in the class (I've tested and dont work for me)

raulraja commented 10 years ago

A @JsonCreator is only to construct instances from responses. You should have separate request and response objects. Try getting rid of that constructor and making your request object match the expected request body. Jackson does not know how to name the json properties by looking at your contructor.

joshid commented 10 years ago

It's quite weird but it seems that if body is in lowercase the WebService returns error in Fetcher, but if body is in uppercase then webservice returns OK.

Jackson allways sends body in lowercase. Is possible to send JSON body in uppercase?

raulraja commented 10 years ago

yes

public class MyRequest {

 @JsonProperty("NAME")
 private String name;

...

}

should turn into

{
"NAME":"Pepe"
}
joshid commented 10 years ago

Still not work... :-(

LogCat

07-03 16:33:51.061 1516-1516/com.educarm.mirador D/android-rest﹕ main -> invoking: http://caspruebas.murciaeduca.es/pxxi-profesores-rs/mensajeria/docente/sms/30008650/201314 with body: com.educarm.mirador.model.webservices.WSMensaje@527cf438 and temporary request content type: application/json 07-03 16:33:51.061 1516-1516/com.educarm.mirador D/android-rest﹕ main -> JacksonBodyConverter.toRequestBody: object: com.educarm.mirador.model.webservices.WSMensaje@527cf438 07-03 16:33:51.085 1516-1516/com.educarm.mirador D/dalvikvm﹕ GC_FOR_ALLOC freed 1193K, 17% free 7743K/9232K, paused 4ms, total 4ms 07-03 16:33:51.089 1516-1516/com.educarm.mirador D/android-rest﹕ main -> JacksonHttpFormValuesConverter.toRequestBody: json: {"ASUNTO":"test","DESTINATARIOS":["3427217"],"TEXTO":"test"} 07-03 16:33:51.089 1516-1516/com.educarm.mirador D/android-rest﹕ main -> JacksonBodyConverter.toRequestBody: result: org.apache.http.entity.StringEntity@527efce4 07-03 16:33:51.089 1516-1516/com.educarm.mirador D/android-rest﹕ main -> CacheAwareHttpClient.sendRequest 07-03 16:33:51.089 1516-1516/com.educarm.mirador D/android-rest﹕ main -> CacheAwareHttpClient.sendRequest.onPostExecute proceeding uncached 07-03 16:33:51.209 1516-1516/com.educarm.mirador D/android-rest﹕ main -> CacheAwareCallback. Loading from cache after response error: Tipo de Medio No Soportado result with cached result: null 07-03 16:33:51.209 1516-1563/com.educarm.mirador D/android-rest﹕ Thread-118 -> parsingResponse: intoTargetClass: class com.educarm.mirador.model.webservices.WSMensajeResultadoresponseBody: 07-03 16:33:51.209 1516-1563/com.educarm.mirador D/android-rest﹕ Thread-118 -> JacksonBodyConverter.fromResponseBody: target: class com.educarm.mirador.model.webservices.WSMensajeResultado responseBody: org.apache.http.entity.StringEntity@5297ed90 07-03 16:33:51.209 1516-1563/com.educarm.mirador D/BaseJsonHttpResponseHandler﹕ parseResponse thrown an problem ly.apps.android.rest.exceptions.SerializationException: java.io.EOFException: No content to map to Object due to end of input at ly.apps.android.rest.converters.impl.JacksonBodyConverter.fromResponseBody(JacksonBodyConverter.java:91) at ly.apps.android.rest.converters.impl.DelegatingConverterService.fromResponseBody(DelegatingConverterService.java:53) at ly.apps.android.rest.cache.CacheAwareCallback.parseResponse(CacheAwareCallback.java:137) at com.loopj.android.http.BaseJsonHttpResponseHandler$2.run(BaseJsonHttpResponseHandler.java:136) at java.lang.Thread.run(Thread.java:841) Caused by: java.io.EOFException: No content to map to Object due to end of input at org.codehaus.jackson.map.ObjectMapper._initForReading(ObjectMapper.java:2775) at org.codehaus.jackson.map.ObjectMapper._readMapAndClose(ObjectMapper.java:2718) at org.codehaus.jackson.map.ObjectMapper.readValue(ObjectMapper.java:1923) at ly.apps.android.rest.converters.impl.JacksonBodyConverter.fromResponseBody(JacksonBodyConverter.java:84)             at ly.apps.android.rest.converters.impl.DelegatingConverterService.fromResponseBody(DelegatingConverterService.java:53)             at ly.apps.android.rest.cache.CacheAwareCallback.parseResponse(CacheAwareCallback.java:137)             at com.loopj.android.http.BaseJsonHttpResponseHandler$2.run(BaseJsonHttpResponseHandler.java:136)             at java.lang.Thread.run(Thread.java:841) 07-03 16:33:51.209 1516-1516/com.educarm.mirador D/android-rest﹕ main -> onFailure: status415 rawResponse: 07-03 16:33:51.209 1516-1516/com.educarm.mirador D/MIRADOR_PROF﹕ -----> HEADER: Authorization: Bearer TGT-51-E4VXYlMsItRd5gvhQdCCa6BvjTmw7bKNepinWu7WqmfZybMICV-caspruebas.murciaeduca.es 07-03 16:33:51.209 1516-1516/com.educarm.mirador D/MIRADOR_PROF﹕ -----> HEADER: Accept: application/json 07-03 16:33:51.209 1516-1516/com.educarm.mirador D/MIRADOR_PROF﹕ -----> HEADER: Content-Type: application/json 07-03 16:33:51.209 1516-1516/com.educarm.mirador D/MIRADOR_PROF﹕ -----> HEADER: Content-Type: application/json 07-03 16:33:51.209 1516-1516/com.educarm.mirador D/MIRADOR_PROF﹕ -----> URI http://caspruebas.murciaeduca.es/pxxi-profesores-rs/mensajeria/docente/sms/30008650/201314 07-03 16:33:51.209 1516-1516/com.educarm.mirador D/MIRADOR_PROF﹕ [MessagesComposerManagerFragment - sendMessage] StatusCode: 415 07-03 16:33:51.209 1516-1516/com.educarm.mirador D/MIRADOR_PROF﹕ [MessagesComposerManagerFragment - sendMessage] getResult: null 07-03 16:33:51.209 1516-1516/com.educarm.mirador D/MIRADOR_PROF﹕ [MessagesComposerManagerFragment - sendMessage] getRawData:

raulraja commented 10 years ago

mmm, is the generated JSON correct for posting to your service?

{"ASUNTO":"test","DESTINATARIOS":["3427217"],"TEXTO":"test"}

Also can you paste here the actual call to the service where you pass the callback?

joshid commented 10 years ago

captura de pantalla 2014-07-03 a la s 18 45 35

joshid commented 10 years ago

Current Body Class

public class WSMensaje implements Serializable {

// Fields

@JsonProperty("ASUNTO")
private String ASUNTO;                   // Asunto del mensaje a envíar
@JsonProperty("TEXTO")
private String TEXTO;                    // Texto del mensaje a envíar
@JsonProperty("DESTINATARIOS")
private List<String> DESTINATARIOS;      // Listado con los IdAlumno a los que envíar el mensaje

// Constructor

public WSMensaje(String ASUNTO,
                 String TEXTO,
                 List<String> DESTINATARIOS) {

    this.ASUNTO = ASUNTO;
    this.TEXTO = TEXTO;
    this.DESTINATARIOS = DESTINATARIOS;

}

public WSMensaje() {}

// Getters and Setters

public String getASUNTO() {
    return ASUNTO;
}

public void setASUNTO(String ASUNTO) {
    this.ASUNTO = ASUNTO;
}

public String getTEXTO() {
    return TEXTO;
}

public void setTEXTO(String TEXTO) {
    this.TEXTO = TEXTO;
}

public List<String> getDESTINATARIOS() {
    return DESTINATARIOS;
}

public void setDESTINATARIOS(List<String> DESTINATARIOS) {
    this.DESTINATARIOS = DESTINATARIOS;
}

}

WEBSERVICE CALL

List managerList = new ArrayList(); managerList.add(msgManagerId);

WSMensaje msg = new WSMensaje(msgHeader, msgBody, managerList);

restApi.postMensajeriaDocente(bearerToken, msgChannel, centerCode, periodCode, msg, new Callback<List>() {

                    @Override
                    public void onResponse(Response<List<WSMensajeResultado>> response) {
                    ...
                    }

});

raulraja commented 10 years ago

Can you also post the Interface definition where you have the @POST ?

joshid commented 10 years ago
@POST("/mensajeria/docente/{canal}/{codCentro}/{codPeriodo}")
void postMensajeriaDocente(@Header("Authorization") String token,
                           @Path("canal") String canal,
                           @Path("codCentro") String codCentro,
                           @Path("codPeriodo") String codPeriodo,
                           @Body WSMensaje mensaje,
                           Callback<List<WSMensajeResultado>> callback);
joshid commented 10 years ago

have you seen something wrong in code?

raulraja commented 10 years ago

So far just the property names. Seems to be a server related issue. Have you talked to your server guys to see what can be wrong with the response and if they are receiving the payload at all? On Jul 3, 2014 8:01 PM, "Jose Hidalgo" notifications@github.com wrote:

have you seen something wrong in code?

— Reply to this email directly or view it on GitHub https://github.com/47deg/appsly-android-rest/issues/43#issuecomment-47963844 .

joshid commented 10 years ago

I think that if it were backend problem it must also fail with Fetcher... backenders told me that Payload is received fine.

I noticed that Content-Type is sended twice in Header, could that be the cause of the problem?:

Headers: {accept-encoding=[gzip], Authorization=[Bearer TGT-43-dFniCZfFNDGjj1TgjZSfqTJESa0Hev6eAArKIOPR52CBb3xMEE-caspruebas.murciaeduca.es], connection=[Keep-Alive], Content-Length=[138], content-type=[application/json, application/json], max-forwards=[10], user-agent=[android-async-http/1.4.4

joshid commented 10 years ago

any ideas of what's wrong in my code? Could you use a POST example in the Test Source?

raulraja commented 10 years ago

Why is the content type sent twice? Also the header name for the content type is sent in lowercase. Are you manually enforcing the content type anywhere in your app?

joshid commented 10 years ago

I'm not enforcing the content-type anywhere. Dont know why is sended twice...

raulraja commented 10 years ago

Try adding this method to your callback to debug any additional headers that may be getting passed to the request.

    @Override
    protected Result parseResponse(String responseBody) throws Throwable {
        super.parseResponse(responseBody);
        Logger.d("additional headers: " + this.getAdditionalHeaders());    
    }
joshid commented 10 years ago

-----> HEADER: Authorization: Bearer TGT-64-DahbaE4VtNVmbaW6cHtZ61lRoZSf6GOtilb7QcJhAaDbrN4Wiw-caspruebas.murciaeduca.es -----> HEADER: Accept: application/json -----> HEADER: Content-Type: application/json -----> HEADER: Content-Type: application/json -----> ADITIONAL HEADER: Authorization: Bearer TGT-64-DahbaE4VtNVmbaW6cHtZ61lRoZSf6GOtilb7QcJhAaDbrN4Wiw-caspruebas.murciaeduca.es -----> ADITIONAL HEADER: Accept: application/json

joshid commented 10 years ago

I've seen in your source code this:

/* * Performs an asynchronous POST request delegating results to a @see APIDelegate replacing url placeholder based on args using @see java.lang.String#format. * @param url the url * @param body the body to be posted as an implementer of JSONSerializable * @param delegate the APIDelegate that will handle results, error and request and response interception if necessary */

void post(String url, Object body, Callback delegate); Does my body class have to implement JSONSerializable?
47degdev commented 10 years ago

Nope, but the output indicates the Content-Type header is getting set twice. At this point I don't think I can further help you unless I see more of the source code. Feel free to give me access to it if you want me to further help you debug this issue.

On Sun, Jul 6, 2014 at 11:06 PM, Jose Hidalgo notifications@github.com wrote:

I've seen in your source code this:

/**

Does my class have to implement JSONSerializable?

Reply to this email directly or view it on GitHub https://github.com/47deg/appsly-android-rest/issues/43#issuecomment-48127121 .

Raúl Raja Martínez Co-founder @ 47 Degrees h: http://raulraja.com w: http://47deg.com t: http://twitter.com/raulraja

joshid commented 10 years ago

Sure. Give me your bitbucket user and I'll share my repo with you.

47degdev commented 10 years ago

raulraja

On Sun, Jul 6, 2014 at 11:46 PM, Jose Hidalgo notifications@github.com wrote:

Sure. Give me your bitbucket user and I'll share my repo with you.

Reply to this email directly or view it on GitHub https://github.com/47deg/appsly-android-rest/issues/43#issuecomment-48128085 .

Raúl Raja Martínez Co-founder @ 47 Degrees h: http://raulraja.com w: http://47deg.com t: http://twitter.com/raulraja

47degdev commented 10 years ago

This is most likely an issue on your server side parser. but in any case. Remove all of these from the API calls... they still have it in the latest code you gave me access to.

@Header("Authorization") String token, @Header("Content-Type") String contentType, @Header("Accept") String accept,

Set the "Authorization" at the Restclient or http client level programmatically so it's applied to all calls.

Let me know if that works. You should not have to add manually the content type headers unless you are not using JSON.

On Mon, Jul 7, 2014 at 12:19 AM, Raul Raja Martinez raul@47deg.com wrote:

raulraja

On Sun, Jul 6, 2014 at 11:46 PM, Jose Hidalgo notifications@github.com wrote:

Sure. Give me your bitbucket user and I'll share my repo with you.

Reply to this email directly or view it on GitHub https://github.com/47deg/appsly-android-rest/issues/43#issuecomment-48128085 .

Raúl Raja Martínez Co-founder @ 47 Degrees h: http://raulraja.com w: http://47deg.com t: http://twitter.com/raulraja

joshid commented 10 years ago

But if it's a server-side issue why it runs ok from Fetcher as you can see in the capture from a previous comment?

47degdev commented 10 years ago

What happens in Fetcher if you manually add the Content-Type twice?

On Mon, Jul 7, 2014 at 12:51 AM, Jose Hidalgo notifications@github.com wrote:

But if it's a server-side issue why it runs ok from Fetcher as you can see in the capture from a previous comment?

— Reply to this email directly or view it on GitHub https://github.com/47deg/appsly-android-rest/issues/43#issuecomment-48129658 .

joshid commented 10 years ago

captura de pantalla 2014-07-07 a la s 01 03 54

joshid commented 10 years ago

It seems to send only once "Content-Type" per Request. I don't know the reason why I'm sending "Content-Type" twice...

47degdev commented 10 years ago

Fetcher may be not allowing duplicate headers. Have you tried removing the annotations like I had mentioned earlier? On Jul 7, 2014 1:05 AM, "Jose Hidalgo" notifications@github.com wrote:

It seems to send only once "Content-Type" per Request. I don't know the reason why I'm sending "Content-Type" twice...

— Reply to this email directly or view it on GitHub https://github.com/47deg/appsly-android-rest/issues/43#issuecomment-48129956 .

joshid commented 10 years ago

I've removed Accept & Content-Type headers. How can I set the "Authorization" at the Restclient or http client level?

raulraja commented 10 years ago

At the Http Client initialization level. Ex.

RestClient client = new DefaultRestClientImpl(
                new CacheAwareHttpClient(new ContextPersistentCacheManager(this)) {{
                    enableHttpResponseCache(10 * 1024 * 1024, new File(MainActivity.this.getCacheDir(), "android-rest-http"));
                    addHeader("Authorization", "value");
                }},
                new JacksonQueryParamsConverter(),
                new DelegatingConverterService(){{
                    addConverter(new JacksonBodyConverter());
                    addConverter(new JacksonHttpFormValuesConverter());
                }}
        );

You can also use

setBasicAuth("username", "password");

instead of addHeader if your API is basic auth based

joshid commented 10 years ago

I've modified RestClient but now now Auth Header is sent to backend. And Content-Type keep on being sended twice.

Here's my code:

RestClient creation

bearerToken = "Bearer " + session_data.getString(Constants.SESSION_OAUTH_TOKEN, "");

client = new DefaultRestClientImpl( new CacheAwareHttpClient(new ContextPersistentCacheManager(getActivity().getApplicationContext())) {{ enableHttpResponseCache(10 * 1024 * 1024, new File(getActivity().getApplicationContext().getCacheDir(), "android-rest-http")); addHeader("Authorization", bearerToken); }}, new JacksonQueryParamsConverter(), new DelegatingConverterService(){{ addConverter(new JacksonBodyConverter()); addConverter(new JacksonHttpFormValuesConverter()); }} );

Log

D/android-rest﹕ main -> CacheAwareHttpClient.sendRequest.onPostExecute proceeding uncached W/DefaultRequestDirector﹕ Authentication error: Unable to respond to any of these challenges: {bearer=WWW-Authenticate: Bearer} D/android-rest﹕ main -> CacheAwareCallback. Loading from cache after response error: No Autorizado result with cached result: null HEADER: Content-Type: application/json HEADER: Content-Type: application/json StatusCode: 401

joshid commented 10 years ago

Any hints about the code, please? I'm still blocked in the project

47degdev commented 10 years ago

Looks like this is an unrelated issue. Your authentication failed.

On Tue, Jul 8, 2014 at 2:02 PM, Jose Hidalgo notifications@github.com wrote:

Any hints about the code, please? I'm still blocked in the project

Reply to this email directly or view it on GitHub https://github.com/47deg/appsly-android-rest/issues/43#issuecomment-48327103 .

Raúl Raja Martínez Co-founder @ 47 Degrees h: http://raulraja.com w: http://47deg.com t: http://twitter.com/raulraja

raulraja commented 10 years ago

Also I don't see any of these changes on the latest bitbucket sources

joshid commented 10 years ago

I've just commited changes to bitbucket. The test sources is in MessagesComposerManagerFragment.java in lines 107-116.

raulraja commented 10 years ago

You can also set the authentication at the callback level. e.g.

package com.educarm.mirador.api;

import android.content.Context;
import ly.apps.android.rest.client.Callback;
import org.apache.http.Header;
import org.apache.http.message.BasicHeader;

public abstract class AuthenticatedCallback<T> extends Callback<T> {

    private String token;

    private void applyToken() {
        Header authHeader = new BasicHeader("Authorization", String.format("Bearer %s", token));
        setAdditionalHeaders(new Header[]{authHeader});
    }

    protected AuthenticatedCallback(Context context, Class<T> targetClass, String token) {
        super(context, targetClass);
        this.token = token;
        applyToken();
    }

}

Then use ... new AuthenticatedCallback(context, targetClass, token) {...} instead of the default callback.

That way you can just pass the token on each call. Looks like you don't have a token at the time that you are constructing the rest client so this may be a better option.

raulraja commented 10 years ago

you are still using

 public WSMensaje(@JsonProperty("ASUNTO") String asunto,
                     @JsonProperty("TEXTO")String texto,
                     @JsonProperty("DESTINATARIOS") List<String> destinatarios) {

        this.asunto = asunto;
        this.texto = texto;
        this.destinatarios = destinatarios;

    }

I don't know if that's related but try with a default public constructor and not that. That has no effect in the property names for outbound objects AFAIK.

joshid commented 10 years ago

I set the "Authorization" at the Restclient level in MainMenuoptionsFragment.java that uses GET method and works perfect. I think is something related to POST implementation.

I've updated bitbucket sources.

joshid commented 10 years ago

I've changed my code to using AsyncHttpClient directly and it works so I'm pretty sure that you're setting a duplicate Content-Type somewhere in the lib.

Check MessagesComposerManagerFragment (lines 170-198) to see the changes.

47degdev commented 10 years ago

Thanks Jose, I'll check it out. Still weird that the server would bail in such a case. I'll keep you posted

On Wed, Jul 9, 2014 at 1:39 AM, Jose Hidalgo notifications@github.com wrote:

I've changed my code to using AsyncHttpClient directly and it works so I'm pretty sure that you're setting a duplicate Content-Type somewhere in the lib.

Check MessagesComposerManagerFragment to see the changes.

Reply to this email directly or view it on GitHub https://github.com/47deg/appsly-android-rest/issues/43#issuecomment-48413348 .

Raúl Raja Martínez Co-founder @ 47 Degrees h: http://raulraja.com w: http://47deg.com t: http://twitter.com/raulraja

raulraja commented 10 years ago

@joshid i deployed a new snapshot with some fixes so the headers don't get added more than once. Can you try and let me know if it works? Thanks.

joshid commented 10 years ago

Now it works like a charm! Thank you VERY much for the support, @raulraja

joshid commented 10 years ago

I'm going to keep the issue open until I test other methods (PUT, DELETE), ok? I let you know when I've finished my tests.

raulraja commented 10 years ago

You're welcome!