Search code examples
androidretrofitretrofit2query-string

Android retrofit POST method with String Query param double quotes in newer versions


I have this retrofit method for an API:

@POST("/searchCity/byname")
Call<Cities> searchCityByName(@Query("name") String name);

It being called this way from the code:

final String cityName ="City"
restClient.getApiService().searchCityByName(cityName);

And whenever I send a request with retrofit 2.1.0 the url is the following one (correct):

searchCity/byname?name=City

However, when I send the same request using retrofit 2.2.0 and above the url changes to (incorrect):

searchCity/byname?name=%22City%22

To that point, the server fails when processing the request due to the encoded double quotes (%22 decodes to ").

I have followed the program flow and I found nothing... What could be happening?

I need to use retrofit 2.3.0 while having the first URL (the one without the quotes). Is there any way to achieve it?

EDIT - 03/Apr/2018:

Here are the used classes:

RetrofitClient.java

@EBean(scope = EBean.Scope.Singleton)
public class RetrofitClient {
    private static volatile Retrofit retrofit;
    private static Retrofit.Builder builder = new Retrofit.Builder().baseUrl("https://exampleserver.com/");
    private PrivateApi apiService;

    public static <S> S createService(Class<S> serviceClass) {
        final GsonBuilder gsonBuilder = new GsonBuilder();
        final Gson gson = gsonBuilder.create();
        retrofit = builder.client(new WebClient().getClient())
                .addConverterFactory(GsonConverterFactory.create(gson))
                .build();
        return retrofit.create(serviceClass);
    }

    @AfterInject
    public void privateRestClientAfterInject() {
        final PrivateApi apiService = createService(PrivateApi.class);
        final PrivateApiServiceInvocationHandler publicApiServiceInvocationHandler = new
                PrivateApiServiceInvocationHandler(apiService);
        this.apiService = (PrivateApi) Proxy.newProxyInstance(PrivateApi.class.getClassLoader(),
                new Class[]{PrivateApi.class}, publicApiServiceInvocationHandler);
    }

    public PrivateApi getApiService() {
        if (apiService == null) {
            privateRestClientAfterInject();
        }
        return apiService;
    }
}

WebClient.java

public class WebClient {

    public OkHttpClient getClient() {
        final HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
        logging.setLevel(HttpLoggingInterceptor
        .Level.BODY);

        final OkHttpClient.Builder builder = new OkHttpClient.Builder().readTimeout
                (30, TimeUnit.SECONDS)
                .writeTimeout(30, TimeUnit.SECONDS)
                .connectTimeout(30, TimeUnit.SECONDS)
                .addInterceptor(logging)
                .followRedirects(false)
                .followSslRedirects(false);



                builder.addInterceptor(new Interceptor() {
                    @Override
                    public Response intercept(Chain chain) throws IOException {
                        final Request originalRequest = chain.request();
                        final HttpUrl newurl = originalRequest.url().newBuilder().build();
                        final Request newRequest = originalRequest.newBuilder()
                                .url(newurl)
                                .headers(originalRequest.headers())
                                .header("Connection", "close")
                                .header("User-Agent",
                                        getUserAgentAndKeyboard())
                                .method(originalRequest.method(), originalRequest.body())
                                .build();
                        return chain.proceed(newRequest);
                    }
                });
        return builder.build();
    }

    /**
     * Returns the standard user-agent with the default input method brand name
     *
     * @return The concatenated "user-agent; keyboard" string
     */
    private static String getUserAgentAndKeyboard() {
        // WEWE-857
        ContentResolver contentResolver = BaseApplication.getInstance().getContentResolver();
        if (contentResolver != null) {
            String keyboard = Settings.Secure.getString(contentResolver, Settings.Secure
                    .DEFAULT_INPUT_METHOD);
            return String.format(Locale.getDefault(), "%s;%s", System.getProperty("http.agent"),
                    keyboard);
        }
        return System.getProperty("http.agent");
    }

}

RetrofitInvocationHandler.java

public class RetofitInvocationHandler implements InvocationHandler {

    private final Object target;

    public RetofitInvocationHandler(Object target) {
        super();
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        return method.invoke(target, args);
    }
}

PrivateApi.java

public interface PrivateApi {
  @Headers({
    "Content-Type:application/json"
  })
  @POST("/searchCity/byname")
  Call<Cities> searchCityByName(@Query("name") String name);
}

Solution

  • Finally I got the solution:

    Just had to add a custom converter to re-write the query params:

    public static <S> S createService(Class<S> serviceClass) {
        final GsonBuilder gsonBuilder = new GsonBuilder();
        final Gson gson = gsonBuilder.create();
        retrofit = builder.client(new WebClient().getClient())
                .addConverterFactory(GsonConverterFactory.create(gson))
                // custom interceptor
                .addConverterFactory(new StringConverterFactory(GsonConverterFactory.create(gson)))
                // custom interceptor
                .build();
        return retrofit.create(serviceClass);
    }
    

    StringCoverterFactory.java

    public class DateStringConverterFactory extends Converter.Factory {
    
        private final Converter.Factory delegateFactory;
    
        DateStringConverterFactory(Converter.Factory delegateFactory) {
            super();
            this.delegateFactory = delegateFactory;
        }
    
        @Override
        public Converter<?, String> stringConverter(Type type, Annotation[] annotations, Retrofit
                retrofit) {
            for (final Annotation annotation : annotations) {
                if (annotation instanceof Query) {
    
                    final Converter<?, RequestBody> delegate = delegateFactory
                            .requestBodyConverter(type, annotations, new Annotation[0], retrofit);
                    return new DelegateToStringConverter<>(delegate);
                }
            }
            return null;
        }
    
    
        static class DelegateToStringConverter<T> implements Converter<T, String> {
            private final Converter<T, RequestBody> delegate;
    
            DelegateToStringConverter(Converter<T, RequestBody> delegate) {
                this.delegate = delegate;
            }
    
            @Override
            public String convert(T value) throws IOException {
                final okio.Buffer buffer = new okio.Buffer();
                delegate.convert(value).writeTo(buffer);
                if(value instanceof String){
                    return value.toString();
                }
                else {
                    return buffer.readUtf8();
                }
            }
        }
    }
    

    Thanks to all that tried to contribute!!