微信公众号 API 推送公众号模板消息随机出现NoHttpResponseException: api.weixin.qq.com failed to respond 异常修复

  |   2 评论   |   7,155 浏览

RBA 的业务需要统一推送一些通知给运营. 为统一服务出口.所有的推送服务由用户中心完成. 一开始使用的时候还好好的.但是通过监控系统发现系统每天有零星的异常org.apache.http.NoHttpResponseException: api.weixin.qq.com failed to respond 此文用以记录解决此异常的过程和最终的方法.

现象还原

  • 使用工具包: apache HttpClient / DefaultHttpClient
  • 版本: 4.3.6
  • 工具类: org.apache.http.impl.client.DefaultHttpClient
  • 使用连接池: org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager

日志信息如下


url请求异常 请求url:https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=XXXX 请求异常:

org.apache.http.NoHttpResponseException: api.weixin.qq.com failed to respond
	at org.apache.http.impl.conn.DefaultHttpResponseParser.parseHead(DefaultHttpResponseParser.java:143)
	at org.apache.http.impl.conn.DefaultHttpResponseParser.parseHead(DefaultHttpResponseParser.java:57)
	at org.apache.http.impl.io.AbstractMessageParser.parse(AbstractMessageParser.java:260)
	at org.apache.http.impl.AbstractHttpClientConnection.receiveResponseHeader(AbstractHttpClientConnection.java:283)
	at org.apache.http.impl.conn.DefaultClientConnection.receiveResponseHeader(DefaultClientConnection.java:251)
	at org.apache.http.impl.conn.AbstractClientConnAdapter.receiveResponseHeader(AbstractClientConnAdapter.java:223)
	at org.apache.http.protocol.HttpRequestExecutor.doReceiveResponse(HttpRequestExecutor.java:271)
	at org.apache.http.protocol.HttpRequestExecutor.execute(HttpRequestExecutor.java:123)
	at org.apache.http.impl.client.DefaultRequestDirector.tryExecute(DefaultRequestDirector.java:685)
	at org.apache.http.impl.client.DefaultRequestDirector.execute(DefaultRequestDirector.java:487)
	at org.apache.http.impl.client.AbstractHttpClient.doExecute(AbstractHttpClient.java:863)
	at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:82)
	**************************************

说明:

通过查阅资料发现此类情况是在链接 server 端关闭,client 端不知情的情况下.使用了 Stale Connection 时会出现. 也就是说前提是使用了链接池的情况下. Httpclient 使用了过期的连接发起数据请求.

