retrofit icon indicating copy to clipboard operation
retrofit copied to clipboard

How can I avoid request parameter names getting encoded when making a form-urlencoded POST request?

Open RacZo opened this issue 9 years ago • 35 comments

Hello all!

First of all, RetroFit and RetroFit 2.0 are awesome, I've used them in several Android Apps in the past couple of years with great success, well, those Apps had decent backend APIs... but now... I'm working with this horribly designed API (made with ruby on rails) and I can't change the API. This nightmarish API has (among other awful things) POST methods with parameters such as:

  • user[email]
  • location[address]
  • location[city]

And I've created this method to consume one of the methods:

@FormUrlEncoded
@POST("userExists.json")
Call<ExistsResponse> emailExists(@Field("user[email]") String email);

My service is created with a GsonConverterFactory with the following GSON object:

            Gson gson = new GsonBuilder()
                    .excludeFieldsWithModifiers(Modifier.TRANSIENT)
                    .setDateFormat(WebService.API_DATE_FORMAT)
                    .disableHtmlEscaping()
                    .create();

I read that using disableHtmlEscaping() on the gson object would help but it didn't.

The problem I'm having is that the square brackets in the parameter name is getting encoded like this:

... D/OkHttp: user%5Bemail%5D=email%40example.com

(Yes!, I'm using the very neat HttpLoggingInterceptor to log the body of the requests!)

This is driving me crazy, I've tried all the possible ways to make this request and the backend API keeps sending me 404 because it is not understanding the request parameter names.

Is there a way to tell retrofit not to encode the parameter names in the body of a post request?

Your help will be greatly appreciated!

Happy holidays!

RacZo avatar Dec 24 '15 02:12 RacZo

AFAIK: Passed string value is not going through converter (Gson) since it's a Form parameter. Try @Field(encoded = true)

On Thu, Dec 24, 2015, 05:03 Oscar S. [email protected] wrote:

Hello all!

First of all, RetroFit and RetroFit 2.0 are awesome, I've used them in several Android Apps in the past couple of years with great success, well, those Apps had decent backend APIs... but now... I'm working with this horribly designed API (made with ruby on rails) and I can't change the API. This nightmarish API has (among other awful things) POST methods with parameters such as:

  • user[email]
  • location[address]
  • location[city]

And I've created this method to consume one of the methods:

@FormUrlEncoded @POST("userExists.json")Call<ExistsResponse> emailExists(@Field("user[email]") String email);

My service is created with a GsonConverterFactory with the following GSON object:

        Gson gson = new GsonBuilder()
                .excludeFieldsWithModifiers(Modifier.TRANSIENT)
                .setDateFormat(WebService.API_DATE_FORMAT)
                .disableHtmlEscaping()
                .create();

I read that using disableHtmlEscaping() on the gson object would help but it didn't.

The problem I'm having is that the square brackets in the parameter name is getting encoded like this:

... D/OkHttp: user%5Bemail%5D=email%40example.com

(Yes!, I'm using the very neat HttpLoggingInterceptor https://github.com/square/okhttp/tree/master/okhttp-logging-interceptor to log the body of the requests!)

This is driving me crazy, I've tried all the possible ways to make this request and the backend API keeps sending me 404 because it is not understanding the request parameter names.

Is there a way to tell retrofit not to encode the parameter names in the body of a post request?

Your help will be greatly appreciated!

Happy holidays!

— Reply to this email directly or view it on GitHub https://github.com/square/retrofit/issues/1407.

@artem_zin

artem-zinnatullin avatar Dec 24 '15 02:12 artem-zinnatullin

Digging around, I found this commit 4c38147 made by @JakeWharton that adds methods to tell if the field name or value should be encoded or not in the body.

But, I can't find a way to use them... will keep trying.

UPDATE: Ok... apparently this worked at some point in retrofit one, not retrofit2. :(

RacZo avatar Dec 24 '15 02:12 RacZo

There is a test case that proves encoded=true works: https://github.com/square/retrofit/blob/90729eb2ae2f3329281c1f9813ab1de3daa71ad0/retrofit/src/test/java/retrofit2/RequestBuilderTest.java#L1488-L1498. What output do you get when you use it?

JakeWharton avatar Dec 24 '15 05:12 JakeWharton

Hey thank you @JakeWharton, you rock man!,

Evidently I missed that test case. I tried with:

@Field(value = "user[email]", encoded = false)

and the request didn't went through, then I tried with:

@Field(value = "user[email]", encoded = true)

and that was all I had to do in order to get a response from this API.

On the other hand, there seems to be an issue with the HttpLoggingInterceptor because the output in my log is still:

... D/OkHttp: user%5Bemail%5D=email%40example.com

RacZo avatar Dec 24 '15 07:12 RacZo

i have meet this question,too.forexample:Date type,brackets

hatcher521 avatar Jan 08 '16 08:01 hatcher521

Stop spamming our issues. This is your one and only warning.

(Their comment was since deleted)

On Fri, Jun 3, 2016 at 12:41 AM AMIT SHEKHAR [email protected] wrote:

You can use this library . This library supports this Android Networking https://github.com/amitshekhariitbhu/AndroidNetworking

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/square/retrofit/issues/1407#issuecomment-223489318, or mute the thread https://github.com/notifications/unsubscribe/AAEEEX_rEgq_ZiW4cDdz0WLykbVoc4pgks5qH7CDgaJpZM4G64vQ .

JakeWharton avatar Jun 03 '16 05:06 JakeWharton

I am facing the same issue after upgrading to Retrofit 2.0

Tried Both : @Field(value = "email", encoded = false) String email @Field(value = "email", encoded = true) String email

Getting same result "sriramji.k**%40**gmail.com", but i want "[email protected]"

sriramji avatar Nov 10 '16 13:11 sriramji

@JakeWharton Please help me out of this issue

sriramji avatar Nov 22 '16 06:11 sriramji

This issue can be fix if you add one more method in Formbody.Builder like

public static final class Builder {
......
public Builder addPlainText(String name, String value) {
      names.add(name);
      values.add(value);
      return this;
    }
.....
}

sriramji avatar Nov 29 '16 11:11 sriramji

I ran into the same issue since I upgraded from retrofit 1.9 to retofit 2.

@Field(value = "videos[]") List<Integer> videoIds Result: videos%5B%5D=4340934

@Field(value = "videos[]", encoded = true) List<Integer> videoIds Result: videos%5B%5D=4340934

I am not sure how the encoded parameter works but I guess this is only for the values of the field?

@sriramji did you find any solution for this?

mzander avatar Dec 19 '16 07:12 mzander

@mzander For now i created my own FormBody.Builder like i said before

 @POST(LOGIN_URL)
 Call<BaseResponse> loginUser(@Body RequestBody body);
 RequestBody formBody = new FormBody.Builder()
                .add(EMAIL, emailId.getText().toString())
                .add(PASSWORD, password.getText().toString())
                .build();

 Call<BaseResponse> loginCall = RetorfitService.service.loginUser(formBody);

sriramji avatar Dec 19 '16 08:12 sriramji

I've read the source code of Retrofit and Okhttp, here is the key:

// in package okhttp3
public final class HttpUrl {
  static final String QUERY_ENCODE_SET = " \"'<>#";
  static final String QUERY_COMPONENT_ENCODE_SET = " \"'<>#&=";
  static final String QUERY_COMPONENT_ENCODE_SET_URI = "\\^`{|}";
  static final String FORM_ENCODE_SET = " \"':;<=>@[]^`{}|/\\?#&!$(),~";

  static void canonicalize(Buffer out, String input, int pos, int limit, String encodeSet,
      boolean alreadyEncoded, boolean strict, boolean plusIsSpace, boolean asciiOnly) {
    Buffer utf8Buffer = null; // Lazily allocated.
    int codePoint;
    for (int i = pos; i < limit; i += Character.charCount(codePoint)) {
      codePoint = input.codePointAt(i);
      if (alreadyEncoded
          && (codePoint == '\t' || codePoint == '\n' || codePoint == '\f' || codePoint == '\r')) {
        // Skip this character.
      } else if (codePoint == '+' && plusIsSpace) {
        // Encode '+' as '%2B' since we permit ' ' to be encoded as either '+' or '%20'.
        out.writeUtf8(alreadyEncoded ? "+" : "%2B");
      } else if (codePoint < 0x20
          || codePoint == 0x7f
          || codePoint >= 0x80 && asciiOnly
          || encodeSet.indexOf(codePoint) != -1
          || codePoint == '%' && (!alreadyEncoded || strict && !percentEncoded(input, i, limit))) {
        // Percent encode this character.
        if (utf8Buffer == null) {
          utf8Buffer = new Buffer();
        }
        utf8Buffer.writeUtf8CodePoint(codePoint);
        while (!utf8Buffer.exhausted()) {
          int b = utf8Buffer.readByte() & 0xff;
          out.writeByte('%');
          out.writeByte(HEX_DIGITS[(b >> 4) & 0xf]);
          out.writeByte(HEX_DIGITS[b & 0xf]);
        }
      } else {
        // This character doesn't need encoding. Just copy it over.
        out.writeUtf8CodePoint(codePoint);
      }
    }
}

Draw a conclusion is that, @Query won't encode char '[' and ']' into percent-encoding, but @Field will. So if you use @Query("user[email]"), it will be ok, but @Field("user[email]") will fail

ylfzq avatar May 04 '17 03:05 ylfzq

Any clue how to solve this problem? I'm facing the same issue. I have an API call like this:

@FormUrlEncoded
@POST(Constantes.URL_AUTHENTICATE)
Call<Object> authenticateUser(@Field("name") String name , @Field("password") String pwd);

And when the password parameter is an encoded string, for example, MTIzNA== and when I make the request this String becomes MTIzNA%3D%3D.

I've tried what @sriramji says and I've used a FormBody builder like this

 RequestBody formBody = new FormBody.Builder()
  .add("name", etUserName.getText().toString())
  .add("password", new String(encodeValue))
.build();

But didn't work either. I've searched a lot but I didn't find anything. Any help will be appreciated.

adriamt avatar Jun 12 '17 16:06 adriamt

Anybody solve this problem? please help...))

Nan0fm avatar Aug 28 '17 15:08 Nan0fm

I've done this to solve the problem. I have an Interceptor on my httpclient and when the method equals to post or put I decode de body. This is my code.

       HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
        logging.setLevel(HttpLoggingInterceptor.Level.BODY);
        OkHttpClient.Builder httpClient = new OkHttpClient.Builder();
        httpClient.addInterceptor(new Interceptor() {
            @Override
            public Response intercept(Interceptor.Chain chain) throws IOException {
                Request original = chain.request();

                //El problema es que codifica los parametros del body y no los queremos codificados
                String postBody = bodyToString(original.body());
                String newPostBody = URLDecoder.decode(postBody);
                RequestBody body = original.body();
                RequestBody requestBody = null;

                if(body!=null){
                    requestBody = RequestBody.create(original.body().contentType(),newPostBody);
                }

                // Aqui el problema es que se tiene que modificar el body en los put y los post para decodificar,
                // pero los get y delete no tienen body
                Request request;
                if (original.method().equals("post")) {
                    request = original.newBuilder()
                            .method(original.method(), original.body())
                            .post(requestBody)
                            .build();
                }else if(original.method().equals("put")){
                    request = original.newBuilder()
                            .method(original.method(), original.body())
                            .put(requestBody)
                            .build();
                }else{
                    request = original.newBuilder()
                            .method(original.method(), original.body())
                            .build();
                }


                return chain.proceed(request);
            }

            public String bodyToString(final RequestBody request){
                try {
                    final RequestBody copy = request;
                    final Buffer buffer = new Buffer();
                    if(copy != null)
                        copy.writeTo(buffer);
                    else
                        return "";
                    return buffer.readUtf8();
                }
                catch (final IOException e) {
                    return "did not work";
                }
            }
        });

        httpClient.addInterceptor(logging);

adriamt avatar Aug 30 '17 09:08 adriamt

@adriamt what does your API Interface look like? Tried your solution but no success.

Tindi avatar Aug 30 '17 13:08 Tindi

I've got two classes, my interface :

public interface MyApi{
    @FormUrlEncoded
    @POST(Constantes.URL_USER)
    Call<Object> createUser(
            @Field(value="name") String name,
            @Field(value="password") String pwd);
}

I have other calls, but all look like this and then my RestClient:

public class MyRestClient {
    public static MyApi REST_CLIENT;
    private static String ROOT = BuildConfig.API_HOST;

    static {
        setupRestClient();
    }

    public MyRestClient() {}

    private static void setupRestClient() {

        HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
        logging.setLevel(HttpLoggingInterceptor.Level.BODY);
        OkHttpClient.Builder httpClient = new OkHttpClient.Builder();
        httpClient.addInterceptor(new Interceptor() {
            @Override
            public Response intercept(Interceptor.Chain chain) throws IOException {
                Request original = chain.request();

                String postBody = bodyToString(original.body());
                String newPostBody = URLDecoder.decode(postBody);
                RequestBody body = original.body();
                RequestBody requestBody = null;

                if(body!=null){
                    requestBody = RequestBody.create(original.body().contentType(),newPostBody);
                }

                Request request;
                if (original.method().equals("post")) {
                    request = original.newBuilder()
                            .method(original.method(), original.body())
                            .post(requestBody)
                            .build();
                }else if(original.method().equals("put")){
                    request = original.newBuilder()
                            .method(original.method(), original.body())
                            .put(requestBody)
                            .build();
                }else{
                    request = original.newBuilder()
                            .method(original.method(), original.body())
                            .build();
                }


                return chain.proceed(request);
            }

            public String bodyToString(final RequestBody request){
                try {
                    final RequestBody copy = request;
                    final Buffer buffer = new Buffer();
                    if(copy != null)
                        copy.writeTo(buffer);
                    else
                        return "";
                    return buffer.readUtf8();
                }
                catch (final IOException e) {
                    return "did not work";
                }
            }
        });

        httpClient.addInterceptor(logging);

        OkHttpClient client = httpClient
                .readTimeout(60, TimeUnit.SECONDS)
                .writeTimeout(60, TimeUnit.SECONDS)
                .build();

        Gson gson = new GsonBuilder().registerTypeAdapter(Date.class, new DateDeserializer()).setLenient().create();


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

        REST_CLIENT = retrofit.create(MyApi.class);
    }

    public static MyApi get() {
        return REST_CLIENT;
    }

    public staticMyApi post() {
        return REST_CLIENT;
    }

    public static MyApi put() {
        return REST_CLIENT;
    }

    public static MyApi delete() {
        return REST_CLIENT;
    }
}

I don't know if its the best solution but it works for me for the moment.

adriamt avatar Aug 30 '17 13:08 adriamt

A very simple solution is to pass the value encoded and then add the encoded = true flag to retrofit.

String item = "MTUwNTIyODgxMDg4Mw==";

encodedItem = URLEncoder.encode(item, "utf-8");

@Query(value = "item", encoded = true) String item

This way when the time comes it'll decode it.

NeLk42 avatar Sep 12 '17 16:09 NeLk42

@NeLk42 but how I can use your solution in POST parameters? I need send params like this ( key : value) login[name] : some-name date : 2017-08-28T12:12:12+0200 and request looks like

@FormUrlEncoded
@POST(urlLogin)
Call<Login> signIn(@Field("login[name]") String name,      
                               @Field("date")   String date);

@adriamt I'm had trying your solution, but when I changing request body, then I can't parse back them to key-value params and I don't know what I 'm sending ....

I don't know what i'm doing wrong .. :(

Nan0fm avatar Sep 13 '17 16:09 Nan0fm

@Nan0fm Can you try this?

Before passing the value, encode it.

String username = getUsername();
String encodedUsername = URLEncoder.encode(username, "utf-8");
retrofitObject.signIn(encodedUsername, date)

Let retrofit know that you've encoded that value.

@FormUrlEncoded
@POST(urlLogin)
Call<Login> signIn(@Field("login[name]", encoded = true) String name,      
                               @Field("date")   String date);

In my case, I'm using it to validate a SHA512 encoded password against a signed header, I don't see why it shouldn't work for you.

NeLk42 avatar Sep 13 '17 16:09 NeLk42

@sriramji can you give a full code? because you add a method called addPlainText then your demo use add method. is it your new added method?

bbfxier avatar Sep 15 '17 05:09 bbfxier

yeah that was my added method. It is not recommended but I have no other choice, so i created my own custom FormBody class

This is the class that we need to look



package okhttp3;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import okhttp3.internal.Util;
import okio.Buffer;
import okio.BufferedSink;

public final class FormBody extends RequestBody {
    private static final MediaType CONTENT_TYPE = MediaType.parse("application/x-www-form-urlencoded");
    private final List<String> encodedNames;
    private final List<String> encodedValues;

    private FormBody(List<String> encodedNames, List<String> encodedValues) {
        this.encodedNames = Util.immutableList(encodedNames);
        this.encodedValues = Util.immutableList(encodedValues);
    }

    public int size() {
        return this.encodedNames.size();
    }

    public String encodedName(int index) {
        return (String)this.encodedNames.get(index);
    }

    public String name(int index) {
        return HttpUrl.percentDecode(this.encodedName(index), true);
    }

    public String encodedValue(int index) {
        return (String)this.encodedValues.get(index);
    }

    public String value(int index) {
        return HttpUrl.percentDecode(this.encodedValue(index), true);
    }

    public MediaType contentType() {
        return CONTENT_TYPE;
    }

    public long contentLength() {
        return this.writeOrCountBytes((BufferedSink)null, true);
    }

    public void writeTo(BufferedSink sink) throws IOException {
        this.writeOrCountBytes(sink, false);
    }

    private long writeOrCountBytes(BufferedSink sink, boolean countBytes) {
        long byteCount = 0L;
        Buffer buffer;
        if(countBytes) {
            buffer = new Buffer();
        } else {
            buffer = sink.buffer();
        }

        int i = 0;

        for(int size = this.encodedNames.size(); i < size; ++i) {
            if(i > 0) {
                buffer.writeByte(38);
            }

            buffer.writeUtf8((String)this.encodedNames.get(i));
            buffer.writeByte(61);
            buffer.writeUtf8((String)this.encodedValues.get(i));
        }

        if(countBytes) {
            byteCount = buffer.size();
            buffer.clear();
        }

        return byteCount;
    }

    public static final class Builder {
        private final List<String> names = new ArrayList();
        private final List<String> values = new ArrayList();

        public Builder() {
        }

        public FormBody.Builder add(String name, String value) {
            this.names.add(HttpUrl.canonicalize(name, " \"':;<=>@[]^`{}|/\\?#&!$(),~", false, false, true, true));
            this.values.add(HttpUrl.canonicalize(value, " \"':;<=>@[]^`{}|/\\?#&!$(),~", false, false, true, true));
            return this;
        }

        public FormBody.Builder addEncoded(String name, String value) {
            this.names.add(HttpUrl.canonicalize(name, " \"':;<=>@[]^`{}|/\\?#&!$(),~", true, false, true, true));
            this.values.add(HttpUrl.canonicalize(value, " \"':;<=>@[]^`{}|/\\?#&!$(),~", true, false, true, true));
            return this;
        }

        public FormBody build() {
            return new FormBody(this.names, this.values);
        }
    }
}

In Builder Inner class add your custom method

public FormBody.Builder addPlainText(String name, String value) {
            this.names.add(name);
            this.values.add(value);
            return this;
}

sriramji avatar Sep 15 '17 06:09 sriramji

The problem was finally solved ! image

add @Headers("Content-Type:application/x-www-form-urlencoded; charset=utf-8")

hubangmao avatar Dec 21 '17 03:12 hubangmao

还是不行

KomoriWu avatar Feb 02 '18 11:02 KomoriWu

Neither the extra content-type header nor setting the encoded to false works. The fields still got encoded.

jial-apa avatar Feb 02 '18 11:02 jial-apa

The Field(encoded = true) directive is ONLY there for the scenario that the value you're sending is already encoded. Setting encoded = true does NOT disable encoding it simply ensures the value is not double encoded.

Unfortunately though, as has been mentioned, a lot of people want to turn the encoding off altogether. focussing on the behaviour of the encoded option in the Field is not the place to get this as this is not by design, for turning the encoding off - it is only to stop double encoding.

A feature request should rather be logged to add a flag to turn off encoding altogether via a @Field annotation.

michaelwiles avatar Feb 13 '18 15:02 michaelwiles

I am passing a complex POJO @POST("project/{pro_id}/sender") Single<AssetRenderResponse> uploadRenderingJSON( @Path("pro_id") String proId, @Body RenderRequest renderRequest);

Some fields of the POJO are already encoded using StringEscapeUtils.escapeJava(textCaption);

How can I avoid the strings getting encoded again. Since double quotes and backslash are converted to \" and \\ respectively.

Please suggest.

pmashelkar avatar May 08 '18 09:05 pmashelkar

I'm facing same situation.

yusufonderd avatar Jul 15 '18 13:07 yusufonderd

I'm having this problem and nothing of the solutions above is working

BoukhariAyoub avatar Jul 17 '18 16:07 BoukhariAyoub

[ { "ofsNo": "180007", "dispatchDate": "07/04/2018", "vehicleNo": "ka45p3654", "transporterName": "trns", "depotName": "KSBCL", "ofsId": 1 }, { "ofsNo": "180004", "dispatchDate": "07/04/2018", "vehicleNo": "KA09B6435", "transporterName": "trns", "depotName": "KSBCL", "ofsId": 10006 } ]

Hi every one i want get ofsno in spinner and base on ofsno populate value into EditText like as "dispatchDate": "07/04/2018", "vehicleNo": "KA09B6435", "transporterName": "trns", "depotName": "KSBCL", etc but using retrofit

public void networkCall(){ ofsworld = new ArrayList<OfsId>(); // Create an array to populate the spinner ofsIdArrayList = new ArrayList<String>(); final ApiInterface apiInterface = ApiClient.getClient().create(ApiInterface.class); Call<List<OfsId>> call =apiInterface.getOfsID(); call.enqueue(new Callback<List<OfsId>>() { @Override public void onResponse(Call<List<OfsId>> call, Response<List<OfsId>> response) { if (response.isSuccessful()){ Log.d("data",String.valueOf(response)); }

           try {
                List<OfsId> ofsIds = response.body();
                for (int i=0;i<ofsIds.size();i++){
                    if (i==0){
                        //String code = ofsIds.get(i).getOfsNo();
                        String leaveType = ofsIds.get(i).getOfsNo();
                        String dipatchDate = ofsIds.get(i).getDispatchDate();
                        String depotName = ofsIds.get(i).getDepotName();
                        String vechicleNo = ofsIds.get(i).getVehicleNo();
                        String trnsName = ofsIds.get(i).getTransporterName();
                        ofsIdArrayList.add(leaveType);
                        et_DispatchDate.setText(dipatchDate);
                        et_DepotName.setText(depotName);
                        et_VechicleNo.setText(vechicleNo);
                        et_TransporterName.setText(trnsName);


                    }


                    arrayAdapter = new ArrayAdapter(MainActivity.this,android.R.layout.simple_spinner_item,ofsIdArrayList);
                    arrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
                    ofs_Spinner.setAdapter(arrayAdapter);
                }

            }catch (Exception e){
                e.printStackTrace();
            }
        }

        @Override
        public void onFailure(Call<List<OfsId>> call, Throwable t) {

        }
    });
}

Sainathhiwale avatar Jul 27 '18 11:07 Sainathhiwale