Retrofit 2.0-beta-2 is adding literal quotes to MultiPart values
Asked Answered
J

7

33

Went to upgrade to Retrofit 2.0 and running into this weird problem.

I have a method to log a user in

public interface ApiInterface {

    @Multipart
    @POST("user/login/")
    Call<SessionToken> userLogin(@Part("username") String username, @Part("password") String password);
}

When I look at the key value POST params on the server side they print like this

username : "brian"
password : "password"

The same method using retrofit 1.9 the K:V pairs look like

username : brian
password : password

It's adding literal quotes to the POST variables

If I use any other rest client the variables print like the second way without the quotes.

Here is how I build the Retrofit instance with an interceptor

 OkHttpClient client = new OkHttpClient();
    client.interceptors().add(new Interceptor() {
        @Override
        public Response intercept(Chain chain) throws IOException {
            Request original = chain.request();

            // Customize the request
            Request request = original.newBuilder()
                    .header("Accept", "application/json")
                    .header("Authorization", myPrefs.accessToken().getOr(""))
                    .method(original.method(), original.body())
                    .build();

            Response response = chain.proceed(request);

            // Customize or return the response
            return response;
        }
    });

    Ok2Curl.set(client);

    Retrofit retrofit = new Retrofit.Builder()
            .baseUrl(apiEndpoint)
            .addConverterFactory(GsonConverterFactory.create())
            .client(client)
            .build();

I imagine i'm doing something wrong with the converter but not sure what.

Has anyone else ran into this problem yet? I know its in beta but it's pretty widly used.

Joannajoanne answered 19/10, 2015 at 2:48 Comment(7)
Exact same here. Just finished a 2-day massive Retrofit 2.0 upgrade, most things working perfectly, but I'm going crazy with these extra quotes added to strings. Using a @Multipart Retrofit API method, string is a @Part item. Server receives the string "test" as ""test"". UGH!!!Snath
@MatthewHousser I didn't find a real solution yet, instead since I have control over the backend as well I set a special header in the client app, then if this header exists I run a method that strips quotes from GET and POST params, its super hacky but atleast the requests work for now. I think i'll open an issue on the github repoJoannajoanne
please post your issue here after you create it, thanks!Snath
For reference, I actually have a very similar issue, which I believe is based on the same Retrofit 2.0 bug, though I arrived at the problem through the use of Enums: #33207900Snath
@MatthewHousser did you try downgrading to 2.0-beta1? I tried too but would have to refactor too much code to get it to work.Joannajoanne
github issue github.com/square/retrofit/issues/1210Joannajoanne
nope I didn't try downgrading. I have a hundred endpoints, and a similar number of Callback handlers throughout 50 fragments, dialogs, etc. It was a large undertaking just to upgrade to Retrofit 2.0-beta2.. I don't even want to think about downgrading =PSnath
P
40

This is because it's running through the JSON converter.

Solution1: use RequestBody instead of String

public interface ApiInterface {
    @Multipart
    @POST("user/login/")
    Call<SessionToken> userLogin(@Part("username") RequestBody username, @Part("password") RequestBody password);
}

Build RequestBody:

RequestBody usernameBody = RequestBody.create(MediaType.parse("text/plain"), usernameStr);
RequestBody passwordBody = RequestBody.create(MediaType.parse("text/plain"), passwordStr);

Launch network operation:

 retrofit.create(ApiInterface.class).userLogin(usernameBody , passwordBody).enqueue()....

Solution2: Create a custom ConverterFactory to dispose String part value.

For: Retrofit2 final release not beta. (com.squareup.retrofit2:retrofit:2.0.0)

Create your StringConverterFactory:

public class StringConverterFactory extends Converter.Factory {
private static final MediaType MEDIA_TYPE = MediaType.parse("text/plain");

public static StringConverterFactory create() {
    return new StringConverterFactory();
}

@Override
public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) {
    if (String.class.equals(type)) {
        return new Converter<ResponseBody, String>() {

            @Override
            public String convert(ResponseBody value) throws IOException {
                return value.string();
            }
        };
    }
    return null;
}

@Override
public Converter<?, RequestBody> requestBodyConverter(Type type, Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit) {
    if(String.class.equals(type)) {
        return new Converter<String, RequestBody>() {
            @Override
            public RequestBody convert(String value) throws IOException {
                return RequestBody.create(MEDIA_TYPE, value);
            }
        };
    }

    return null;
}

}

