HttpURLConnection自动重试机制导致请求重复两次

HttpURLConnection自动重试机制导致请求重复两次

对接某第三方聚合支付的反扫支付时,对方返回单号重复,导致收银失败。在业务层确认外部单号没有重复以及没有做失败重试处理后,怀疑是http客户端存在自动重试机制,于是往这个方面查可以确认是HttpURLConnection自动进行了请求重试。

HttpURLConnection 采用 Sun 私有的一个 HTTP 协议实现类: HttpClient.java
关键是下面这段发送请求、解析响应头的方法:

569       /** Parse the first line of the HTTP request.  It usually looks
570           something like: "HTTP/1.0 <number> comment\r\n". */
571   
572       public boolean parseHTTP(MessageHeader responses, ProgressSource pi, HttpURLConnection httpuc)
573       throws IOException {
574           /* If "HTTP/*" is found in the beginning, return true.  Let
575            * HttpURLConnection parse the mime header itself.
576            *
577            * If this isn't valid HTTP, then we don't try to parse a header
578            * out of the beginning of the response into the responses,
579            * and instead just queue up the output stream to it's very beginning.
580            * This seems most reasonable, and is what the NN browser does.
581            */
582   
583           try {
584               serverInput = serverSocket.getInputStream();
585               if (capture != null) {
586                   serverInput = new HttpCaptureInputStream(serverInput, capture);
587               }
588               serverInput = new BufferedInputStream(serverInput);
589               return (parseHTTPHeader(responses, pi, httpuc));
590           } catch (SocketTimeoutException stex) {
591               // We don't want to retry the request when the app. sets a timeout
592               // but don't close the server if timeout while waiting for 100-continue
593               if (ignoreContinue) {
594                   closeServer();
595               }
596               throw stex;
597           } catch (IOException e) {
598               closeServer();
599               cachedHttpClient = false;
600               if (!failedOnce && requests != null) {
601                   failedOnce = true;
602                   if (httpuc.getRequestMethod().equals("POST") && (!retryPostProp || streaming)) {
603                       // do not retry the request
604                   }  else {
605                       // try once more
606                       openServer();
607                       if (needsTunneling()) {
608                           httpuc.doTunneling();
609                       }
610                       afterConnect();
611                       writeRequests(requests, poster);
612                       return parseHTTP(responses, pi, httpuc);
613                   }
614               }
615               throw e;
616           }
617   
618       }

在第 600 – 614 行的代码里:

failedOnce 默认是 false,表示是否已经失败过一次了。这也就限制了最多发送 2 次请求。
httpuc 是请求相关的信息。
retryPostProp 默认是 true,可以通过命令行参数(-Dsun.net.http.retryPost=false)来指定值。
streaming:默认 false。 true if we are in streaming mode (fixed length or chunked) 。

通过 Linux 的命令 socat tcp4-listen:8080,fork,reuseaddr system:“sleep 1”!!stdout 建立一个只接收请求、不返回响应的 HTTP 服务器。
对于 POST 请求,第一次请求发送出去后解析响应会碰到流提前结束,这是个 SocketException: Unexpected end of file from server,parseHTTP 捕获后发现满足上面的条件就会进行重试。服务端就会收到第二个请求。

解决方案
1、禁用 HttpURLConnection 的重试机制。
通过启动程序命令参数 -Dsun.net.http.retryPost=false
或代码设置 System.setProperty(“sun.net.http.retryPost”, “false”)

2、使用 Apache HttpComponents 库。
默认的, HttpClient 尝试自动从 I/O 异常恢复。这种自动恢复机制仅限于一些被认为是安全的异常。

  • HttpClient 不会尝试从任何逻辑或 HTTP 协议错误恢复;
  • HttpClient 会自动重试那些被认为是幂等的方法;
  • HttpClient 会自动重试那些仍在发送 HTTP 请求到目标服务器时出错的方法。(例如,请求还没有完整传输到服务器)。

关于java.net.SocketException: Unexpected end of file from server
这个异常说明数据已经发送成功。有可能服务端是防火墙的原因,没有处理客户端发来的数据。也有可能是客户端的数据不符合要求,服务端没有做出响应。数据不符合要求可能是传送的数据含有奇怪的字符。也有可能是两边编码不一致导致。客户端将字符串变成字节数据传送时需要指定编码格式,如str.getBytes(“UTF-8”);如不指定,可能导致上面的错误。另外有人说当URL过长时也会发生此错误,当使用URL发送数据时,可以参考此意见。

参考资料
https://stackoverflow.com/questions/24417817/stopping-silent-retries-in-httpurlconnection
https://coderbee.net/index.php/java/20170829/1541

  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值