How to dynamically set headers in Retrofit (Android) - android

I am using an API that uses an authorization scheme that requires a special "X-Authorization" header to be set to authenticate the request. For example, this Retrofit setup works perfectly for the user whose auth token is abc123:
#Headers("X-Authorization: abc123")
#GET("/posts")
Observable<List<Post>> get_posts();
I cache the user's X-Authorization token, so I have access to that, however, I can't just drop it in the #Headers declaration.
#Headers("X-Authorization: " + token)
#GET("/posts")
Observable<List<Post>> get_posts();
I get a compile error here: Error:(41, 34) error: element value must be a constant expression
Any ideas on how I could get around this?

Since Retrofit 2.0 you have two options
1) Using OkHttp 2.2+ use Interceptor
At the Http level, you have more control over the request, so you could do things like applying headers only to a specific request made to a specific endpoint, and so on.
public class MyOkHttpInterceptor implements Interceptor {
#Override
public Response intercept(Chain chain) throws IOException {
Request originalRequest = chain.request();
if (!"/posts".contains(originalRequest.url()) ) {
return chain.proceed(originalRequest);
}
String token = // get token logic
Request newRequest = originalRequest.newBuilder()
.header("X-Authorization", token)
.build();
return chain.proceed(newRequest);
}
[...]
OkHttpClient okHttpClient = new OkHttpClient();
okHttpClient.networkInterceptors().add(new MyOkHttpInterceptor());
OkClient okClient = new OkClient(okHttpClient);
YourApi api = new RestAdapter.Builder()
.setEndpoint(url)
.setClient(okClient)
.build()
.create(YourApi.class);
Edit:
Adding #JakeWarthon comment as another option as is also valid.
2) Put #Header on a method parameter and pass it as a value when invoking.
From the docs:
// Replaces the header with the the value of its target.
#GET("/")
void foo(#Header("Accept-Language") String lang, Callback<Response> cb);
Header parameters may be null which will omit them from the request. Passing a List or array will result in a header for each non-null item.
Note: Headers do not overwrite each other. All headers with the same name will be included in the request.
EDIT: This option should not be considered as Retrofit 2.* dropped support for interceptors.
3) User retrofit RequestInterceptor
From the docs:
Intercept every request before it is executed in order to add additional data.
You could do something like
public class MyRetrofitInterceptor implements RequestInterceptor {
#Override
public void intercept(RequestFacade req) {
String token = // get token logic
if (token != null) {
req.addHeader("X-Authorization", token);
}
}
[...]
YourApi api = new RestAdapter.Builder()
.setEndpoint(url)
.setRequestInterceptor(new MyRetrofitInterceptor())
.build()
.create(YourApi.class);
The "problem" with this approach is that the interceptor will get executed on all the endpoints, as it's set at the RestAdapter level, and not per endpoint. Also, the RequestFacade doesn't expose much information about the request, so no chance to add much logic around it.

Passing header in parameter would be helpful. Look to the following code;
#GET("/posts")
Observable<JsonElement> getDataFromService(
#HeaderMap Map<String, String> headers,
#QueryMap HashMap<String, Object> queryParams
);
hashMap1.put("Authorization", token);
return ApiService.getAPI_test().getDataFromService(hashMap1, url, hashMap)
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io());
Update:
More better would be
#GET("/posts")
Observable<JsonElement> getDataFromService(
#Header("Authorization") token: String = "Bearer " + PreferenceUtils.getToken(),
#QueryMap HashMap<String, Object> queryParams
);

