Retrofit2 204 No Content has content exception - android

I get from server empty json ("{}") as a delete response with code 204.
In okhttp3.internal.http.HttpEngine class there is this annoying thing that gets thrown:
if ((code == 204 || code == 205) && response.body().contentLength() > 0) {
throw new ProtocolException(
"HTTP " + code + " had non-zero Content-Length: " + response.body().contentLength());
}
If you try return something without content (server side) in headers still Content-Length greater than 0;
Any non server side ideas how to solve this issue?

You can trap the ProtocolException in an interceptor and return a placeholder 204 Response. Caveats with this approach -- 1) you may end up trapping other protocol errors (too many redirects, etc). If this is a concern, you could compare e.getMessage() to okhttp's exception message and rethrow the exception if there is not a match. 2) you still don't have access to the original response, so if you are out of luck if you need to inspect any of the returned headers.
OkHttpClient.Builder builder = new OkHttpClient.Builder();
builder.addNetworkInterceptor(new Interceptor() {
#Override
public Response intercept(Chain chain) throws IOException {
Response response;
try {
response = chain.proceed(chain.request());
} catch (ProtocolException e) {
response = new Response.Builder()
.request(chain.request())
.code(204)
.protocol(Protocol.HTTP_1_1)
.build();
}
return response;
}
});

