square / retrofit

A type-safe HTTP client for Android and the JVM
https://square.github.io/retrofit/
Apache License 2.0
42.99k stars 7.3k forks source link

2.0-beta2 adding extra quotes to multipart string form values #1210

Closed bperin closed 8 years ago

bperin commented 8 years ago

retrofit 2.0 seems to be double quoting strings in multi part post requsts.

for example this interface

public interface ApiInterface {

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

server side will print the key value pairs as

username : "brian"
password : "password"

instead of

username : brian
password : password

The second way is how it would show up in retrofit 1.9 and any other rest client.

stack link https://stackoverflow.com/questions/33205855/retrofit-2-0-beta-2-is-adding-literal-quotes-to-multipart-values?noredirect=1#comment54246402_33205855

JakeWharton commented 8 years ago

This is because it's running through the JSON converter. There's no special support for String and I'm not sure I want to add it.

What content type would you expect these parts to use?

bperin commented 8 years ago

@JakeWharton multipart/form-data I think.

JakeWharton commented 8 years ago

That's what the entire request uses, but each part also has a content type associated with it. In this case, it's going to be set to application/json alongside the extra quotes.

mhousser commented 8 years ago

Same issue. Huge roadblock to my 2-day upgrade to Retrofit 2.0-beta2.

I opened a similar issue, although I encountered this same problem during Enum serialization (with @SerializedName annotations) having extra quotes being received by back end server.

@JakeWharton - Our only expectation is that quotes would not suddenly appear to back-end after Retrofit 2.0 upgrade.

My Retrofit endpoint is defined as such:

@Multipart
@POST("images")
Call<ImageResponse> createImage(
        @Part("image") RequestBody image,
        @Part("lat") double lat,
        @Part("lng") double lng,
        @Part("type") ImageType type,
        @Part("description") String description);

Before Retrofit 2.0, the string test would arrive to the server as test. Now it's arriving with extra quotes: "test". (ImageType enum value also has troublesome extra quotes, but the String example is much more simple so I'll focus on that.)

Really don't know how to proceed.

mhousser commented 8 years ago

Specifically, my back end system is built on Laravel's Lumen microframework, and POST values are accessed via $request->input('field_name');.

These values now contain the quotes in them. Other mobile clients/etc don't send these extra quotes, so a hack-y 'strip off quotes on server' solution isn't an option.

JakeWharton commented 8 years ago

You can write your own Converter for String and other primitives and put them on the wire in whatever format you want. As I said, right now they're going through the JSON converter that you are presumably using.

mhousser commented 8 years ago

I'm serializing using plain old Gson just like I was with Retrofit 1.9. Nothing fancy.

You're saying with Retrofit 2.0 we have to create a type converter for serializing Strings...?

mhousser commented 8 years ago
    Gson gson = new GsonBuilder() .create();

    return new Retrofit.Builder()
            .baseUrl(Env.GetApiBaseUrl())
            .addConverterFactory(GsonConverterFactory.create(gson))
            .client(getHttpClient())
            .build();
mhousser commented 8 years ago

I would certainly categorize this as unexpected behaviour, an obstacle to upgrading to 2.0, and generally incompatible with the expectations put forth by of other REST libraries' (including Retrofit 1.9's) out-of-the-box String serialization.

JakeWharton commented 8 years ago

Pull requests welcome. But as with most of the issues filed on Retrofit, you are trivializing the problem and assuming everyone wants the same behavior as you.

mhousser commented 8 years ago

Is it not fair to say that Retrofit 1.9 and 2.0, when using the same API endpoint annotations and the same converter (Gson), should send the same data to the server?

JakeWharton commented 8 years ago

No. There are fundamental changes in the converter pipeline that's used. Some special cases have been removed (like String currently) while others have been added (RequestBody, ResponseBody, and Void).

mhousser commented 8 years ago

I like the word "currently" - it gives me hope that the no-magical-quotes behaviour will come back..

In the meantime I suppose I have no choice but to build a JakeWhartonHowCouldYouDoThisToMeConverterFactory class..

JakeWharton commented 8 years ago

I would actually argue that the absence of quotes from 1.x was the more magical behavior of the two. It's very clear what's happening in 2.x and you actually have the power to control it by placing another converter before the JSON one. This is the same issue as https://github.com/square/retrofit/issues/763 just with another built-in Java primitive.

bperin commented 8 years ago

@JakeWharton I'll mess around with custom converter but I would imagine as more people upgrade to 2.0 this will come up more than once. On a side note thanks for a great library, I moved over from Spring Android a while ago besides this issue it's been great.