此异常在两个机房都会出现.一个机房量相当少(CN1),另外一个机房量相对多一些(CN2).从日志看.量级对比为1:20左右. 一开始认为 CN2的机房网络质量与硬件质量差一些会使网络质量没有机房1好.因此可能出现网络问题的可能性更大些. (后来通过监控发现 CN2实际的流量是 CN1的3~4倍,具体流量图如下), 现在流量更大.网络更差.因此出现偶发的网络问题的可能性也更大一些.
PUSH消息量对比.png
当时直观的想法是使用了HTTPCLIENT 链接池.从连接池中拿到连接后,使用此链接向微信服务器发起请求的时候由于没有检测连接的有效性(或者是检测的瞬间链接是有效的).导致在发起请求后,对方服务器早就已经关闭了链接.所以发起请求后,由于对方服务器已经关闭了链接,所以HttpClient 得到了NoHttpResponseException异常.
那这个问题怎么解决呢. 通过查阅资料,可以添加 Stale Connection check.下面 是一段引用自 stackoverflow(https://stackoverflow.com/questions/10558791/apache-httpclient-interim-error-nohttpresponseexception?answertab=votes )的话:

Most likely persistent connections that are kept alive by the connection manager become stale. That is, the target server shuts down the connection on its end without HttpClient being able to react to that event, while the connection is being idle, thus rendering the connection half-closed or 'stale'. Usually this is not a problem. HttpClient employs several techniques to verify connection validity upon its lease from the pool. Even if the stale connection check is disabled and a stale connection is used to transmit a request message the request execution usually fails in the write operation with SocketException and gets automatically retried. However under some circumstances the write operation can terminate without an exception and the subsequent read operation returns -1 (end of stream). In this case HttpClient has no other choice but to assume the request succeeded but the server failed to respond most likely due to an unexpected error on the server side.
The simplest way to remedy the situation is to evict expired connections and connections that have been idle longer than, say, 1 minute from the pool after a period of inactivity.

通过以上的建议,我应该启用一个周期性的线程来检查我的连接管理器里面的连接是否是Stale的;通过检查我的代码, 我使用的 Httpclient 的确是没有这个线程来做这个事情.但是通过调试我发现虽然我的工具类Httpclient没有周期性的线程来检查连接是否是可用的.但是在使用的版本里面是有一个默认的特性是开启的.

这里的这份代码是用来试验一个服务器的长链接特性的.因为我不知道微信的服务器是怎么构建的.所以无法得知微信的服务的长链接的特性是什么样的.

> 特别说明

这里用到的测试服务器是我直接连接的 tomcat 服务器进行测试的. tomcat 的 Connector 的默认超时时间配置的是20000ms,也就是20s. 这里特别需要说明的是,实际的生产环境的服务器会被比这个复杂得多.一般 Java 环境会使用如Nginx这类的反向代理软件来做负载均衡.这样的话,client 端连接 server 端的长链接特性实际是而对的如Nginx这样的反向代理服务器的长链接特性. 实际情况一般是: 反向代理 Server 来维持与 Client 端的长链接(如浏览器端),而被代理的服务端实际上多半使用的是短链接,具体可以参考:Nginx与Tomcat性能调优,前后端KeepAlive不同步引发的问题:https://blog.csdn.net/nimasike/article/details/81129163

String s = httpsClient
            .httpPost("http://10.2.1.1.1:38080/xxx.jsp", JSON.toJSONString(param), "utf-8");
        System.out.println(s);

        System.out.println("sleep start");
        Thread.sleep(parseLong("1000"));
        s = httpsClient
            .httpPost("http://10.2.1.1.1:38080/xxx.jsp", JSON.toJSONString(param), "utf-8");
        System.out.println(s);

        System.out.println("sleep start");
        Thread.sleep(parseLong("25000"));
        System.out.println("sleep end");

        s = httpsClient
            .httpPost("http://10.2.1.1.1:38080/xxx.jsp", JSON.toJSONString(param), "utf-8");
        System.out.println(s);
        System.out.println("*************************************************");

日志信息如下(为了突出重点,做了精简):

第一次请求:

ThreadSafeClientConnManager.getConnection(ThreadSafeClientConnManager.java:240)] Get connection: {}->http://10.2.1.1.1:38080, timeout = 500
ConnPoolByRoute.getEntryBlocking(ConnPoolByRoute.java:347)] [{}->http://10.2.1.1.1:38080] total kept alive: 0, total issued: 0, total allocated: 0 out of 
ConnPoolByRoute.getFreeEntry(ConnPoolByRoute.java:522)] No free connections [{}->http://10.2.1.1.1:38080][null]
ConnPoolByRoute.getEntryBlocking(ConnPoolByRoute.java:366)] Available capacity: 20 out of 20 [{}->http://10.2.1.1.1:38080][null]
ConnPoolByRoute.createEntry(ConnPoolByRoute.java:548)] Creating new connection [{}->http://10.2.1.1.1:38080]
tClientConnectionOperator.openConnection(DefaultClientConnectionOperator.java:177)] Connecting to 10.2.1.1.1:38080
RequestAddCookies.process(RequestAddCookies.java:123)] CookieSpec selected: ignoreCookies
RequestAuthCache.process(RequestAuthCache.java:77)] Auth cache not set in the context
RequestTargetAuthentication.process(RequestTargetAuthentication.java:80)] Target auth state: UNCHALLENGED
RequestProxyAuthentication.process(RequestProxyAuthentication.java:89)] Proxy auth state: UNCHALLENGED
tpsClient.tryExecute(DefaultRequestDirector.java:682)] Attempt 1 to execute request

******

ThreadSafeClientConnManager.releaseConnection(ThreadSafeClientConnManager.java:286)] Released connection is reusable.
ConnPoolByRoute.freeEntry(ConnPoolByRoute.java:431)] Releasing connection [{}->http://10.2.1.1.1:38080][null]
ConnPoolByRoute.freeEntry(ConnPoolByRoute.java:457)] Pooling connection [{}->http://10.2.1.1.1:38080][null]; keep alive for 30000 MILLISECONDS
ConnPoolByRoute.notifyWaitingThread(ConnPoolByRoute.java:678)] Notifying no-one, there are no waiting threads