This can be avoided if used differently a bit. Instead of:
#DELETE("vehicles/{id}/")
Observable<Response<BaseResponse>> deleteVehicle(#Header("Authorization") String token, #Path("id") Long vehicleId);
I use:
#HTTP(method = "DELETE", path = "vehicles/{id}/", hasBody = true)
Observable<Response<BaseResponse>> deleteVehicle(#Header("Authorization") String token, #Path("id") Long vehicleId);

Related

How To Write Retry Interceptor For Data Stream In OkHttp?

Or thinking the interceptor for this scenario applicable ?
Our app using OkHttp for downloading files (new version of app, daily databases etc.)
Sometimes server fails just while the app streaming bytes (btw the problem is, recvfrom failed: ECONNRESET)
So to fix this case i just wanted to write OkHttp retry interceptor. But seems this is appropriate for operations which aren't streaming.
Is there a solution(like interceptor) to handle this case ?
To make more clear exposition
0%==============================100% (Just started streaming)
0%==============================100% (10% completed)
0%==============================100% (20% completed)
0%==============================100% (ECONNRESET - Connection reset by peer)
At just this point, streaming gets stopped. The thing i'm wishing from OkHttp is recognizing this situation then starting the stream from scratch (not from 20%)
Related code here, pay attention to comments
Call call = client.newCall(new Request.Builder().url(url).get().build());
Response response = call.execute();
// PROBLEM DOES NOT OCCUR THERE
// PROBLEM DOES NOT OCCUR THERE
// PROBLEM DOES NOT OCCUR THERE
if (response.code() == 200 || response.code() == 201) {
InputStream inputStream = null;
try {
long downloaded = 0;
byte[] buff = new byte[1024 * 4];
inputStream = response.body().byteStream();
long target = response.body().contentLength();
while (true) {
// EXCEPTION OCCURS THERE
// EXCEPTION OCCURS THERE
// EXCEPTION OCCURS THERE
int read = inputStream.read(buff);
if (read == -1) {
break;
}
downloaded += read;
}
...
} catch (IOException e) {
// EXCEPTION SAYS
// ECONNRESET - Connection reset by peer
...
}
}
You can write a custom Interceptor like below:
OkHttp has Interceptors. You need a custom Interceptor like one below:
public class CustomResponseInterceptor implements Interceptor {
private final String TAG = getClass().getSimpleName();
#Override
public Response intercept(Object object) throws IOException {
Request request = chain.request();
Response response = chain.proceed(request);
if (response.code() != 200//Your error code here,) {
//Cancel your Request here
return something;
}
Log.d(TAG, "INTERCEPTED:$ " response.toString());
return response;
}
The code shown is extracted from this Medium Article on Interceptors.
You can also look at this library which implements Retry Interceptors, But you can modify it for your use.
When ECONNRESET - Connection reset by peer occurs why don't you cancel your ongoing call in your catch block and start a new network call for the same file
catch (IOException e) {
// EXCEPTION SAYS
// ECONNRESET - Connection reset by peer
...
call.cancel();//something like this
startDownloadingFromScratch();//new network request to start from scratch
}
Try this.
OkHttpClient client = new OkHttpClient();
client.setConnectTimeout(CONNECT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
client.setReadTimeout(READ_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
client.interceptors().add(new Interceptor() {
#Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
// try the request
Response response = chain.proceed(request);
int tryCount = 0;
while (!response.isSuccessful() && tryCount < 3) {
Log.d("intercept", "Request is not successful - " + tryCount);
tryCount++;
// retry the request
response = chain.proceed(request);
}
// otherwise just pass the original response on
return response;
}
});
set this client as your retrofit client.
new Retrofit.Builder()
...other codes
.client(client)
...other codes
.build();
Good luck.

how to contain header when I redirect in retrofit

I tried to make oauth2 for android application. it has little bug.
My bug is It doesn't have header like Authorization when I redirect
MyCookieCode. It send Authorization when I was login. but It doesn't work when I redirect
public static Retrofit getLoginRetrofitOnAuthz() {
Retrofit.Builder builder = new Retrofit.Builder().baseUrl(ServerValue.AuthServerUrl).addConverterFactory(GsonConverterFactory.create());
if (LoginRetrofitAuthz == null) {
httpClientAuthz.addInterceptor(new Interceptor() {
#Override
public okhttp3.Response intercept(Chain chain) throws IOException {
String str = etUsername.getText().toString() + ":" + etPassword.getText().toString();
String Base64Str = "Basic " + Base64.encodeToString(str.getBytes(), Base64.NO_WRAP);
System.out.println(Base64Str);
Request request = chain.request().newBuilder().addHeader("Authorization", Base64Str).build();
return chain.proceed(request);
}
});
CookieManager cookieManager = new CookieManager();
cookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ALL);
httpClientAuthz.cookieJar(new JavaNetCookieJar(cookieManager));
LoginRetrofitAuthz = builder.client(httpClientAuthz.build()).build();
}
return LoginRetrofitAuthz;
}
Server Result (Top-Login, Bottom, Redirect)
Do you know how to staying header on redirect ?
in fact the sinner is OkHttp, but not Retrofit.
OkHttp removes all authentication headers on purpose:
https://github.com/square/okhttp/blob/7cf6363662c7793c7694c8da0641be0508e04241/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java
// When redirecting across hosts, drop all authentication headers. This
// is potentially annoying to the application layer since they have no
// way to retain them.
if (!sameConnection(url)) {
requestBuilder.removeHeader("Authorization");
}
Here is the discussion of this issue: https://github.com/square/retrofit/issues/977
You could use the OkHttp authenticator. It will get called if there is a 401 error returned. So you could use it to re-authenticate the request.
httpClient.authenticator(new Authenticator() {
#Override
public Request authenticate(Route route, Response response) throws IOException {
return response.request().newBuilder()
.header("Authorization", "Token " + DataManager.getInstance().getPreferencesManager().getAuthToken())
.build();
}
});
However in my case server returned 403 Forbidden instead of 401. And I had to get
response.headers().get("Location");
in-place and create and fire another network request:
public Call<Response> getMoreBills(#Header("Authorization") String authorization, #Url String nextPage)

How to refresh Cookies using okhttp3?

The REST API I connect to uses cookie-based authorization. Basically, issuing a call, I get a cookie, which then I use in all my requests. That cookie has an expiry time on the Server side. Once cookie expired, I get notified, I have to acquire a new cookie to proceed.
To achieve this, I am using OkHttp3 client, to which I attached a PersistentCookieJar. Somewhere in between, I also have an Interceptor that catches the session expired responses, and tries to renew the cookie. After my Interceptor does the call to renew the cookie, it re-tries the previously failed call. Which, fails again. I suppose it is because the retry of the call is done with old cookie.
Any suggestions?
Some code snippets below:
//OkHttp client with the interceptor and the cookiejar
OkHttpClient.Builder builder = new OkHttpClient.Builder();
builder.addInterceptor(sessionExpiryInterceptor);
if (cache != null) {
builder.cache(cache);
}
return builder
.cookieJar(cookieJar)
.connectTimeout(1, TimeUnit.MINUTES)
.build();
The PersistentCookieJar is used as described here: https://github.com/franmontiel/PersistentCookieJar
And last, the Interceptor:
public class SessionExpiryInterceptor implements Interceptor {
#Override public Response intercept(Chain chain) throws IOException {
final Request request = chain.request();
final Response response = chain.proceed(request);
//analyze to see if it is "session expired"
String bodyString = response.body().string();
Response copyResponse = copyResponse(response, bodyString);
//attempt to parse the original body
if (isRefreshRequired(bodyString)) {
//we send the response copy from here on out, as the original no longer as a "body" - extracted and parsed earlier
return refreshData(request, copyResponse, chain);
} else {
//return the copy since body was extracted earlier
return copyResponse;
}
}
private boolean isRefreshRequired(String responseBody) throws IOException {
//does some parsing of the responseBody to extract a "status" and "message"
return status == RESPONSE_CODE_NO_SESSION || message.contains("session");
}
private Response refreshData(Request request, Response responseCopy, Chain chain) throws IOException {
//does some checks before attempting to actually refresh the cookie,
mAuthService.login(user, pass)
.subscribeOn(Schedulers.immediate())
.observeOn(Schedulers.immediate())
.subscribe(createLoginSubscriber());
if (mDataRefreshed) {
// rebuild the request with the new acquired cookie
Request retry = createRetryRequest(request);
return chain.proceed(retry);// !!! this call fails again - I get "session expired" response message.
} else {
//for whatever reason the refresh failed - send back the original response's copy
return responseCopy;
}
}
private Subscriber<LoginResponse> createLoginSubscriber() {
return new Subscriber<LoginResponse>() {
#Override public void onCompleted() {
}
#Override public void onError(Throwable e) {
LOG.e(e, "login failed" + e.getMessage());
}
#Override public void onNext(LoginResponse response) {
if(response.isSuccess()) {
mDataRefreshed = true;
} else {
mDataRefreshed = false;
//some cleanup code here
}
}
};
}
private Request createRetryRequest(Request source) {
//I suspect this copy might be the issue?
return source.newBuilder().build();
}
//copies the given response rewriting the body with the given one with character set UTF-8
private Response copyResponse(Response original, String body) {
return original.newBuilder()
.body(new RealResponseBody(original.headers(), new Buffer().writeUtf8(body)))
.build();
}
}

