How To Write Retry Interceptor For Data Stream In OkHttp? - android

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.

Related

OkHttp post streaming - switching from cellular to WiFi

I'm using OkHttp to stream files up to the server. My code looks something like this:
OkHttpClient client = new OkHttpClient.builder()
.connectTimeout(20, TimeUnit.SECONDS)
.readTimeout(20, TimeUnit.SECONDS)
.writeTimeout(20, TimeUnit.SECONDS)
.retryOnConnectionFailure(false)
.build();
Response response = null;
try {
RequestBody requestBody = new RequestBody() {
private long uploadedBytes;
#Override
public MediaType contentType() {
// return type based on file
}
#Override
public void writeTo(BufferedSink sink) throws IOException {
byte[] buffer = new byte[8 * 1024];
int count = 0;
while ((count = source.read(buffer)) != -1) {
sink.write(buffer, 0, count);
}
source.close();
}
}
final Request request = new Request.Builder()
.url(uploadUrl)
.put(requestBody)
// header stuff
.build();
response = client.newCall(request).execute();
} catch (IOException e) {
// handle
}
In my code I'm streaming files (bytes) and my timeout set on the OkHttpClient is 20 seconds.
So, this is my scenario - a user is uploading a file on LTE when the phone connects to a WiFi spot. Right now what happens is that OkHttp continues to upload for some time (which I think is the timeout value but I'm not too sure), then fails with a broken pipe.
javax.net.ssl.SSLException: Write error: ssl=0x74a6315680: I/O error during system call, Broken pipe
at com.android.org.conscrypt.NativeCrypto.SSL_write(Native Method)
at com.android.org.conscrypt.OpenSSLSocketImpl$SSLOutputStream.write(OpenSSLSocketImpl.java:824)
at okio.Okio$1.write(Okio.java:78)
...
At that point the task is rescheduled and picked up on WiFi whenever JobScheduler feels like it. So all in all, the switch between LTE and WiFi becomes quite painful and takes up to 30 seconds on a good day (timeout plus scheduler).
Considering that the reverse scenario works perfectly fine (WiFi -> LTE), I was hoping maybe there's something in OkHttp that I could use to improve this transition? My version of OkHttp is 3.4.1

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();
}
}

How to check if OkHttp3 request timed out?

I am making an HTTP GET request to a Web Server using the code below:
private final OkHttpClient client = new OkHttpClient();
public void run() throws Exception {
Request request = new Request.Builder()
.url("http://publicobject.com/helloworld.txt")
.build();
Response response = client.newCall(request).execute();
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
Headers responseHeaders = response.headers();
for (int i = 0; i < responseHeaders.size(); i++) {
System.out.println(responseHeaders.name(i) + ": " + responseHeaders.value(i));
}
System.out.println(response.body().string());
}
According to OkHttp document as shown below, okhttp3 execute calls will throw IOException if the request could not be executed due to cancellation, a connectivity problem or timeout.
Throws:
IOException - if the request could not be executed due to cancellation, a connectivity problem or timeout. Because networks can fail during an exchange, it is possible that the remote server accepted the request before the failure.
I would like to know if there's a way to know if IOException was due to the request getting timed out?
Although I could not find any information about timeout exception in okHttp3 documentation, looking at tests here shows that a SocketTimeoutException exception will be raised in case of timeouts.
So, to answer my own question, we can catch SocketTimeoutException first in order to know if IOException was due to the request getting timed out like below:
try {
// make http request
} catch (SocketTimeoutException e) {
// request timed out
} catch (IOException e) {
// some other error
}

Retrofit2 204 No Content has content exception

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);

Spring Android POST no return type application/octet-stream in ResponseEntity

I am working with a Node.js server running Express 3.0.
During one part in my application I POST json to a URI to purchase a powerup at /api/transactions/powerup. This has several return options.
I will either receive a 201 meaning that the transaction was valid, a 404 that the request was not valid due to insufficient funds. My problem is that if I want to tell the android client whether it was successful based only on the headers that are in the ResponseEntity.
There is no json that is passed back to the client because the HTTP codes can tell me if it was successful. Does anyone know how to do this?
PowerupPurchase purchase = new PowerupPurchase();
purchase.setPowerId(params[0]);
final String url = getString(R.string.external_api_url) + "/transactions/powers?apiKey="+mCurrentUser.getApiKey();
Log.i(TAG,url);
// create the headers
HttpHeaders requestHeaders = new HttpHeaders();
requestHeaders.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
requestHeaders.setAcceptEncoding(ContentCodingType.GZIP);
requestHeaders.setContentType(MediaType.APPLICATION_JSON);
// Create the Json that will be exchanged
HttpEntity<?> requestEntity = new HttpEntity<PowerupPurchase>(purchase, requestHeaders);
// Create a new RestTemplate instance
RestTemplate restTemplate = new RestTemplate();
MappingJacksonHttpMessageConverter converter = new MappingJacksonHttpMessageConverter();
List<MediaType> mediaList = new LinkedList<MediaType>();
mediaList.add(new MediaType("application","octet-stream",Charset.forName("UTF-8")));
converter.setSupportedMediaTypes(mediaList);
restTemplate.getMessageConverters().add(converter);
try {
// Make the network request
ResponseEntity<?> response = restTemplate.postForEntity(url, requestEntity, Object.class);
// HOW DO I GET THE HEADERS??
response.getHeaders()
400's seem to always throw errors in Spring Android is there anyway that I can simply look at the http header and not worry about a return object map to?
DefaultResponseErrorHandler used by RestTemplate throws exceptions for all 400 and 500 series HTTP Status responses. One option is to use a custom ResponseErrorHandler with your RestTemplate request that won't throw exceptions. You can modify your request to use the custom handler and then check for the expected status codes:
// set the custom error handler
restTemplate.setErrorHandler(new CustomErrorHandler());
// make the request
ResponseEntity<Void> response = restTemplate.postForEntity(url, requestEntity, null);
// check status code
HttpStatus statusCode = response.getStatusCode();
if (statusCode == HttpStatus.CREATED) {
// handle success
} else if (statusCode == HttpStatus.NOT_FOUND) {
// handle error
}
Here is an example of an "empty" implementation that won't throw any exceptions:
class CustomErrorHandler implements ResponseErrorHandler {
#Override
public void handleError(ClientHttpResponse response) throws IOException {
}
#Override
public boolean hasError(ClientHttpResponse response) throws IOException {
return false;
}
}
Because the server might return other status codes, it's probably a good idea to look for the expected status codes and not throw exceptions for those particular ones. But it simply depends on what your needs are. This example extends the default handler used by RestTemplate and checks for the expected status codes:
class CustomErrorHandler extends DefaultResponseErrorHandler {
#Override
public void handleError(ClientHttpResponse response) throws IOException {
// check for expected status codes
if (isExpected(response.getStatusCode())) {
return;
}
super.handleError(response);
}
#Override
public boolean hasError(ClientHttpResponse response) throws IOException {
// check for expected status codes
if (isExpected(response.getStatusCode())) {
return false;
}
return super.hasError(response);
}
private boolean isExpected(HttpStatus statusCode) {
return (statusCode == HttpStatus.CREATED || statusCode == HttpStatus.NOT_FOUND);
}
}
Surely you can use the exception as failed?
boolean success;
try{
ResponseEntity<?> response = restTemplate.postForEntity(url, requestEntity, Object.class);
int result = response.getHeaders().getStatus()...
success = result >= 200 && result < 300;
}catch(Exception e){
success = false;
}

Categories

Resources