第二次请求:

ThreadSafeClientConnManager.getConnection(ThreadSafeClientConnManager.java:240)] Get connection: {}->http://10.2.1.1.1:38080, timeout = 500
ConnPoolByRoute.getEntryBlocking(ConnPoolByRoute.java:347)] [{}->http://10.2.1.1.1:38080] total kept alive: 1, total issued: 0, total allocated: 1 out of 50
ConnPoolByRoute.getFreeEntry(ConnPoolByRoute.java:496)] Getting free connection [{}->http://10.2.1.1.1:38080][null]
tpsClient.execute(DefaultRequestDirector.java:431)] Stale connection check
RequestAddCookies.process(RequestAddCookies.java:123)] CookieSpec selected: ignoreCookies
RequestAuthCache.process(RequestAuthCache.java:77)] Auth cache not set in the context
RequestTargetAuthentication.process(RequestTargetAuthentication.java:80)] Target auth state: UNCHALLENGED
RequestProxyAuthentication.process(RequestProxyAuthentication.java:89)] Proxy auth state: UNCHALLENGED
tpsClient.tryExecute(DefaultRequestDirector.java:682)] Attempt 1 to execute request


er.releaseConnection(ThreadSafeClientConnManager.java:286)] Released connection is reusable.
(ConnPoolByRoute.java:431)] Releasing connection [{}->http://10.2.1.1.1:38080][null]
(ConnPoolByRoute.java:457)] Pooling connection [{}->http://10.2.1.1.1:38080][null]; keep alive for 30000 MILLISECONDS
tingThread(ConnPoolByRoute.java:678)] Notifying no-one, there are no waiting threads

第三次请求:

ThreadSafeClientConnManager.getConnection(ThreadSafeClientConnManager.java:240)] Get connection: {}->http://10.2.1.1.1:38080, timeout = 
ConnPoolByRoute.getEntryBlocking(ConnPoolByRoute.java:347)] [{}->http://10.2.1.1.1:38080] total kept alive: 1, total issued: 0, total al
ConnPoolByRoute.getFreeEntry(ConnPoolByRoute.java:496)] Getting free connection [{}->http://10.2.1.1.1:38080][null]
tpsClient.execute(DefaultRequestDirector.java:431)] Stale connection check
tpsClient.execute(DefaultRequestDirector.java:433)] Stale connection detected
tClientConnection.close(DefaultClientConnection.java:180)] Connection 0.0.0.0:49957<->10.2.1.1.1:38080 closed
tClientConnectionOperator.openConnection(DefaultClientConnectionOperator.java:177)] Connecting to 10.2.1.1.1:38080


releaseConnection(ThreadSafeClientConnManager.java:286)] Released connection is reusable.
nnPoolByRoute.java:431)] Releasing connection [{}->http://10.2.1.1.1:38080][null]
nnPoolByRoute.java:457)] Pooling connection [{}->http://10.2.1.1.1:38080][null]; keep alive for 30000 MILLISECONDS
gThread(ConnPoolByRoute.java:678)] Notifying no-one, there are no waiting threads

说明:
第一次请求.刚刚建立新的连接,因此连接池里面没有连接.所以会直接创建新的连接.连接使用完成后,连接并没有被释放,因为其是 reusable 的Released connection is reusable.,因此连接被放入到了连接池
第二次请求直接从连接池拿了连接,这里重点:这里取到了连接,并且做了一次连接是否有效的检查.这里检查是通过了的.因为间隔了一秒,目标 server 的长链接的 kepp-alive 时间未过期.所以直接重用了刚刚被放入连接池的链接.用完后同样的放入到了连接池:
第三次:再次从连接池拿连接的时候,在做连接有效性检查时, 发现连接已经失效.此时会重新释放掉连接创建新的连接