Add to your retrofit instance:

retrofit = new Retrofit.Builder()
            .baseUrl(SERVER_URL)
            .client(client)
            .addConverterFactory(StringConverterFactory.create())
            .addConverterFactory(GsonConverterFactory.create())
            .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
            .build();

Attention: StringConverterFactory should add before GsonConverterFactory!

then you can use String as part value directly.

You can find more information about this issue in https://github.com/square/retrofit/issues/1210

Putt answered 25/3, 2016 at 7:56 Comment(3)
Why this "Solution2" doesn't commit into Retrofit? I use Retrofit:2.0.2 and only this answer fixed the issue while there are few solutions that didn't worked.Soybean
@JanakaRRajapaksha As Jake Wharton said: "I'm not sure I want to add it". Proof:github.com/square/retrofit/issues/1210Griceldagrid
Thank you very match! What about putting file to this request?Emphysema
C
9

I have the same problem, and how it solved:

1) Add to build.gradle:

compile 'com.squareup.retrofit2:converter-scalars:2.1.0' // Remember to add the same version

2) Add one line here:

Retrofit retrofit = new Retrofit.Builder()
                .baseUrl(URL_BASE)
                .addConverterFactory(ScalarsConverterFactory.create()) // this line
                .addConverterFactory(GsonConverterFactory.create(gson))
                .client(getUnsafeOkHttpClient())
                .build();
Corin answered 21/9, 2017 at 18:29 Comment(0)
N
4

What about to do in that way?

RequestBody caption = RequestBody.create(MediaType.parse("text/plain"), new String("caption"));
Nardi answered 2/12, 2015 at 12:39 Comment(0)
D
3

Here is how to resolve it,

Firstly:

 return new Retrofit.Builder()
    .baseUrl(Env.GetApiBaseUrl())
    .addConverterFactory(new GsonStringConverterFactory())
    .addConverterFactory(GsonConverterFactory.create(gson))
    .client(getHttpClient())
    .build();

Create a CustomConverter like this one, this is needed by Retrofit 2, unless some fix the "feature" added in v2.

public class GsonStringConverterFactory extends Converter.Factory {
    private static final MediaType MEDIA_TYPE = MediaType.parse("text/plain");

    @Override
    public Converter<?, RequestBody> toRequestBody(Type type, Annotation[] annotations) {
        if (String.class.equals(type))// || (type instanceof Class && ((Class<?>) type).isEnum()))
        {
            return new Converter<String, RequestBody>() {
                @Override
                public RequestBody convert(String value) throws IOException {
                    return RequestBody.create(MEDIA_TYPE, value);
                }
            };
        }
        return null;
    }
}
Discant answered 3/1, 2016 at 7:59 Comment(0)
G
3

I've found another one solution except those. Worked with Retrofit 2.1.0. (Rx adapter is optional here)

My retrofit interface looks like this:

@POST("/children/add")
Observable<Child> addChild(@Body RequestBody requestBody);

And in ApiManager I use it like this:

@Override
    public Observable<Child> addChild(String firstName, String lastName, Long birthDate, @Nullable File passportPicture) {
        MultipartBody.Builder builder = new MultipartBody.Builder()
                .setType(MultipartBody.FORM)
                .addFormDataPart("first_name", firstName)
                .addFormDataPart("last_name", lastName)
                .addFormDataPart("birth_date", birthDate + "");

        //some nullable optional parameter
        if (passportPicture != null) {
            builder.addFormDataPart("certificate", passportPicture.getName(), RequestBody.create(MediaType.parse("image/*"), passportPicture));
        }
        return api.addChild(builder.build());
    }

It is similar to Solution1 from Loyea but I think that it's little a bit more elegant.

Griceldagrid answered 19/12, 2016 at 15:29 Comment(0)
F
0

If your UI is showing your responses with quotes, you can use getAsString instead of toString

Flapjack answered 9/4, 2016 at 17:54 Comment(0)
E
0

I don't know if it is too late, but we can also send requests with RequestBody.

Example:

public interface ApiInterface {
   @Multipart
   @POST("user/login/")
   Call<SessionToken> userLogin(@Part("username") String username, @Part("password") String password);
}

We can convert as below:

public interface ApiInterface {
   @Multipart
   @POST("user/login/")
   Call<SessionToken> userLogin(@Part("username") RequestBody username, @Part("password") String password);
}
Eileneeilis answered 19/7, 2017 at 19:3 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.