web-dev-qa-db-fra.com

Retrofit POST request w/Basic HTTP Authentication: "Impossible de réessayer le corps HTTP en streaming"

J'utilise Retrofit pour faire une demande de base POST, et je fournis un @Body de base pour la demande.

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

Lorsque je construis l'interface pour Retrofit, je fournis mon propre OkHttpClient personnalisé, et tout ce que je fais, c'est d'ajouter ma propre authentification personnalisée:

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

Cela fonctionne très bien lorsque j'envoie des demandes directement avec OKHttp et d'autres demandes GET avec modification, mais lorsque j'utilise la modification pour effectuer une demande POST, l'erreur suivante s'affiche:

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)

J'ai joué avec ça. Si je supprime l'authentification et que je pointe vers un serveur qui ne nécessite pas l'authentification, cela fonctionne correctement. 

  1. Je dois donc envoyer les informations.
  2. Obtenir la demande de défi d'authentification.
  3. Répondre à la demande de contestation.
  4. Essayez de renvoyer la demande à nouveau, puis l'erreur est renvoyée.

Pas sûr de savoir comment contourner ça. Toute aide serait merveilleuse.

25
spierce7

Votre meilleur choix est de fournir vos informations d'identification à Retrofit via un RequestInterceptor au lieu de la variable OkAuthenticator de OkHttp. Cette interface fonctionne mieux lorsque la demande peut être réessayée, mais dans votre cas, nous avons déjà jeté le corps de publication au moment où nous découvrons que cela est nécessaire.

Vous pouvez continuer à utiliser la classe Credential d'OkAuthenticator qui peut coder votre nom d'utilisateur et votre mot de passe au format requis. Le nom d'en-tête que vous voulez est Authorization.

14
Jesse Wilson

Merci Jesse.

Juste au cas où cela aiderait, voici le code que j'ai créé pour l'authentification de base.

Tout d'abord, l'init dans la classe MyApplication:

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

Et puis la 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;
    }
}
39
Ferran Maylinch

Étendre la réponse de Naren:

Vous construisez auth String comme ceci:

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

Et puis vous passez basicAuth au service comme authorization.

@GET("/user") 
void getUser(@Header("Authorization") String authorization, Callback<User> callback)
14
Jacek Kwiecień

Pour une autorisation de base, vous pouvez fournir un en-tête du type:

@GET("/user")
void getUser(@Header("Authorization") String authorization, Callback<User> callback)
6
Naren

Si vous faites cela avec la dernière version de Retrofit/OkHttp, les solutions actuelles ne sont pas suffisantes. Retrofit n'offre plus un RequestInterceptor, vous devez donc utiliser les intercepteurs d'OkHttp pour accomplir une tâche similaire:

Créez votre intercepteur:

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

Vous devez ajouter l'intercepteur à votre client OkHttp:

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

Je n'ai pas testé le code ci-dessus, il est donc possible que de légères modifications soient nécessaires. Je programme dans Kotlin pour Android maintenant-a-days.

1
spierce7