在代理后面使用 google-http-client 和 google-http-client-apache-v2 会产生 NonRepeatableRequestException

Using google-http-client and google-http-client-apache-v2 behind a proxy produces NonRepeatableRequestException

我正在使用 google-http-clientgoogle-http-client-apache-v2 库发出 POST 请求 在代理 之后

// 1.- Setting ssl and proxy
HttpClientBuilder builder = HttpClientBuilder.create();
            
SSLContext sslContext = SslUtils.getTlsSslContext();
SslUtils.initSslContext(sslContext, GoogleUtils.getCertificateTrustStore(), SslUtils.getPkixTrustManagerFactory());
builder.setSSLSocketFactory(new SSLConnectionSocketFactory(sslContext));
            
builder.setProxy(new HttpHost(host, port));
CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
credentialsProvider.setCredentials(new AuthScope(host, port), new UsernamePasswordCredentials(user, pass));
builder.setDefaultCredentialsProvider(credentialsProvider);

// 2.- Build request
HttpTransport httpTransport = new ApacheHttpTransport(builder.build());
HttpRequestFactory factory = httpTransport.createRequestFactory(credential);

HttpContent httpContent = new ByteArrayContent("application/json", "{}")
HttpRequest request = factory.buildRequest("POST", new GenericUrl(url), httpContent);

// 3.- Execute request
HttpResponse httpResponse = request.execute();

该请求产生 NonRepeatableRequestException:

org.apache.http.client.ClientProtocolException
    at org.apache.http.impl.client.InternalHttpClient.doExecute(InternalHttpClient.java:187) ~[httpclient-4.5.13.jar!/:4.5.13]
    at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:83) ~[httpclient-4.5.13.jar!/:4.5.13]
    at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:108) ~[httpclient-4.5.13.jar!/:4.5.13]
    at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:56) ~[httpclient-4.5.13.jar!/:4.5.13]
    at com.google.api.client.http.apache.v2.ApacheHttpRequest.execute(ApacheHttpRequest.java:73) ~[google-http-client-apache-v2-1.39.2.jar!/:?]
    at com.google.api.client.http.HttpRequest.execute(HttpRequest.java:1012) ~[google-http-client-1.39.2.jar!/:1.39.2]
    at 
    ...
Caused by: org.apache.http.client.NonRepeatableRequestException: Cannot retry request with a non-repeatable request entity.
    at org.apache.http.impl.execchain.MainClientExec.execute(MainClientExec.java:225) ~[httpclient-4.5.13.jar!/:4.5.13]
    at org.apache.http.impl.execchain.ProtocolExec.execute(ProtocolExec.java:186) ~[httpclient-4.5.13.jar!/:4.5.13]
    at org.apache.http.impl.execchain.RetryExec.execute(RetryExec.java:89) ~[httpclient-4.5.13.jar!/:4.5.13]
    at org.apache.http.impl.execchain.RedirectExec.execute(RedirectExec.java:110) ~[httpclient-4.5.13.jar!/:4.5.13]
    at org.apache.http.impl.client.InternalHttpClient.doExecute(InternalHttpClient.java:185) ~[httpclient-4.5.13.jar!/:4.5.13]
    at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:83) ~[httpclient-4.5.13.jar!/:4.5.13]
    at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:108) ~[httpclient-4.5.13.jar!/:4.5.13]
    at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:56) ~[httpclient-4.5.13.jar!/:4.5.13]
    at com.google.api.client.http.apache.v2.ApacheHttpRequest.execute(ApacheHttpRequest.java:73) ~[google-http-client-apache-v2-1.39.2.jar!/:?]
    at com.google.api.client.http.HttpRequest.execute(HttpRequest.java:1012) ~[google-http-client-1.39.2.jar!/:1.39.2]

        

似乎 ApacheHttpRequest 包装了 ByteArrayContent可重复(参见 JavaDoc) 在 ContentEntity不可重复.

在 google 库中调试执行,代理返回“407 需要代理身份验证”,然后它尝试重复请求(猜测包括凭据)并且出现异常是因为 [=69 使用了 ContentEntity =] 库不可重复。

