Upgrading POST Request w / HTTP Authentication Digest: "Unable to retry HTTP streaming body"
I am trying to do a digest validation using Retrofit. My first solution is to install the OkHttp Authenticator implementation on the OkHttpClient:
class MyAuthenticator implements Authenticator {
private final DigestScheme digestScheme = new DigestScheme();
private final Credentials credentials = new UsernamePasswordCredentials("user", "pass");
@Override public Request authenticate(Proxy proxy, Response response) throws IOException {
try {
digestScheme.processChallenge(new BasicHeader("WWW-Authenticate", response.header("WWW-Authenticate")));
HttpRequest request = new BasicHttpRequest(response.request().method(), response.request().uri().toString());
String authHeader = digestScheme.authenticate(credentials, request).getValue();
return response.request().newBuilder()
.addHeader("Authorization", authHeader)
.build();
} catch (Exception e) {
throw new AssertionError(e);
}
}
@Override public Request authenticateProxy(Proxy proxy, Response response) throws IOException {
return null;
}
}
This works great for GET requests via Retrofit. However, as described in this StackOverflow question, POST requests result in a "Can not retry streamed HTTP body" exception:
Caused by: java.net.HttpRetryException: Cannot retry streamed HTTP body
at com.squareup.okhttp.internal.http.HttpURLConnectionImpl.getResponse(HttpURLConnectionImpl.java:324)
at com.squareup.okhttp.internal.http.HttpURLConnectionImpl.getResponseCode(HttpURLConnectionImpl.java:508)
at com.squareup.okhttp.internal.http.HttpsURLConnectionImpl.getResponseCode(HttpsURLConnectionImpl.java:136)
at retrofit.client.UrlConnectionClient.readResponse(UrlConnectionClient.java:94)
at retrofit.client.UrlConnectionClient.execute(UrlConnectionClient.java:49)
at retrofit.RestAdapter$RestHandler.invokeRequest(RestAdapter.java:357)
at retrofit.RestAdapter$RestHandler.invoke(RestAdapter.java:282)
at $Proxy3.login(Native Method)
at com.audax.paths.job.LoginJob.onRunInBackground(LoginJob.java:41)
at com.audax.library.job.AXJob.onRun(AXJob.java:25)
at com.path.android.jobqueue.BaseJob.safeRun(BaseJob.java:108)
at com.path.android.jobqueue.JobHolder.safeRun(JobHolder.java:60)
at com.path.android.jobqueue.executor.JobConsumerExecutor$JobConsumer.run(JobConsumerExecutor.java:172)
at java.lang.Thread.run(Thread.java:841)
Jesse Wilson explains that we cannot resubmit our request after authentication because the POST body has already been fetched. But we need the returned header WWW-Authenticate
because of the digest authentication, so we can't use RequestInterceptor
to just add the header. It might be possible to make a separate HTTP request in RequestInterceptor
and use the header WWW-Authenticate
in the response, but that seems like a hack.
Is there a way to get around this?
source to share
As a workaround, I ended up replacing OkHttp with Apache HttpClient, which has digest authentication built in. Provide an implementation for retrofit.client.Client that delegates your requests to Apache HttpClient:
import retrofit.client.Client;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.auth.Credentials;
import org.apache.http.auth.AuthScope;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.HttpClientBuilder;
import retrofit.client.Request;
import retrofit.client.Response;
public class MyClient implements Client {
private final CloseableHttpClient delegate;
public MyClient(String user, String pass, String hostname, String scope) {
Credentials credentials = new UsernamePasswordCredentials(user, pass);
AuthScope authScope = new AuthScope(hostname, 443, scope);
CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
credentialsProvider.setCredentials(authScope, credentials);
delegate = HttpClientBuilder.create()
.setDefaultCredentialsProvider(credentialsProvider)
.build();
}
@Override public Response execute(Request request) {
//
// We're getting a Retrofit request, but we need to execute an Apache
// HttpUriRequest instead. Use the info in the Retrofit request to create
// an Apache HttpUriRequest.
//
String method = req.getMethod();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
if (request.getBody() != null) {
request.getBody().writeTo(bos);
}
String body = new String(bos.toByteArray());
HttpUriRequest wrappedRequest;
switch (method) {
case "GET":
wrappedRequest = new HttpGet(request.getUrl());
break;
case "POST":
wrappedRequest = new HttpPost(request.getUrl());
wrappedRequest.addHeader("Content-Type", "application/xml");
((HttpPost) wrappedRequest).setEntity(new StringEntity(body));
break;
case "PUT":
wrappedRequest = new HttpPut(request.getUrl());
wrappedRequest.addHeader("Content-Type", "application/xml");
((HttpPut) wrappedRequest).setEntity(new StringEntity(body));
break;
case "DELETE":
wrappedRequest = new HttpDelete(request.getUrl());
break;
default:
throw new AssertionError("HTTP operation not supported.");
}
//
// Then execute the request with `delegate.execute(uriRequest)`.
//
// ...
//
}
Then install the new Client implementation on your RestAdapter.Builder:
RestAdapter restAdapter = new RestAdapter.Builder()
.setClient(new MyClient("jason", "pass", "something.com", "Some Scope"))
.setEndpoint("https://something.com/api")
.build();
source to share
Copy the Retofit ApacheClient implementation from https://github.com/square/retrofit/blob/master/retrofit/src/main/java/retrofit/client/ApacheClient.java to your own source, rename it to MyClient and add this constructor:
public MyClient(String user, String pass) {
this();
DefaultHttpClient defaultHttpClient = (DefaultHttpClient)this.client;
Credentials credentials = new UsernamePasswordCredentials(user, pass);
AuthScope authScope = new AuthScope(AuthScope.ANY_HOST, AuthScope.ANY_PORT, AuthScope.ANY_REALM);
CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
credentialsProvider.setCredentials(authScope, credentials);
defaultHttpClient.setCredentialsProvider(credentialsProvider);
}
Now you can use MyClient in your RestAdapter linker: .setClient(new MyClient("user", "pass"))
source to share
I solved this problem by using OkHttp client with Interceptor I used three times to make the request. I created an interceptor below:
import android.content.Context;
import android.util.Log;
import com.crmall.androidcommon.helpers.CacheManagerHelper;
import com.crmall.maisequipe.helper.ApplicationSystemHelper;
import com.squareup.okhttp.Interceptor;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.Response;
import org.apache.http.HttpRequest;
import org.apache.http.auth.AuthenticationException;
import org.apache.http.auth.Credentials;
import org.apache.http.auth.MalformedChallengeException;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.impl.auth.DigestScheme;
import org.apache.http.message.BasicHeader;
import org.apache.http.message.BasicHttpRequest;
import java.io.IOException;
import java.net.HttpURLConnection;
/**
* Interceptor used to authorize requests.
*/
public class AuthorizationInterceptor implements Interceptor {
private final DigestScheme digestScheme = new DigestScheme();
private final Credentials credentials;
public AuthorizationInterceptor(String username, String password) throws IOException, ClassNotFoundException {
credentials = new UsernamePasswordCredentials(username, password);
}
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Response response = chain.proceed(request);
if (response.code() == HttpURLConnection.HTTP_UNAUTHORIZED) {
String authHeader = buildAuthorizationHeader(response);
if (authHeader != null) {
request = request.newBuilder().addHeader("Authorization", authHeader).build();
response = chain.proceed(request);
if (response.code() == HttpURLConnection.HTTP_BAD_REQUEST || response.code() == HttpURLConnection.HTTP_UNAUTHORIZED) {
response = chain.proceed(request);
}
}
}
return response;
}
private String buildAuthorizationHeader(Response response) throws IOException {
processChallenge("WWW-Authenticate", response.header("WWW-Authenticate"));
return generateDigestHeader(response);
}
private void processChallenge(String headerName, String headerValue) {
try {
digestScheme.processChallenge(new BasicHeader(headerName, headerValue));
} catch (MalformedChallengeException e) {
Log.e("AuthInterceptor", "Error processing header " + headerName + " for DIGEST authentication.");
e.printStackTrace();
}
}
private String generateDigestHeader(Response response) throws IOException {
HttpRequest request = new BasicHttpRequest(
response.request().method(),
response.request().uri().toString()
);
try {
return digestScheme.authenticate(credentials, request).getValue();
} catch (AuthenticationException e) {
Log.e("AuthInterceptor", "Error generating DIGEST auth header.");
e.printStackTrace();
return null;
}
}
}
Sometimes after the second OkHttp request the client returns a 400 or 401 response status code, then I process the request again and it works fine for me.
source to share