Dynamic Header In Retrofit 2
I have struggled too much to add Dynamic Header In Retrofit 2.
I have gone through so many blogs and StackOver flow. Everyone has shown example with Interceptor.
And it’s not a wise thing ,just for one API call we need to do that much work.
You just have to add #HeaderMap as argument of fun. I have done in very simple way :-
In Kotlin
val headers = HashMap<String, String>()
headers["KEY_AUTHORIZATION"] = "paste AUTHORIZATION value here"
headers["KEY_TOKEN"] = "paste TOKEN value here"
val jsonObject= JsonObject()
I am passing here header and other data also
Calling of fun:-
postEvent(headers,jsonObject)
API Declaration
#POST("/v1/post_data")
fun postEvent(#HeaderMap headers: Map<String, String>, #Body jsonObject: JsonObject): Call<JsonObject>
API Declaration with RxAndroid
#POST("/v1/post_data")
fun postEvent(#HeaderMap headers: Map<String, String>, #Body jsonObject: JsonObject): Single<JsonObject>
2nd argument here i have JsonObject. You can replace with anything whatever you need to pass or you can remove it also.
In Java
HashMap<String, String> headers = new HashMap<String, String>();
headers.put("KEY_AUTHORIZATION","paste AUTHORIZATION value here");
headers.put("KEY_TOKEN", "paste TOKEN value here");
JsonObject jsonObject= new JsonObject();
I am passing here header and other data also
Calling of fun:-
postEvent(headers,jsonObject);
API Declaration
#POST("/v1/post_data")
Call<JsonObject> postEvent(#HeaderMap Map<String, String> headers, #Body JsonObject jsonObject);
API Declaration with RxAndroid
#POST("/v1/post_data")
Single<JsonObject> postEvent(#HeaderMap Map<String, String> headers, #Body JsonObject jsonObject);
2nd argument here i have JsonObject. You can replace with anything whatever you need to pass or you can remove it also.

A request Header can be updated dynamically using the #Header annotation. A corresponding parameter must be provided to the #Header. If the value is null, the header will be omitted. Otherwise, toString will be called on the value, and the result used.
#GET("user")
Call<User> getUser(#Header("Authorization") String authorization)

When the last part of this answer
How to dynamically set headers in Retrofit (Android)
did not work for me (halfway of the project), I improved it:-
public class MyRetrofitInterceptor implements RequestInterceptor {
// volatile variable
public static String token = null; //change at start of app
#Override
public void intercept(RequestFacade req) {
// change token from outside the class.
if (token != null) {
req.addHeader("X-Authorization", token);
}
}
It worked as soon as the token was updated from the response from the server API.
I think it worked as the string variable 'token' was used as the reference to its value, in global terms (being public static).

Related

Android, OkHttp, AWS4Signer, aws-api-gateway - "Missing Authentication Token"

i am trying to sign a http request to aws api gateway in android using okhttp. i have more or less used the code in this stackoverflow question stackoverflow question
i use CognitoCachingCredentialsProvider() to get a credentialsProvider object. i then use getCredentials() to get the credentials. i then use the following: credentials.getAWSAccessKeyId(), credentials.getAWSSecretKey() and credentials.getSessionToken() to get the necessary keys and token. i use them in postman and am able to successfully execute the api gateway.
the request fails in android using okhttp, returning a code 403 with the message "Missing Authentication Token".
this is how i prepare the request: i build a DefaultRequest object, setting the endpoint and httpmethod. i then use AWS4Signer to sign the request, passing the credentials object as the signer.sign(defaultRequest, credentials) parameter.
i get a map of headers by calling getHeaders() on the defaultRequest. i create two lists, one called key for the key and one called value for the value. i then loop through the map, loading the keys and corresponding values into the two lists.
i then build my okhttp request as follows:
Request request = new Request.Builder()
.url(my ApiEndPoint)
.addHeader(key.get(0), value.get(0))
.addHeader(key.get(1), value.get(1))
.addHeader(key.get(2), value.get(2))
.addHeader(key.get(3), value.get(3))
.addHeader("Content-Type", "application/x-www-form-urlencoded")
.post(body)
.build();
i notice the following:
in the headers map, key x-amz-security-token has a value ....ending in hKADF87VZ44w9IvZ1gU=
printing out the okhttp request, the key x-amz-security-token has a value .... ending in hKADF87VZ44w9IvZ1gU\u003d
the = is replaced by \u003d, could this be the problem? if so, how to prevent this?
otherwise, any help in solving this problem will be greatly appreciated.
thanks
managed to solve the problem. seems that assigning the headers to the OkHttp request was the problem. so here's my code:
i first get AWSSessionCredentials credentials. then:
AmazonWebServiceRequest amazonWebServiceRequest = new AmazonWebServiceRequest() {
};
String API_GATEWAY_SERVICE_NAME = "execute-api";
com.amazonaws.Request requestAws = new DefaultRequest(amazonWebServiceRequest, API_GATEWAY_SERVICE_NAME);
you can use either the service endpoint:
URI uri = URI.create("https://apigateway.eu-west-1.amazonaws.com");
or your api url (the invoke url for api as per Api Gateway console Stages option (The deployed api)):
String invokeUrl = "https://xxxx.execute-api.eu-west-1.amazonaws.com/yyy/zzzzz";
// using the invoke url
URI uri = URI.create(invokeUrl);
requestAws.setEndpoint(uri);
requestAws.setResourcePath(invokeUrl);
requestAws.setHttpMethod(HttpMethodName.POST);
now sign the request
AWS4Signer signer = new AWS4Signer();
signer.setServiceName(API_GATEWAY_SERVICE_NAME);
signer.setRegionName(Region.getRegion(Regions.EU_WEST_1).getName());
signer.sign(requestAws, credentials);
get the headers
// get map of headers
Map<String, String> headers = requestAws.getHeaders();
// create objects for the headers to add manually in OkHttp request builder
String x_date = null;
String x_token = null;
String authorization = null;
//get and assign values
for (Map.Entry<String, String> entry : headers.entrySet()) {
if (entry.getKey().equals("x-amz-security-token")) {
x_token = entry.getValue();
}
if (entry.getKey().equals("X-Amz-Date")) {
x_date = entry.getValue();
}
if (entry.getKey().equals("Authorization")) {
authorization = entry.getValue();
}
}
build the OkHttp request:
Request request = new Request.Builder()
.url(invokeUrl)
.addHeader("Content-Type", "application/json")
.addHeader("X-Amz-Date", x_date)
.addHeader("x-amz-security-token", x_token)
.addHeader("Authorization", authorization)
.post(body)
.build();
now make your OkHttp call.
hope this is helpful to someone.

Redundancy requests with Retrofit

I need to build in redundancy into my app where if a server is down it will try a backup redundancy server upon failure of the first request.
Aside from doing
Call<LoginResult> loginCall = apiInterface.login(....);
loginCall.enqueue(new Callback<LoginResult>() {
#Override
public void onResponse(Call<LoginResult> call, Response<LoginResult> response) {
if(response.isSuccessful){
//do normal stuff
}else{
//try second url
}
}
#Override
public void onFailure(Call<LoginResult> call, Throwable t) {
//Try second url
}
}
I don't see a clean way to do this. Creating another retrofit request inside the error block or non-successful block would add a lot of code complexity.
Is there an easier way to handle this in Retrofit or OkHttp?
I have here an option with OkHttp interceptors. The idea is that if the request fails you replace the url and execute the request again.
The following is an api client to the OpenWeather Api. If you want to try out the example you'll need to sign up and get an api key. It should be free so I hope this is ok.
I'll post here the full code and then walk you through it.
private final static String API_KEY = "<API KEY HERE>";
private static class Weather {
#SerializedName("id")
#Expose
private String id;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}
private static final String GOOD_HOST = "api.openweathermap.org";
private static final String BAD_ENDPOINT = "https://api.aaaaaaaaaaa.org";
interface WeatherApiClient {
#GET("/data/2.5/weather")
Call<Weather> get(
#Query("q") String query,
#Query("appid") String apiKey);
}
private static class ReplicaServerInterceptor implements Interceptor {
#Override public okhttp3.Response intercept(Chain chain)
throws IOException {
try {
okhttp3.Response response = chain.proceed(chain.request());
return response;
} catch (IOException e) {
// Let's build a new request based on the old one
Request failedRequest = chain.request();
HttpUrl replicaUrl = failedRequest.url()
.newBuilder()
.host(GOOD_HOST)
.build();
okhttp3.Request request = failedRequest.newBuilder()
.url(replicaUrl)
.build();
return chain.proceed(request);
}
}
}
public static void main(String[] args) {
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.addInterceptor(new ReplicaServerInterceptor())
.build();
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(BAD_ENDPOINT)
.addConverterFactory(GsonConverterFactory.create())
.client(okHttpClient)
.build();
WeatherApiClient weatherApiClient =
retrofit.create(WeatherApiClient.class);
weatherApiClient.get("Lisbon,pt", API_KEY)
.enqueue(new Callback<Weather>() {
#Override public void onResponse(
Call<Weather> call,
Response<Weather> response) {
// This might be null sometimes because
// the api is not super reliable, but I didn't
// add code for this
System.out.println(response.body().id);
}
#Override public void onFailure(
Call<Weather> call,
Throwable t) {
t.printStackTrace();
}
});
}
To be able to fake a server failure I prepare retrofit to call a non existent url - BAD_ENDPOINT. This will trigger the catch clause inside the interceptor.
The interceptor itself is obviously the key thing here. It intercepts every call from retrofit and executes the call. If the call throws an error because the server is down, then it will raise an IOException. Here I copy the request being made and change the url.
Changing the url means changing the host:
HttpUrl replicaUrl = failedRequest.url()
.newBuilder()
.host(GOOD_HOST)
.build();
If you just call url(<some url>) in the request builder, everything gets replaced. Query parameters, protocol, etc. This way, we preserve these from the original request.
(OkHttp offers newBuilder methods which copy the data from the current object and let you just edit what you want. Just like kotlin's copy. This is why we can simply change the url and be safe that everything else remains the same)
I then build the new request with the url and execute it:
okhttp3.Request request = failedRequest.newBuilder()
.url(replicaUrl)
.build();
return chain.proceed(request);
Interceptors work on a chain pattern, that's why calling proceed will call the next interceptor on the chain. In this case we just need to actually make the request.
I didn't bother copying the entire weather resource, so I'm just using the id. I think that's not the main focus of the question
As I said before, this is meant as a proof of concept. As you noticed I'm try-catching the execution of the call, but in your case it might be that the call actually succeeds executing, but the http response is not a 2XX. The okhttp response objects have methods that help you checking if the response was successful namely - isSuccessful(). The idea is the same - Build a new request and carry on if it's not successful.
I didn't bother treating any errors from the replica in this example. They'll just be forwarded to the retrofit client.
As you can see retrofit has no clue where the response is coming from. This might or not be good. Also, the response body needs to be the same from both servers, which I guess it's the case.
Lastly I'm sorry for the awkward okhttp3.Response name spacing there. I was using both Response from retrofit and okhttp and hence had to avoid the name clash.
Versions used for this example: Retrofit 2.3.0 and the okhttp bundled with that

Retrofit2 - Can't get basic authentication for webAPI with dynamic URL

I'm familiar with how to use dynamic URLs with Retrofit2 but having issue sending username & password in the request. The webAPI works by entering a URL, a login prompt appears on the screen for a username & password, after authentication a JSON response is displayed. The interface is defined for a dynamic URL:
#GET
public Call<User> getJSON(#Url String string);
My request is as follows:
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(API_URL)
.addConverterFactory(GsonConverterFactory.create())
.build();
LoginService service = retrofit.create(LoginService.class);
Call<User> call = service.getJSON("https://username:password#api.url.com/");
call.enqueue(new Callback<User>() {
#Override
public void onResponse(Call<User> call, retrofit2.Response<User> response) {
System.out.println("Response status code: " + response.code());
I'm certain the URL is correct as it works in the browser & but I keep getting error the username & password aren't correct?
I/System.out: Response status code: 401
Also, as far as I can tell I can only use #GET rather than #POST because whenever I try #POST the response code is:
I/System.out: Response status code: 405
At first I tried to follow something similar to this post using an encoded flag because it's an example of how to use #PATH & #URL with Retrofit2 but didn't have any success. That's why I tried the username:password# prepend to the URL. Most of the other examples all use the #POST method.
Any feedback or ideas on how I can authenticate? Thanks
Not sure how to do it in retrofit, but you can add it via an OkHttp interceptor --
OkHttpClient client = new OkHttpClient().newBuilder().addNetworkInterceptor(
new Interceptor() {
#Override
public Response intercept(Interceptor.Chain chain) throws IOException {
Request request = chain.request();
HttpUrl url = request.url();
url = url.newBuilder().username("username").password("password").build();
Request newRequest = request.newBuilder().url(url).build();
return chain.proceed(newRequest);
}
}
).build();
be sure to add this client to your retrofit instance --
Retrofit retrofit = new Retrofit.Builder()
.client(client)
.baseUrl(API_URL)
.addConverterFactory(GsonConverterFactory.create())
.build();
Another way to use basic authentication with Retrofit2 would be to pass the authentication string as an argument to your interface method.
So you would change the method signature to:
#GET
public Call<User> getJSON(#Url String string, #Header("Authorization") String myAuthString);
And then call it like this:
Call<User> call = service.getJSON("https://api.url.com/", "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==");
Where you substitute QWxhZGRpbjpvcGVuIHNlc2FtZQ== for your Base64-encoded username:password string.
If you need to pass the username and password for every API call and want to keep the method signatures clean, it might be better to use the custom OkHttpInterceptor method instead.

error with more than one #Body field - retrofit2 beta3

I am just starting out with retrofit for android. I am getting an error when I try to specify 2 fields for the body of a post request:
Multiple #Body method annotations found. (parameter #2) for method
The Call is defined in my API interface file as:
#POST("auth/login")
Call<UserData> login(#Body String username, #Body String password);
And I create the call with:
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(baseURL)
.addConverterFactory(GsonConverterFactory.create())
.build();
APIService service = retrofit.create(APIService.class);
Call<UserData> call = service.login(username, password);
error is generated when the call is created (do not have a chance to execute it).
When I remove one of the body fields it seems to work fine.
Any Ideas?
Using multiple #Body is bad idea, because #Body here means message Body of HTML POST.
(Detail: How to send data in the HTTP request body when using an HTML form?)
I suggest you to define a class containing both username and password, like below.
public class LoginInformation {
String username;
String password;
}
And, fill your information on that class instance, and use that.
#POST("auth/login")
Call<UserData> login(#Body LoginInformation loginInformation);
Any HTTP request is allowed to contain only one message body , if you try adding two #Body parameters you will get this message "Multiple #Body method annotations found."
And you can fix it by following this:
You can send multiple or different type of objects at the same time using HashMap or single type, for example:
HashMap<String, Object> map = new HashMap<>();
map.put("password", "123456");
map.put("username", "Jake Warthon");
or
public class User(){
private String username;
private String password;
public void setUsername(String username){
this.username = username;
}
public void setPassword(String password){
this.password = password;
}
}
User user = new User();
user.setUsername("Jake Warthon")
user.setPassword("123456");
map.put("user", user);
You can append more data (like different type of objects) in a single body if you want (optional)
map.put("user", user);
map.put("authorization", "12uh3u12huhcd2");
map.put("something", someobject);
You have to change the body type of the request to receive a Hashmap or User
#POST("auth/login")
Call<UserData> login(#Body HashMap map);
or
#POST("auth/login")
Call<UserData> login(#Body User user);
Finally you pass the data to service like you already did.
Call<UserData> call = service.login(map);
or
Call<UserData> call = service.login(user);
And remember, the server side have to implement it correctly to receive the data as a map.

retrofit #POST parameters are sent via GET

I am trying to send multiple parameters (as I usually do) with #QueryMap but via POST this time using retrofit.
Retrofit API
#POST("/request.php")
void sendRequest(#QueryMap Map<String, String> parameters, retrofit.Callback<RequestSendResponse> callback);
Map that is being send
public static Map<String, String> parametersSendRequest(Context sender, Request request)
{
Map <String, String> parameters = new HashMap<>();
Operator operator = AppConfig.config().operator;
parameters.put("user_name", request.user_name);
parameters.put("user_surname", request.user_surname);
parameters.put("user_gender", request.user_gender);
parameters.put("user_relationship", request.user_relationship);
parameters.put("user_dob", request.user_dob);
parameters.put("operator_name", operator.name);
parameters.put("request_photoid", request.request_photoid);
parameters.put("request_user_content", request.request_user_content);
parameters.put("request_title", request.request_title);
parameters.put("uuid", UUID(sender));
parameters.put("response_type", "json");
parameters.put("platform", "android");
parameters.put("mode", "send");
return parameters;
}
Server result
{"POST":[],"GET":{"operator_name":....}}
I can see that even the method is sent to POST, #QueryMap causes these parameters to be sent over GET. Even when I use #Body instead of #QueryMap, retrofit converts my #QueryMap to a JSON object, which is not I want.
All I want to do is to send param1=value1&param2=value2 on my request body, instead of a JSON object (Using my Map<String, String>)
to send parameters using POST (#FormUrlEncoded and #FieldMap)
#FormUrlEncoded
#POST("/request.php")
void sendRequest(#FieldMap Map<String, String> parameters, retrofit.Callback<RequestSendResponse> callback);
This one works for me
#FormUrlEncoded
#POST("/profile/")
void getUserProfile(#Field("whatever")String whatever, Callback<Response> callback);
Pay special attention to the final slash after "profile". I had problems because I was not adding it. Hope it helps.

Categories

Resources