// org.apache.http.impl.client.DefaultRequestDirector#execute
// 4.5.3/httpclient-4.5.3-sources.jar!/org/apache/http/impl/client/DefaultRequestDirector.java:338
public HttpResponse execute(final HttpHost targetHost, final HttpRequest request,
                                final HttpContext context)
        throws HttpException, IOException {

        context.setAttribute(ClientContext.TARGET_AUTH_STATE, targetAuthState);
        context.setAttribute(ClientContext.PROXY_AUTH_STATE, proxyAuthState);

        HttpHost target = targetHost;

        final HttpRequest orig = request;
        final RequestWrapper origWrapper = wrapRequest(orig);
        origWrapper.setParams(params);
        final HttpRoute origRoute = determineRoute(target, origWrapper, context);

        virtualHost = (HttpHost) origWrapper.getParams().getParameter(ClientPNames.VIRTUAL_HOST);

        // HTTPCLIENT-1092 - add the port if necessary
        if (virtualHost != null && virtualHost.getPort() == -1) {
            final HttpHost host = (target != null) ? target : origRoute.getTargetHost();
            final int port = host.getPort();
            if (port != -1){
                virtualHost = new HttpHost(virtualHost.getHostName(), port, virtualHost.getSchemeName());
            }
        }

        RoutedRequest roureq = new RoutedRequest(origWrapper, origRoute);

        boolean reuse = false;
        boolean done = false;
        try {
            HttpResponse response = null;
            while (!done) {
                // In this loop, the RoutedRequest may be replaced by a
                // followup request and route. The request and route passed
                // in the method arguments will be replaced. The original
                // request is still available in 'orig'.

                final RequestWrapper wrapper = roureq.getRequest();
                final HttpRoute route = roureq.getRoute();
                response = null;

                // See if we have a user token bound to the execution context
                Object userToken = context.getAttribute(ClientContext.USER_TOKEN);

                // Allocate connection if needed
                if (managedConn == null) {
                    final ClientConnectionRequest connRequest = connManager.requestConnection(
                            route, userToken);
                    if (orig instanceof AbortableHttpRequest) {
                        ((AbortableHttpRequest) orig).setConnectionRequest(connRequest);
                    }

                    final long timeout = HttpClientParams.getConnectionManagerTimeout(params);
                    try {
                        managedConn = connRequest.getConnection(timeout, TimeUnit.MILLISECONDS);
                    } catch(final InterruptedException interrupted) {
                        Thread.currentThread().interrupt();
                        throw new InterruptedIOException();
                    }

                    if (HttpConnectionParams.isStaleCheckingEnabled(params)) {
                        // validate connection
                        if (managedConn.isOpen()) {
                           /*  这里是做 Stale Connection 检查 **/
                            this.log.debug("Stale connection check");
                            if (managedConn.isStale()) {
                                this.log.debug("Stale connection detected");
                                managedConn.close();
                            }
                        }
                    }
                }

                /**********************************************/
                // some other code 
               /**********************************************/

    } // execute

以上是连接有效性的一个检查代码片断. 从代码看出,使用的代码是默认会有Stale Connection检查的.从连接池里面拿链接时都会进行有效性检查的.同时在进行有效性检查前会进行过期连接检查.如下在的日志,是 sleep 30秒后再请求此 url 得到的日志.

ThreadSafeClientConnManager.getConnection(ThreadSafeClientConnManager.java:240)] Get connection: {}->http://10.2.1.1.1:38080, timeout = 500
ConnPoolByRoute.getEntryBlocking(ConnPoolByRoute.java:347)] [{}->http://10.2.1.1.1:38080] total kept alive: 1, total issued: 0, total allocated: 1 out of 50
ConnPoolByRoute.getFreeEntry(ConnPoolByRoute.java:496)] Getting free connection [{}->http://10.2.1.1.1:38080][null]
ConnPoolByRoute.getFreeEntry(ConnPoolByRoute.java:505)] Closing expired free connection [{}->http://10.2.1.1.1:38080][null]
tClientConnection.close(DefaultClientConnection.java:180)] Connection 0.0.0.0:61059<->10.2.1.1.1:38080 closed