是否有任何方法可以避免在第一次请求中与包含凭据的代理握手以避免实体的重用?

有什么方法可以告诉 google 使用可重复实体的库吗?

尝试使用以下库版本:

我在 github 上发布的解决方法以防对某人有帮助:

作为解决方法,我正在尝试将 ApacheHttpTransport 包装在 CustomApacheHttpTransport 中,将方法的结果委托给 ApacheHttpTransport 除了 buildRequest 方法。

buildRequest CustomApacheHttpTransport 中的方法构建类型为 [=29= 的自定义请求]CustomApacheHttpRequest.

public final class CustomApacheHttpTransport extends HttpTransport {
    
    private ApacheHttpTransport apacheHttpTransport;
    
    public CustomApacheHttpTransport (HttpClient httpClient) {
        this.apacheHttpTransport = new ApacheHttpTransport(httpClient);
    }
    
    @Override
    protected LowLevelHttpRequest buildRequest (String method, String url) {
        HttpRequestBase requestBase;
        if (method.equals("DELETE")) {
            requestBase = new HttpDelete(url);
        } else if (method.equals("GET")) {
            requestBase = new HttpGet(url);
        } else if (method.equals("HEAD")) {
            requestBase = new HttpHead(url);
        } else if (method.equals("PATCH")) {
            requestBase = new HttpPatch(url);
        } else if (method.equals("POST")) {
            ..
        }
        return new CustomApacheHttpRequest(apacheHttpTransport.getHttpClient(), requestBase);
    }
}

此自定义请求与 ApacheHttpRequest 相似,不同之处在于它在执行时会创建一个自定义实体 CustomContentEntity,这将是可重复的,具体取决于关于请求内容是否支持重试

final class CustomApacheHttpRequest extends LowLevelHttpRequest {
    
    private final HttpClient httpClient;
    private final HttpRequestBase request;
    private RequestConfig.Builder requestConfig;
    
    CustomApacheHttpRequest (HttpClient httpClient, HttpRequestBase request) {
        this.httpClient = httpClient;
        this.request = request;
        this.requestConfig = RequestConfig.custom().setRedirectsEnabled(false).setNormalizeUri(false).setStaleConnectionCheckEnabled(false);
    }
    
    ...
        
    @Override
    public LowLevelHttpResponse execute () throws IOException {
        if (this.getStreamingContent() != null) {
            Preconditions.checkState(request instanceof HttpEntityEnclosingRequest, "Apache HTTP client does not support %s requests with content.", request.getRequestLine().getMethod());
            
            CustomContentEntity entity = new CustomContentEntity(this.getContentLength(), this.getStreamingContent());
            entity.setContentEncoding(this.getContentEncoding());
            entity.setContentType(this.getContentType());
            if (this.getContentLength() == -1L) {
                entity.setChunked(true);
            }
            ((HttpEntityEnclosingRequest) request).setEntity(entity);
        }
        
        request.setConfig(requestConfig.build());
        return new CustomApacheHttpResponse(request, httpClient.execute(request));
    }
}

CustomContentEntity 中的关键是 isRepeatable 方法 returns总是 false 就像 ContentEntity 一样。

final class CustomContentEntity extends AbstractHttpEntity {
    
    private final long contentLength;
    private final StreamingContent streamingContent;
    
    CustomContentEntity (long contentLength, StreamingContent streamingContent) {
        this.contentLength = contentLength;
        this.streamingContent = streamingContent;
    }
    
    @Override
    public boolean isRepeatable () {
        return ((HttpContent) streamingContent).retrySupported();
    }
    ...
}

我还必须创建 CustomApacheHttpResponse 作为对 CustomApacheHttpRequest 的响应,因为 ApacheHttpResponse 是包-私有(CustomApacheHttpResponseApacheHttpResponse 完全相同)。

库 return 错误“您的请求不可重试”是正确的。它按预期工作。

POST 请求从根本上被视为 non-retryable,因为当服务器成功创建一个或多个资源时,它们最有可能 have a server store data. For example, a server is recommended to return 201 (Created) as a response。重试 POST 请求可能会导致多次插入、上传或发布数据。这就是为什么有时网络浏览器会显示以下提示以避免“重复信用卡交易”:

POST 的潜在重试逻辑应该在用户应用程序级别而不是库级别实现。

在您的情况下,错误的原因是您无权使用代理。因此,您需要在尝试使用代理之前先通过代理进行身份验证,然后发送(或re-send)一个POST请求。


更新评论中稍后提出的问题以及 GitHub issue

Why is the library who tries to repeat the request? (failling on a POST request).

这个问题看起来很奇怪,所以我不确定你在问什么。无论如何,该库旨在有意不重复 POST 的请求。对于 GET,这是另一回事。

Why the library have the same behaviour (retrying the request) with a GET request? (but in this case sucessfully because GET request do not have entity and do not matters if it is repeatable or not).

GET 本质上被认为是可重复的请求。请参见 this doc 示例以了解 GET 和 POST.

的区别的本质

GET requests are only used to request data (not modify)

POST is used to send data to a server to create/update a resource.

. GET POST
BACK button/Reload Harmless Data will be re-submitted (the browser should alert the user that the data are about to be re-submitted)

Why if I change the entity, as show in workaround, to make it repeatable, the POST request works successfully through the proxy for which you say I'm not authorized to use?

您通过使用 Apache API 对您的应用程序进行了编程,使其在应用程序级别失败时重复请求。没有什么能阻止您使用 Apache 库做任何您想做的事。当然,如果我们更改 Google 库来执行您正在尝试执行的操作,那么从技术上讲是可以让它以这种方式工作的。但是,我的意思是图书馆这样做是错误的。最后,auth 并不是真正相关的;这只是您可能遇到的众多失败中的一种。对于POST,几乎在所有情况下,无论遇到哪种错误,自动re-send请求都是没有意义的。

If as you say I'm not authorized to use the proxy:

您无权使用服务器进行初始请求。这就是您从代理服务器获得 407 Proxy Authentication Required 的原因。客户端很可能需要检查 returned Proxy-Authenticate 值并采取适当的操作来确定凭据。它需要采取什么行动取决于 header 的值,如文档中所述:

This status is sent with a Proxy-Authenticate header that contains information on how to authorize correctly.

您提供的凭据形式可能不是代理人期望的最终形式。通常,您的初始凭据用于获取服务器所需的最终形式的凭据。稍后一旦您获得它们,客户端将必须在后续请求中提供这些凭据。无论如何,事实是,服务器确实 return 407,说“我拒绝你的请求,因为需要身份验证。”


更新2

Apache HttpClient is retrying the request

当然可以。您手动对您的应用程序进行了编程,以允许 Apache HttpClient re-send 对 POST 的请求(这对您来说可能是一个可行的解决方法,但不应将其推广到其他情况)。

现在我明白了你的遗漏和错误的地方。当与需要身份验证的代理(或 non-proxy)交互时,通常您(无论是您还是 Apache 库)将 必须至少发出两次请求 。首先,您尝试不发送任何敏感信息(为什么要提前向不可信的人透露您的信息?即使您信任他们,您也根本不知道他们是否会需要您的信息。此外,即使所以,你不知道你应该如何正确地展示你的敏感信息)。第一个请求可能会(也可能不会)失败并出现类似“407 需要代理身份验证”的错误(人们称之为服务器正在“挑战”你),并且 基于服务器给你的挑战类型,您需要采取正确的措施为 第二个请求 准备身份验证 header。 Apache 库会为您做这件事。

despite I provide the credentials

您期望调用 .setDefaultCredentialsProvider() 会做什么?它不会按照您目前的想法进行操作。 Apache 库在第一个请求中对您的密码不做任何处理。正如我之前所说,最后,您需要在检查 Proxy-Authenticate 的值后提供服务器想要的正确形式的凭据,它告诉您应该如何 正确地 与服务器进行身份验证。这就是为什么您通常 必须重复请求 。如果您觉得这些听起来很陌生,请花点时间阅读 this introductory doc 以了解此 challenge-based HTTP 身份验证框架的工作原理。 (该文档指出,出于教育目的,它将仅使用“基本”方案进行解释,但请注意还有其他 non-basic 方案。)