JakeWharton commented 8 years ago

There's a ToStringConverterFactory multiple places in the tests of the library. Should be able to just copy/paste that guy for now.

On Mon, Oct 19, 2015 at 3:55 PM bperin notifications@github.com wrote:

@JakeWharton https://github.com/JakeWharton I'll mess around with custom converter but I would imagine as more people upgrade to 2.0 this will come up more than once. On a side note thanks for a great library, I moved over from Spring Android a while ago besides this issue it's been great.

— Reply to this email directly or view it on GitHub https://github.com/square/retrofit/issues/1210#issuecomment-149328185.

mhousser commented 8 years ago

Ok, here's the route I'm going at the moment:

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

And then inside GsonStringConverterFactory:

@Override
public Converter<?, RequestBody> toRequestBody(Type type, Annotation[] annotations)
{
    if (type == String.class)
        return new GsonStringRequestBodyConverter<>(gson, type);

    return null;
}

I'm just copying GsonRequestBodyConverter into my magical GsonStringRequestBodyConverter class, I guess. I only need to change the MEDIA_TYPE, I imagine.

What do you recommend I change it to?

mhousser commented 8 years ago

Oh, I see, so:

private static final MediaType MEDIA_TYPE = MediaType.parse("text/plain");

bperin commented 8 years ago

@mhousser Content-Type: text/plain; charset=UTF-8

mhousser commented 8 years ago

Hmm. Having trouble. Back end still receiving strings with quotes in them..

mhousser commented 8 years ago

Ok. Finally got Strings (Enums are still my next issue..) to work. Certainly not out-of-the-box any more.

Firstly:

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

As per @JakeWharton 's suggestion:

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;
    }
}

Confirmed that back end server is receiving proper string without quotes inside it.

rikochet commented 8 years ago

@mhousser cheers for the Converter Factory! I switched from @FormUrlEncoded to @Multipart so that I may POST files, and all of a sudden these annoying quotes were appearing around my strings!

Thanks for putting together a work-around, much appreciated!

JakeWharton commented 8 years ago

Looks like this is a dupe of #763!

ramesh586 commented 8 years ago

Still this issue not cleared from my side how you are closed it

JakeWharton commented 8 years ago

Did you look at the PR? There's a scalars converter you can use.

On Thu, Nov 26, 2015 at 8:08 AM ramesh586 notifications@github.com wrote:

Still this issue not cleared from my side how you are closed it

— Reply to this email directly or view it on GitHub https://github.com/square/retrofit/issues/1210#issuecomment-159908206.

pedrofraca commented 8 years ago

What about to do in that way?

RequestBody caption = RequestBody.create(MediaType.parse("text/plain"), new String("caption"));

regisoliveira commented 8 years ago

For those that are still facing this problem, I changed my Interface from:

    @Multipart
    @POST("api/comm/orders")
    Call<Void> postOrder(@Part("orderID") String orderID, @Part("orderNumber") Long orderNumber, @Part("order\"; filename=\"orderData.dat ") RequestBody order);

to

    @Multipart
    @POST("api/comm/orders")
    Call<Void> postOrder(@Part("orderID") RequestBody orderID, @Part("orderNumber") RequestBody orderNumber, @Part("order\"; filename=\"orderData.dat ") RequestBody order);

Then, declared the RequestBodies as below:

RequestBody requestBodyOrderID = RequestBody.create(MediaType.parse("text/plain"), pedido.getPedidoID());
RequestBody requestBodyOrderNumber = RequestBody.create(MediaType.parse("text/plain"), String.valueOf(pedido.getNumero()));
RequestBody requestBodyOrderDataFile = RequestBody.create(MediaType.parse("multipart/form-data"), file);

Call<Void> postOrderCall = ordersClient.postOrder(requestBodyOrderID, requestBodyOrderNumber, requestBodyOrderDataFile);
postOrderCall.execute();
Bajranghudda1 commented 8 years ago

The above solution working fine in simple primitive values but it still add extra ("\"") in your request, when we pass an array or a List of items as multipart... If we send an array or list like... getVerty(........, @Part("items_info") List items); We get request like...............

{"status":"1","message":{"nfc_tag_id":"U-123456-123-12/346","time":"2015-12-16 16:54:04","resident_at_home":"Y","items_info":"[{\"cna_number\":\"\",\"item_bar_code\":\"SKN6466A\",\"item_type\":\"SP\",\"time\":\"2015-12-16 16:54:10\"}]"}}