以上说明:
HttpClient 本身是有无效链接检测的.同时是有两道检测.一个是过期检测.一个是有效性探测.所以在这看来.无论怎样的情况下都会事先检测连接的有效性.但是为何还是会出现NoHttpResponseException呢.那只有一种可能就是:在做Stale Connection Check的时候,连接是有效的.但是在使用的过程中连接被 Server 端关闭了.同时一个连接的保持空闲时间超过了30s.那就会被 Httpclient 检测到过期连接. 因此只能可能是长链接被微信服务器在不到30秒的空闲时间内被微信服务器给关闭了,在 HttpClient 使用连接池中的连接进行检测时,连接是好的,但是在 post 数据的时候连接就被关闭了. 从这一点来说,也可以解释只出现了零星的异常.(每分钟有1-4个推送请求.一在有24 x 60 x 2,约3000请求/天,但是出现异常一天0-20次.约1%的请求会有此异常). 这里的HttpClient的默认长链接被强制设置为了30s 的保持时间.这个在查阅了不同的开源代码后,发现不同的软件基本都会使用这个长链接的保持时间.长链接的可保持时间一般在连接的访问过程中进行协商出来的.整个过程如下:

  1. client 端发起链接. 请求中添加了 Header Connection: Keep-Alive.表示client 端支持长链接.如果使用HTTP 1.1 版本时,可以不包含此 Header.默认被理解为长链接.
  2. 如果长链接要指定一个可保持的超时时间(或者是最长保持时间).则可以在 Header 中添加:Keep-Alive: timeount=50,这个是50s 的超时时间.如果不指定则认为此长链接可以长时间保持.即长期有效.这个是 client 端向 server 端传递客户端能够接受的长链接参数.
  3. server 在接收到请求时,根据自己的实际情况和 client 的实际情况.返回如上所说的Connection: Keep-Alive 此类的 Header.返回给 Client 端.此时 Client 端也可以根据 Server 端返回的 Header 的参数来保持这个长链接.比如 Server 端说,我只能保持20s 的长链接.就把 timeout 放到 header 中.但是.....

问题就在于,所有的长连接在大多数情况下都不会传递超时时间.此时客户端可以根据自己的实际情况来保持长链接. 有的是10s 的有的20s,有的是30s. 这里大部分的处理代码都把长链接的保持时间设定为了30s.

微信服务器的长链接的保持时间是多少呢? 20s,如果超过了20s 就会被自动释放. 可以用以上的实验方法.把 sleep 的时间设定从1
s 到20s 的间隔不停的测试.最后发现在 sleep 20s 的时候.可以复现 Server 端的错误.

问题复述:

当一个连接在第一次使用完成后.会被放到连接池中.

  • 如果下一次使用不超过20s.则连接在微信 server 端不会被释放.此时在 client 端不管是过期检测和 stale check 都是通过了的.
  • 如果是超过了20s-30s 内, 则在 client 端的过期检测是pass 的.但是 stale check 的时候链接会被检测出来为无效连接.会被 close.
  • 如果是超过了30s. 则会被检测到过期.因此也会被直接 close
  • 如果是刚好20s,则有可能 stale check 的时候连接有效,而在使用过程中连接被微信 Server 端释放

修复办法:

  1. 把微信相关的服务器的 keep-alive的服务器参数调整为小于20s.则可以规避此问题.
  2. 同时也可以升级 httpclient 的使用方法.使用 HttpClientBuilder 来完成请求.此方法会有 监控线程检测

处理后的 push 监控图如下,基本上没有此类的错误出现.

修复后异常数据对比.png

最后小结:

分析了一大堆的日志.看了无数的网页. 最后的解决方案是相当的简单.就是把 keep-alive 的时间修改为20s 以内.同时,也用自己的办法分析出了 微信服务器的Keep-Alive的时间即就是20s.

其它:

上面的 stackoverflow 的引文中,提到了返回-1的问题.相关日志如下,这个日志是在更新为 keep-alive 时间为15s 后出现的.也就是说,其实还是有可能会出现临界状态(此临界状态是由于在特殊情况下微信服务端在20s 前关闭了链接,具体原因可能是服务部署,也有可能是网络不稳定原因.)

