I'm trying to send requests to an API that uses semicolon(;) instead of ampersand(&) for query parameters, The url request looks like this:
http://api.example.com/path/?param1=p1;param2=p2
The most similar post I found on the internet suggests to use another method that puts url parts together and then use #Url annotation.
Does retrofit contain any solution for these cases ?
Thanks.
It is possible to use an Interceptor to rewrite the URL from using '&' to ';'. The tricky part is doing it in a way that does not make the URL invalid.
new Interceptor() {
Response intercept(Chain chain) throws IOException {
HttpUrl originalUrl = chain.request().url();
String query = originalUrl.encodedQuery();
if (query == null) {
return chain.proceed(chain.request());
} else {
Request.Builder builder = chain.request().newBuilder();
builder.url(originalUrl.newBuilder()
.encodedQuery(query.replace('&', ';'))
.build());
return chain.proceed(builder.build());
}
}
};
This OkHttp (the networking library that powers Retrofit) Interceptor will check if the given URL has a query and replace any '&' in the query section with a ';'. In theory this should behave as you specified.
It can be applied by creating the OkHttpClient instance yourself and installing the abovementioned Interceptor while building it.
Related
In Spring Boot 3, they have changed so that trailing slashes, by default, are no longer ignored. For example, if I have a GET resource, /users, and I navigate to /users/ then Spring Boot webflux will now respond with 404.
You can change this by implementing a WebFluxConfigurer and overriding the configurePathMatching method:
#Override
public void configurePathMatching(PathMatchConfigurer configurer) {
configurer.setUseTrailingSlashMatch();
}
However, setUseTrailingSlashMatch is deprecated, and the docs says to use PathPatternParser.setMatchOptionalTrailingSeparator(boolean) instead. However, I don't understand how/where you actually configure this.
So the question is, how do I set PathPatternParser.setMatchOptionalTrailingSeparator(boolean)?
As #joe-clay has mentioned in his comment, PathPatternParser.setMatchOptionalTrailingSeparator(boolean) is deprecated as well in favour of explicit redirects. So you have 3 options:
Declare both routes explicitly in the controller handler #GetMapping("/users", "/users/"). Downside is that you need to do this for every controller, but can be used as a stop-gap solution.
Implement org.springframework.web.server.WebFilter interface to explicitly redirect to the desired url. Something along these lines:
#Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
URI originalUri = exchange.getRequest().getURI();
if (/* check condition for trailing slash using originalUri getPath(), getQuery() etc. */) {
String originalPath = originalUri.getPath();
String newPath = originalPath.substring(0, originalPath.length() - 1); // ignore trailing slash
try {
URI newUri = new URI(originalUri.getScheme(),
originalUri.getUserInfo(),
originalUri.getHost(),
originalUri.getPort(),
newPath,
originalUri.getQuery(),
originalUri.getFragment());
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.MOVED_PERMANENTLY); // optional
response.getHeaders().setLocation(mutatedUri);
return Mono.empty();
} catch (URISyntaxException e) {
throw new IllegalStateException(e.getMessage(), e);
}
}
return chain.filter(exchange);
}
Explicitly rewrite the incoming url in the proxy (for example using rewrite rules in nginx) to match the expected url.
In options 2 and 3, you may choose to return an HTTP 301 response as well.
I'm currently migrating an application from jersey 1 to 2. In the old app, we used a ClientFilter for all jersey clients that automatically refreshed expired OAuth tokens like this:
#Override
public ClientResponse handle(ClientRequest cr) {
ClientResponse resp = getNext().handle(cr);
if (resp.getStatus() == Status.UNAUTHORIZED.getStatusCode()) {
// Try to refresh the token
boolean refreshed = refreshToken(oAuthInfo);
if (refreshed) {
resp = getNext().handle(cr);
}
}
return resp;
}
It might not haven been the most elegant way, but the benefit was that rest client users did not have to care about expired tokens themselves.
With the ContainerResponseFilter for jersey 2, this does not seem to be that simple anymore. The only option I currently see is to use the ClientRequestContext and try to re-create the original request using getClient, getHeaders etc... and then update the result in ContainerResponseContext. This however seems a bit clunky so I was wondering if there is any more convenient way to refresh an OAuth token without having to deal with this wherever a jersey client is used?
It looks like there is not more convenient way than intercepting the response with a client filter, refreshing the token if needed and trying to repeat the exact same request with the new token. In fact, this approach is also used by jersey own filter classes.
Sample code for repeating the original rest call from within a filter class can be found in jerseys HttpAuthenticationFilter:
static boolean repeatRequest(ClientRequestContext request, ClientResponseContext response, String newAuthorizationHeader) {
Client client = request.getClient();
String method = request.getMethod();
MediaType mediaType = request.getMediaType();
URI lUri = request.getUri();
WebTarget resourceTarget = client.target(lUri);
Invocation.Builder builder = resourceTarget.request(mediaType);
MultivaluedMap<String, Object> newHeaders = new MultivaluedHashMap<String, Object>();
for (Map.Entry<String, List<Object>> entry : request.getHeaders().entrySet()) {
if (HttpHeaders.AUTHORIZATION.equals(entry.getKey())) {
continue;
}
newHeaders.put(entry.getKey(), entry.getValue());
}
newHeaders.add(HttpHeaders.AUTHORIZATION, newAuthorizationHeader);
builder.headers(newHeaders);
builder.property(REQUEST_PROPERTY_FILTER_REUSED, "true");
Invocation invocation;
if (request.getEntity() == null) {
invocation = builder.build(method);
} else {
invocation = builder.build(method,
Entity.entity(request.getEntity(), request.getMediaType()));
}
Response nextResponse = invocation.invoke();
if (nextResponse.hasEntity()) {
response.setEntityStream(nextResponse.readEntity(InputStream.class));
}
MultivaluedMap<String, String> headers = response.getHeaders();
headers.clear();
headers.putAll(nextResponse.getStringHeaders());
response.setStatus(nextResponse.getStatus());
return response.getStatus() != Response.Status.UNAUTHORIZED.getStatusCode();
}
This code is used for example in DigestAuthenticator or BasicAuthenticator to repeat a request with provided credentials in case an Unauthorised response is received from server.
Let's say I have 5 API endpoints and 4 of them require the #Header "Authorization"; the other one doesn't.
Is there a drawback or anything of the sort when I use an Interceptor to insert the header for all API calls even though one of the endpoints does not require it? :)
Probably would be better use a more customizable approach that's retrofit2 provides - dynamic headers. A dynamic header is passed like a parameter to the method. The provided parameter value gets mapped by Retrofit before executing the request. Example:
#GET("/yourEndpoint")
Call<List<Obj>> getSomth(#Header("Your-Header") String yourHeader);
But if you only worrying about extra header passing, I don't see any drawback in your particular case.
#Override
public okhttp3.Response intercept(Chain chain) throws IOException {
Request.Builder builder = chain.request().newBuilder();
Request request = chain.request();
if (!request.url().toString().contains("/tapi/login/login")) {
String cookie = SharedObj.getCookie();
builder.addHeader("Cookie", cookie);
}
return chain.proceed(builder.build());
}
This is my solution.All api need cookie except login api('/tapi/login/login') ,So I judge url to decide to wether to add cookie in request.
There is no problem using Interceptor with header for all API.
Set headers in interceptors and use to all API. If the API method is with or without auth it will be worked.
Like this - Create ones, use everywhere.
OkHttpClient.Builder httpClient = new OkHttpClient.Builder();
httpClient.addInterceptor(chain -> {
Request original = chain.request();
Request.Builder requestBuilder = original.newBuilder();
//Add headers here using requestbuilder.
String authToken =//Your Authtoken
if (authToken != null)
requestBuilder.header("Authorization", authToken);
requestBuilder.method(original.method(), original.body());
return chain.proceed(requestBuilder.build());
});
In Retrofit
Retrofit retrofit= new Retrofit.Builder().baseUrl("baseUrl").client(httpClient.build()).build();
I want to send a POST with Retrofit 2. The url has some parameters:
#Headers({
"Accept: application/x-www-form-urlencoded;",
"User-Agent: my-app"
})
#FormUrlEncoded
#POST("server/directory/location.type")
`public Call<POJOStringValue> dataWithUr(#Path("arg1") String arg1, #Path("arg2"), String arg2);
The url looks like this
www.website.com/server/directory/location.type?arg1=value1&arg2=value2
I was requested to use a POST request. The values (value1 and value2) are dynamic at runtime. I started the project with Xamarin using HttpClient and now I'm rewriting it in Java native. In C# all I had to do was to concact the strings and send the resulting string in a single Post.
I tried to use #Path and the error was :
"server/directory/location.type" does not contain "{arg1}". (parameter #1)
Then, I tried to use #Query and the error was:
java.lang.IllegalArgumentException: Form-encoded method must contain at least one #Field.
Finally I tried with #Field the request never gets any response (I sette the connection timeout to 5 seconds)
Please help me, or tell me if I have to don't have any other choice but to use a GET request.
((EDIT))
Here is my code for the setup of the client:
private static void setupClient(){
final OkHttpClient client = new okhttp3.OkHttpClient.Builder()
.connectTimeout(CONNECTION_TIMEOUT, TimeUnit.SECONDS)
.readTimeout(READ_TIMEOUT, TimeUnit.SECONDS)
.writeTimeout(WRITE_TIMEOUT, TimeUnit.SECONDS)
.retryOnConnectionFailure(false)
.build();
//define retrofit
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(iXUtils.getUrl_())
.addConverterFactory(GsonConverterFactory.create())
.client(client)
.build();
this.client_ = retrofit.create(RequestInterface.class);
}
The get() method:
public static RequestInterface get(){
return this.client_;
}
Here is how I call it:
public String callFunctionDB(String arg1, String arg2){
setupClient();
Call<POJOStringValue> call = get().dataWithUrlString(arg1, arg2);
try {
POJOStringValue response = call.execute().body();
String value = response.getValue();
int test = 0;
} catch (IOException e) {
String value = "it failded";
e.printStackTrace();
}
return "test";
}
I put the test=0 to be able to put a breaking point, it never gets there. Plus I called the method "callFunctionDB" in a doInbackground to avoid the android.os.NetworkOnMainThreadException.
Retrofit requires you to have at least one form parameter if you request form encoding. You have answered your own question -- you are using query parameters instead of POST fields, so that annotation is not necessary. Remove the #FormUrlEncoded annotation, and change your parameters to #Query annotations.
I am using CRest to deserialize a JSON stream on Android. My first steps looks very promising.
To get the JSON stream from the server and not the XML one I use the following construct:
(Accept: application/json)
#EndPoint("http://localhost:8080/myserver/rest")
#Param(name = "Accept", value = "application/json", dest = Destination.HEADER)
public interface VersionService {
#ConnectionTimeout(10000)
#Path("/version")
VersionTO getVersion();
}
This works but it's a bit annoying to copy the "Param thing" for every service.
Is there a better way to configure all Services at one place only to return JSON?
Well I'm afraid there isn't any simple way in the current version, but feel free to open a request on the issue tracker.
Cheers,
Laurent
I faced a similar situation where I used a custom HTTP client. In your case it could look like as follows:
DefaultHttpClient client = new DefaultHttpClient();
client.addRequestInterceptor(new HttpRequestInterceptor() {
public void process(HttpRequest request, HttpContext context) throws HttpException, IOException {
request.addHeader("Accept", "application/json");
}
});
CRestBuilder builder = new CRestBuilder();
builder.expectsJson();
builder.setRestService(new HttpClientRestService(client));
Another option is to set default parameter for ClientPNames.DEFAULT_HEADERS of the custom HttpClient instance.
Details can be found on http://hc.apache.org/httpcomponents-client-ga/tutorial/html/httpagent.html.