We can see there is still extra (\").....??

Note:- But backend still can remove these extra (\") from requeset.

But you can send an array or list also like this...

RequestBody rb;
    LinkedHashMap<String, RequestBody> mp= new LinkedHashMap<>();
    for(int i=0;i<itemBeam.getItems_info().size();i++)
    {
               rb=RequestBody.create(MediaType.parse("text/plain"), itemBeam.getItems_info().get(i).getItem_bar_code());
        mp.put("item_bar_code["+i+"]", rb);

        rb=RequestBody.create(MediaType.parse("text/plain"),itemBeam.getItems_info().get(i).getItem_type());
        mp.put("item_type["+i+"]",rb);

        rb=RequestBody.create(MediaType.parse("text/plain"),itemBeam.getItems_info().get(i).getCna_number());
        mp.put("cna_number["+i+"]",rb);

        rb=RequestBody.create(MediaType.parse("text/plain"),itemBeam.getItems_info().get(i).getTime());
        mp.put("times["+i+"]",rb);
    }

Now pass your map in the callback function ....

getVerty("11221", mp, "Bajrang Hudda");

And change your interface like this..... @Multipart @POST("vertical") Call getVerty( @Part("id") Sting id, @PartMap() Map<String, RequestBody> phot, @Part("name") String name);

That's it..... now your request will be like.... {"status":"1","message":{"nfc_tag_id":"U-123456-123-12/346","time":"2015-12-17 10:04:29","resident_at_home":"Y","item_bar_code":["MC140517193S","9804456119139","CN04G4817161653H01AV","SP40D71289"],"item_type":["O","O","O","SP"],"cna_number":["","","",""],"times":["2015-12-17 10:04:33","2015-12-17 10:04:38","2015-12-17 10:04:46","2015-12-17 10:04:53"]}}

pflammertsma commented 8 years ago

I've extended on @mhousser's converter, and included enums and all primary types:
https://gist.github.com/pflammertsma/b9cb0c4688bbd335ab66

I agree with @JakeWharton that these sorts of things are by design. Retrofit 2 really behaves well in using the defined converter for everything, including multipart data. It simply appears that the API I'm communicating with doesn't respect the Content-Type of each part, assuming it's always "text/plain".

This solution worked for me, but I'd emphasize that the underlying problem is really the API.

PortionLK commented 8 years ago

"Solution2" in this answer solved my issue regarding to this.

DenisShov commented 8 years ago

Use this: compile 'com.squareup.retrofit2:converter-scalars:2.0.1' Check this: http://stackoverflow.com/a/36907435/3844201

NhamPhanDinh commented 8 years ago

In version 2.1.0 still not work

Yazon2006 commented 7 years ago

I use another one solution. Worked with Retrofit 2.1.0. (Rx adapter is optional here)

My retrofit interface looks like this:

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

And in ApiManager I use it like this:

@Override 
    public Single<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 solution from @regisdaniel but I think that this one is little a bit more elegant.

tpatterson commented 6 years ago

Do like DenisShov said. Works great for me:

https://stackoverflow.com/a/36907435/716237

JuKu commented 5 years ago

Is their a official fix available?

sanjeet007 commented 4 years ago

public interface ApiConfig { @Multipart @POST("createOnlineSession") Call saveSessionData( @Part("sessionUnit") RequestBody sessionUnit, @Part("sessionType") RequestBody sessionType, @Part("sessionClass") RequestBody sessionClass, @Part("sessionStudent") ArrayList sessionStudent, @Part("sessionSubject") RequestBody sessionSubject, @Part("teacherInstruction") RequestBody teacherInstruction, @Part MultipartBody.Part file, @Part("schoolId") RequestBody schoolId, @Part("branchId") RequestBody branchId, /@Part("createdBy") CreatedByModel createdBy,/ @Part("sessionName") RequestBody sessionName, @Part("sessionDate") RequestBody sessionDate, @Part("videoName") RequestBody videoName );

this is my request but may data reaching on server for arraylist is in [ '{"_id":"5c98936d37b84518c878a79f","fullName":"Asad Siddiqui","stuId":"EPS000119S001"}','{"_id":"5c98936d37b84518c878a79f","fullName":"Asad Siddiqui","stuId":"EPS000119S001"}']

why these ' ' quotes here by this my this list json {\"_id\":\"5c98936d37b84518c878a79f\",\"fullName\":\"Asad Siddiqui\",\"stuId\":\"EPS000119S001\"} converting this \ with back slash format the format i pasted here is removed here in this comment i don't know why please help me out

sanjeet007 commented 4 years ago

@JakeWharton please look in this

osumoclement commented 2 years ago

Strange that this exists even today, and only this works

theGBguy commented 1 year ago

@osumoclement Did you tried ScalarsConverterFactory before GsonConverterFactory. It already contains the idea that the SO link above put forward.

osumoclement commented 1 year ago

Yes, and that didn't work. I ended up using a custom String Converter in between ScalarsConverterFactory and GsonConverterFactory, like this:

private static Retrofit.Builder builder = new Retrofit.Builder() .baseUrl(getAPI_BASE_URL()) .addConverterFactory(ScalarsConverterFactory.create()) .addConverterFactory(CustomRetrofitStringConverter.create()) .addConverterFactory(GsonConverterFactory.create());

theGBguy commented 1 year ago

@osumoclement Really? I am running into the same issue now and trying with ScalarsConverterFactory. The logic to convert String to RequestBody and vice-versa is already there is ScalarsConverterFactory so I think it is redundant. I checked the logs with Interceptor and seems like it is working for me.

osumoclement commented 1 year ago

I think it depends on how else you're using Retrofit/GSON in other parts of your code. In my case, I was doing a MultiPartBody request as below:

List<MultipartBody.Part> jsonbody = new ArrayList<>();

ProgressRequestBody reqFile = new ProgressRequestBody((File) value, API.this);
multipartbody.add(MultipartBody.Part.createFormData(key.toString(), ((File) value).getName(), reqFile));

jsonbody.add(MultipartBody.Part.createFormData((String) key, (String) value));
call = genericCreateService.sendCreateRequest((String) action.get("url"), multipartbody, jsonbody);

I tried many variations of solutions suggested online, including ScalarsConverterFactory but eventually just settled for the "hacky" solution of having a custom String Converter. Also, I had encountered a similar case where GSON would also double quote dates if I used Gson gson = new GsonBuilder().setDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'") so this was yet another hack:


else if (value instanceof Date) {
        //this is hacky as hell and ideally shouldn't be there
        //gson's setdateformat is double quoting dates for some reason, so I am unquoting them
        jsonbody.add(MultipartBody.Part.createFormData((String) key, 
        (gson.toJson(value)).substring(1, gson.toJson(value).length() - 1)));
                        }
Clopma commented 1 year ago

For those that are still facing this problem, I changed my Interface from:

    @Multipart
    @POST("api/comm/orders")
    Call<Void> postOrder(@Part("orderID") String orderID, @Part("orderNumber") Long orderNumber, @Part("order\"; filename=\"orderData.dat ") RequestBody order);

to

    @Multipart
    @POST("api/comm/orders")
    Call<Void> postOrder(@Part("orderID") RequestBody orderID, @Part("orderNumber") RequestBody orderNumber, @Part("order\"; filename=\"orderData.dat ") RequestBody order);

Then, declared the RequestBodies as below:

RequestBody requestBodyOrderID = RequestBody.create(MediaType.parse("text/plain"), pedido.getPedidoID());
RequestBody requestBodyOrderNumber = RequestBody.create(MediaType.parse("text/plain"), String.valueOf(pedido.getNumero()));
RequestBody requestBodyOrderDataFile = RequestBody.create(MediaType.parse("multipart/form-data"), file);

Call<Void> postOrderCall = ordersClient.postOrder(requestBodyOrderID, requestBodyOrderNumber, requestBodyOrderDataFile);
postOrderCall.execute();

@regisoliveira This was so useful for me. It even worked with the @Body annotation.

@POST("/access") Call postAccess(@Body RequestBody qrToken);



Thank you. I hope it gets fixed soon...
arESrv commented 3 weeks ago

What if I'd like to send single string using POST method:

package com.somePackage.client.web.business.feature.control;

import retrofit2.Call;
import retrofit2.http.Body;
import retrofit2.http.GET;
import retrofit2.http.Path;
import retrofit2.http.POST;

import com.somePackage.client.web.business.feature.entity.featureModel;
import com.somePackage.client.web.business.feature.entity.featureExtensiveModel;

public interface someFeatureApi {

    @GET("someFeature/{deviceId}")
    Call<featureModel> getDeviceId(@Path("deviceId") String deviceId);

    @POST("someFeature/deviceData")
    Call<featureExtensiveModel> getDeviceData(@Body String deviceId);
}

But the deviceId must be specifically String, because it's passed as string to this getDeviceData method somewhere else.

And when I set the deviceId type inside of getDeviceData method to DeviceIdType instead of String, the IntelliJ IDEA linter shows warning that it's potentially error producing, because types don't match, because somewhere else in some class, this getDeviceData method, as a I already mentioned before, is called with deviceId parameter being String.