Retrofit POST request w/ Basic HTTP Authentication: "Cannot retry streamed HTTP body"
Asked Answered
U

5

25

I'm using Retrofit to do a basic POST request, and I'm providing a basic @Body for the request.

@POST("/rest/v1/auth/login")
LoginResponse login(@Body LoginRequest loginRequest);

When I'm building the interface for Retrofit I'm providing my own custom OkHttpClient, and all that I'm doing to it is adding my own custom authentication:

    @Provides
    @Singleton
    public Client providesClient() {
        OkHttpClient httpClient = new OkHttpClient();

        httpClient.setAuthenticator(new OkAuthenticator() {
            @Override
            public Credential authenticate(Proxy proxy, URL url, List<Challenge> challenges) throws IOException {
                return getCredential();
            }

            @Override
            public Credential authenticateProxy(Proxy proxy, URL url, List<Challenge> challenges) throws IOException {
                return getCredential();
            }
        });

        return new OkClient(httpClient);
    }

This works great when I'm sending requests directly with OKHttp, and other GET requests with retrofit but when I use retrofit to do a POST request I get the following error:

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)

I've played around with it. If I remove the authentication, and point to a server that doesn't require the authentication, then it works fine.

  1. So I must be sending the information.
  2. Getting the Authentication challenge request.
  3. Responding to the challenge request.
  4. Trying to resend the request again, and then the error is being thrown.

Not sure how to get around this. Any help would be wonderful.

Unlimber answered 26/1, 2014 at 23:23 Comment(0)
K
14

Your best bet is to provide your credentials to Retrofit via a RequestInterceptor instead of OkHttp's OkAuthenticator. That interface works best when the request can be retried, but in your case we've already thrown out the post body by the time we find out that's necessary.

You can continue to use OkAuthenticator's Credential class which can encode your username and password in the required format. The header name you want is Authorization.

Knut answered 26/1, 2014 at 23:46 Comment(5)
Worked like a charm. Thanks a bunch. I've been banging my head against the wall on this one for a few hours. :-)Unlimber
@Jesse Wilson (or is it @jesse-wilson) I'm having this same problem in OkHttp. Do you have a recommendation there as well if I am not using Retrofit? ThanksTuxedo
@Tuxedo you can manually retry the request when there's an authentication problem.Knut
@JesseWilson your link is brokenOndometer
Yep. The advice only works for Retrofit 1.x. In Retrofit 2.x you'll need an OkHttp interceptor. github.com/square/okhttp/wiki/InterceptorsKnut
B
39

Thanks, Jesse.

Just in case it helps, here is the code I did for Basic auth.

First, the init in MyApplication class:

ApiRequestInterceptor requestInterceptor = new ApiRequestInterceptor();
requestInterceptor.setUser(user); // I pass the user from my model

ApiService apiService = new RestAdapter.Builder()
            .setRequestInterceptor(requestInterceptor)
            .setServer(Constants.API_BASE_URL)
            .setClient(new OkClient()) // The default client didn't handle well responses like 401
            .build()
            .create(ApiService.class);

And then the ApiRequestInterceptor:

import android.util.Base64;
import retrofit.RequestInterceptor;

/**
 * Interceptor used to authorize requests.
 */
public class ApiRequestInterceptor implements RequestInterceptor {

    private User user;

    @Override
    public void intercept(RequestFacade requestFacade) {

        if (user != null) {
            final String authorizationValue = encodeCredentialsForBasicAuthorization();
            requestFacade.addHeader("Authorization", authorizationValue);
        }
    }

    private String encodeCredentialsForBasicAuthorization() {
        final String userAndPassword = user.getUsername() + ":" + user.getPassword();
        return "Basic " + Base64.encodeToString(userAndPassword.getBytes(), Base64.NO_WRAP);
    }

    public User getUser() {
        return user;
    }

    public void setUser(User user) {
        this.user = user;
    }
}
Beatrizbeattie answered 17/2, 2014 at 11:16 Comment(2)
What is class ApiService?Cantharides
ApiService is the interface for my API. You would implement your interface depending on your desired REST API. Likewise User is the model class for our users. You would create your own.Beatrizbeattie
M
15

Extending Naren's answer:

You build auth String like this:

String basicAuth = "Basic " + Base64.encodeToString(String.format("%s:%s", "your_user_name", "your_password").getBytes(), Base64.NO_WRAP);

And then you pass basicAuth to service as authorization.

@GET("/user") 
void getUser(@Header("Authorization") String authorization, Callback<User> callback)
Magi answered 18/12, 2014 at 11:5 Comment(2)
If you need to add the same credentials to all outgoing requests, it's not very nice to have to add @Header("Authorization") all over your Retrofit interfaces, is it?Augustus
The below methiod can be used to generate basicAuth easily. var basicAuth = Credentials.basic("YOUR_USERNAME", "YOUR_PASSWORD")Advection
K
14

Your best bet is to provide your credentials to Retrofit via a RequestInterceptor instead of OkHttp's OkAuthenticator. That interface works best when the request can be retried, but in your case we've already thrown out the post body by the time we find out that's necessary.

You can continue to use OkAuthenticator's Credential class which can encode your username and password in the required format. The header name you want is Authorization.

Knut answered 26/1, 2014 at 23:46 Comment(5)
Worked like a charm. Thanks a bunch. I've been banging my head against the wall on this one for a few hours. :-)Unlimber
@Jesse Wilson (or is it @jesse-wilson) I'm having this same problem in OkHttp. Do you have a recommendation there as well if I am not using Retrofit? ThanksTuxedo
@Tuxedo you can manually retry the request when there's an authentication problem.Knut
@JesseWilson your link is brokenOndometer
Yep. The advice only works for Retrofit 1.x. In Retrofit 2.x you'll need an OkHttp interceptor. github.com/square/okhttp/wiki/InterceptorsKnut
N
6

For basic authorization you can provide a header like:

@GET("/user")
void getUser(@Header("Authorization") String authorization, Callback<User> callback)
Nola answered 23/9, 2014 at 22:45 Comment(0)
U
1

If you are doing this with the latest version of Retrofit / OkHttp, the current set of solutions aren't sufficient. Retrofit no longer offers a RequestInterceptor, so you need to use OkHttp's interceptors to accomplish a similar task:

Create your interceptor:

public class HttpAuthInterceptor implements Interceptor {
  private String httpUsername;
  private String httpPassword;

  public HttpAuthInterceptor(String httpUsername, String httpPassword) {
    this.httpUsername = httpUsername;
    this.httpPassword = httpPassword;
  }

  @Override public Response intercept(Chain chain) throws IOException {
    Request newRequest = chain.request().newBuilder()
        .addHeader("Authorization", getAuthorizationValue())
        .build();

    return chain.proceed(newRequest);
  }

  private String getAuthorizationValue() {
    final String userAndPassword = "httpUsername" + ":" + httpPassword;
    return "Basic " + Base64.encodeToString(userAndPassword.getBytes(), Base64.NO_WRAP);
  }
}

You'd need to add the interceptor to your OkHttp Client:

// Create your client
OkHttpClient client = new OkHttpClient.Builder()
    .addInterceptor(new HttpAuthInterceptor("httpUsername", "httpPassword"))
    .build();

// Build Retrofit with your client
Retrofit retrofit = new Retrofit.Builder()
    .client(client)
    .build();

// Create and use your service that now authenticates each request.
YourRetrofitService service = retrofit.create(YourRetrofitService.class);

I didn't test the above code, so some slight modifications may need to be made. I program in Kotlin for Android now-a-days.

Unlimber answered 12/12, 2016 at 16:4 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.