# 此日志是使用新
[org.apache.http.client.protocol.RequestAddCookies.process(RequestAddCookies.java:123)] CookieSpec selected: ignoreCookies
[org.apache.http.client.protocol.RequestAuthCache.process(RequestAuthCache.java:77)] Auth cache not set in the context
[org.apache.http.impl.conn.PoolingHttpClientConnectionManager.requestConnection(PoolingHttpClientConnectionManager.java:255)] Connection request: [route: {s}->https://api.weixin.qq.com:443][total kept alive: 1; route allocated: 1 of 50; total allocated: 1 of 200]
[org.apache.http.wire.wire(Wire.java:87)] http-outgoing-5655 << "[read] I/O error: Read timed out"
[org.apache.http.impl.conn.PoolingHttpClientConnectionManager.leaseConnection(PoolingHttpClientConnectionManager.java:288)] Connection leased: [id: 5655][route: {s}->https://api.weixin.qq.com:443][total kept alive: 0; route allocated: 1 of 50; total allocated: 1 of 200]
[org.apache.http.impl.conn.DefaultManagedHttpClientConnection.setSocketTimeout(LoggingManagedHttpClientConnection.java:88)] http-outgoing-5655: set socket timeout to 30000
[org.apache.http.impl.execchain.MainClientExec.execute(MainClientExec.java:256)] Executing request POST /cgi-bin/message/template/send?access_token=token_value HTTP/1.1
[org.apache.http.impl.execchain.MainClientExec.execute(MainClientExec.java:261)] Target auth state: UNCHALLENGED
[org.apache.http.impl.execchain.MainClientExec.execute(MainClientExec.java:267)] Proxy auth state: UNCHALLENGED
[org.apache.http.headers.onRequestSubmitted(LoggingManagedHttpClientConnection.java:133)] http-outgoing-5655 >> POST /cgi-bin/message/template/send?access_token=$token_value HTTP/1.1
[org.apache.http.headers.onRequestSubmitted(LoggingManagedHttpClientConnection.java:136)] http-outgoing-5655 >> Content-Length: 544
[org.apache.http.headers.onRequestSubmitted(LoggingManagedHttpClientConnection.java:136)] http-outgoing-5655 >> Content-Type: application/json
[org.apache.http.headers.onRequestSubmitted(LoggingManagedHttpClientConnection.java:136)] http-outgoing-5655 >> Host: api.weixin.qq.com
[org.apache.http.headers.onRequestSubmitted(LoggingManagedHttpClientConnection.java:136)] http-outgoing-5655 >> Connection: Keep-Alive
[org.apache.http.headers.onRequestSubmitted(LoggingManagedHttpClientConnection.java:136)] http-outgoing-5655 >> User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.204 Safari/534.16

[org.apache.http.headers.onRequestSubmitted(LoggingManagedHttpClientConnection.java:136)] http-outgoing-5655 >> Accept-Encoding: gzip,deflate
[org.apache.http.wire.wire(Wire.java:73)] http-outgoing-5655 >> "POST /cgi-bin/message/template/send?access_token=$access_token_value


[org.apache.http.wire.wire(Wire.java:73)] http-outgoing-5655 >> "Content-Length: 544[\r][\n]"
[org.apache.http.wire.wire(Wire.java:73)] http-outgoing-5655 >> "Content-Type: application/json[\r][\n]"
[org.apache.http.wire.wire(Wire.java:73)] http-outgoing-5655 >> "Host: api.weixin.qq.com[\r][\n]"
[org.apache.http.wire.wire(Wire.java:73)] http-outgoing-5655 >> "Connection: Keep-Alive[\r][\n]"
[org.apache.http.wire.wire(Wire.java:73)] http-outgoing-5655 >> "User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.1

[org.apache.http.wire.wire(Wire.java:73)] http-outgoing-5655 >> "Accept-Encoding: gzip,deflate[\r][\n]"
[org.apache.http.wire.wire(Wire.java:73)] http-outgoing-5655 >> "[\r][\n]"
[org.apache.http.wire.wire(Wire.java:73)] http-outgoing-5655 >> "{[\n]"
[org.apache.http.wire.wire(Wire.java:73)] http-outgoing-5655 >> ""touser":"888888888888888888",[\n]"
[org.apache.http.wire.wire(Wire.java:73)] http-outgoing-5655 >> ""template_id":"6666666666666666666666666666666",[\n]"
[org.apache.http.wire.wire(Wire.java:73)] http-outgoing-5655 >> ""url":"https://open.weixin.qq.com/connect/oauth2/authorize?appid=xxxxxx"
****
[org.apache.http.wire.wire(Wire.java:87)] http-outgoing-5655 >> "}"
[org.apache.http.wire.wire(Wire.java:87)] http-outgoing-5655 << "end of stream" # 注意这个日志

评论

发表评论


取消