0

我正在使用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中。

在此处输入图像描述

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

有什么方法可以避免与代理握手,包括第一次请求中的凭据以避免实体的重用?

有没有办法告诉谷歌图书馆使用可重复实体?

尝试使用以下库版本:

  • google-api-client-1.31.5
  • 谷歌-http-client-jackson2-1.39.2
  • 谷歌-oauth-client-1.31.5
  • google-http-client-apache-v2-1.39.2
  • 谷歌-http-client-1.39.2
  • httpclient-4.5.13
  • httpcore-4.4.14
4

2 回答 2

0

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

POST 请求基本上被认为是不可重试的,因为它们最有可能让服务器存储数据。例如,当服务器成功创建一个或多个资源时,建议服务器返回 201 (Created) 作为响应。重试 POST 请求可能会导致多次插入、上传或发布数据。这就是为什么有时网络浏览器会显示以下提示以避免“重复信用卡交易”:

图片

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

在您的情况下,错误的原因是您无权使用代理。因此,您需要先通过代理进行身份验证,然后再尝试使用它,然后发送(或重新发送)POST 请求。


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

为什么试图重复请求的图书馆?(在 POST 请求上失败)。

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

为什么库具有与 GET 请求相同的行为(重试请求)?(但在这种情况下是成功的,因为 GET 请求没有实体,并且它是否可重复并不重要)。

GET 本质上被认为是可重复的请求。例如,请参阅此文档以了解 GET 和 POST 差异的本质。

GET 请求仅用于请求数据(不修改)

POST 用于将数据发送到服务器以创建/更新资源。

. 得到 邮政
BACK 按钮/重新加载 无害 数据将被重新提交(浏览器应该提醒用户数据即将被重新提交)

为什么如果我更改实体(如解决方法中所示)以使其可重复,POST 请求会通过您说我无权使用的代理成功运行?

您通过使用 Apache API 对应用程序进行了编程,使其在应用程序级别失败时重复请求。没有什么能阻止您使用 Apache 库实现您想要的任何东西。当然,如果我们改变谷歌库来做你想做的事,在技术上是有可能让它以这种方式工作的。但是,我要说的是图书馆这样做是错误的。最后,auth 并不真正相关。这只是您可能遇到的许多故障中的一种。对于 POST,几乎在所有情况下,无论遇到哪种错误,自动重新发送请求都是没有意义的。

如果如您所说,我无权使用代理:

您无权使用服务器进行初始请求。这就是您407 Proxy Authentication Required从代理服务器获取信息的原因。客户端很可能需要检查返回的Proxy-Authenticate值并采取适当的措施来找出凭据。它需要采取什么行动取决于标头的值,如文档中所述:

此状态与Proxy-Authenticate包含有关如何正确授权的信息的标头一起发送。

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


更新2

Apache HttpClient 正在重试请求

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

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

尽管我提供了凭据

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

于 2021-11-22T20:24:11.923 回答
-2

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

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

CustomApacheHttpTransport中的这个buildRequest方法构建了一个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方法,它不会像ContentEntity那样总是返回false

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完全相同)。

于 2021-07-07T07:05:42.637 回答