Moodle Login with an external Android App (Okhttp)

Thanks for your time. Im programming an Android App for School and need a Authentification Form for the Moodle Login (Server ends with /index.php).
My Goal: Get the "Set-Cookie" Header in the Response with the active Cookie in it. This is given, if the Server Status returnes "303(See Other)".
My Question: What should I post to the Login Server, to get the Status "303" (and therefore also the right Cookie) ?
I dont know, if the ".add" Method is the right or wrong or if I should send more or less to the Server.
class MoodleLogin{
public static void FirstRequest(String url) throws Exception {
final OkHttpClient client = new OkHttpClient();
//FORM BODY
RequestBody formBody = new FormBody.Builder()
.add("username", USERNAME) // Username
.addEncoded("password", PASSWORT) //Passwort
.add("token", "6f65e84f626ec97d9eeee7ec45c88303") //Security Key for Moodle mobile web service (I dont know if I need that)
.build();
// A normal Request
Request request = new Request.Builder()
.url(url)
.post(formBody)
.build();
// Asynchronous Call via Callback with onFailure and onResponse
client.newCall(request).enqueue(new okhttp3.Callback() {
#Override
public void onFailure(Call call, IOException e) {
Log.i("Internet", "request failed: " + e.toString());
}
#Override
public void onResponse(Call call, Response response) throws IOException {
if (!response.isSuccessful()) { // Server Status is not 200 - THAT IS WHAT I WANT
Log.d("Server Status", response.message().toString());
throw new IOException("Unexpected code ");
} else { // Server Status is 200(OK)
Log.d("Server Status", response.message().toString());
}
response.body().close();
}
}); // End of "onResponse"
}
This peace of Code only returns the Server Status "200" (what is wrong in my case).
Do anyone know, what I must change to get the Status "303" ? I tested it with hurl.it (A Website for HTTP Requests) and it works only if I post the "username" and "password" like normal Parameters.
Im grateful for every answer and tip !
I answered it myself. So here is the right code:
Connection.Response res = Jsoup
.connect("your Moodle-Login Page")
.data("username", username, "password", password)
.method(Connection.Method.POST)
.execute();
String Login_cookies = res.cookies().toString();
String cookie = Login_cookies.substring(1, 50);
System.out.println("COOKIES: "+cookie);

android - OkHttp interceptor - response already "consumed"

I'm trying to use this interceptor for authentication:
public class CustomInterceptor implements Interceptor {
#Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
// try the request
Response response = chain.proceed(request);
if (response shows expired token) {
// get a new token (I use a synchronous Retrofit call)
// create a new request and modify it accordingly using the new token
Request newRequest = request.newBuilder()...build();
// retry the request
return chain.proceed(newRequest);
}
// otherwise just pass the original response on
return response;
}
The problem is that my check (response shows expired token) is not status-related, I need to check the actual response (body content).
So after the check, the response is "consumed" and any attempt to ready the body will fail.
I've tried to "clone" the response buffer before read, like:
public static String responseAsString(Response response ){
Buffer clonedBuffer = response.body().source().buffer().clone();
return ByteString.of(clonedBuffer.readByteArray()).toString();
}
but it doesn't work, the clonedBuffer is empty.
Any help will be appreciated.
I just had the same problem myself, and the solution I found was to consume the response's body and build a new response with a new body. I did it like so:
...
Response response = chain.proceed(request);
MediaType contentType = response.body().contentType();
String bodyString = response.body().string();
if (tokenExpired(bodyString)) {
// your logic here...
}
ResponseBody body = ResponseBody.create(contentType, bodyString);
return response.newBuilder().body(body).build();

Categories

Resources