背景
这是个大坑,耗费了我极多的时间。
事情呢,是这样的。最近几天做了一个微信里的潜入页,用于注册账户的。注册很简单,输入手机号-验证短信验证码-填一点资料-注册成功。
作为一个单页面操作,所有请求都是通过AJAX和服务器交互的,这思路很常规。唯一的特点是,最后一步超长。
超长的原因是:创建账户需要创建几百张表,还有无数初始化操作,所以乐观估计需要至少八秒钟才会成功。
也许你会问为什么会这么慢,要创建这么多表呢?
原谅我不想说,因为与本文无关。
然则这个创建其实是有步骤的,第一步就是把用户的邮箱给占位:创建为最基本的信息,不允许重复创建。
OK,背景说完。
直到上线
开发,测试,包括内测都是很完美的,没有任何问题。
但上线后,突然有测试提出了这么一个问题:微信里注册的时候,任何一个邮箱都会提示邮箱已使用。
面对这样一个错误且不管怎么换邮箱都同样的错误,所有人的表情都是懵逼的。
内心OS:这特么什么鬼。
后端(Java,没错,语法和性能巨烂的Java):这是个很低级的BUG,很明显是服务器首次返回错误信息后乃们直接缓存了记录信息,所以后面其实都没验证。
我还没去反驳,测试直接打脸。
测试:我第一次输入就这样。
Java:……
Java同学陷入了沉思。
反正我带着『不是自己问题』的乐观心态,坐在旁边看他们三脸懵逼。
后来Java同学很不乐意地回去看了下数据库,发现邮箱确实存在了。
Java同学一拍大腿,说哎呀你们这群测试都是猪呀,这明显存在嘛!
测试的同学一脸懵逼,极不情愿地又用了一个小号邮箱重新注册,一样的错误。
Java同学一查数据库,“行不行啊你们,这邮箱还是有啊”。
测试同学一脸茫然,说我准备重新申请个QQ,你丫别吵吵……你看看XXX这个邮箱存不存在。
Java:这个邮箱不存在。
测试同学低头鼓捣了一下,说,“一样的错误”。
Java同学低头查了一下,哎哟卧槽,怎么数据库有记录了,不是返回已存在了么。
然后所有人都沉默了一下,最后他们把头一起转向我,表情是这样的。
他们:一定是你重复发请求却只拿最后一次请求当结果了。
我直接甩锅:别看我,关我毛事,所有浏览器都测试过,按钮在发请求的时候都禁用了不可能重复点击,发请求状态都有标记不可能重复发,怎么可能发两次,你们别闹了,证据呢就乱说话。
Java同学回去给代码加上Log输出,然后一边测一边看输出。
然后他们看到了对我很不利的结果:
“木鱼你看,你就是发出了两次请求嘛!”
我看了看记录,特么的果然前后差了两秒钟,有两次开始执行记录的请求……
我……
漫长地找原因
我极不情愿地回来找前端的问题。
然而我不管怎么看代码,怎么调试,怎么用自己的微信测试,不管是安卓(小米)还是iPhone5S,都测不出半点问题。从程序逻辑上看也根本没有发两次请求的机会。
好郁闷……用了MVVM框架,难道这框架有问题??
然后我就开始怀疑是服务器那边的问题。Java的后端是跑在Tomcat上的,前面还有个Nginx做反代。于是我到运维那边找Ngxin的访问日志看了一下。
查看了前后关联的请求,确实有重复的,但是客户端IP都不一样,时间也不一样,而同一个IP的请求前后都是一个序列不存在重复。
所以根据Nginx的日志,很明显客户端没有重复请求啊,要是重复请求了,应该看到同一个IP的请求会重复出现才对。那么Java那边的重复记录怎么回事,难道Java这边自己调用了两次??
看着Java同学那么天真的脸,我觉得把这个锅就这样甩给他们是莫大的伤害。
于是我本着“有则改进无则加勉”的出发点,决定改前端代码。
第一次尝试,在进入ajax方法之前,加一个bool变量标记,进入时加标记,完成后清除标记,进入之前判断是否已标记,如果已标记则直接退出。
完全没改进,依然同样的错误。
第二次尝试,将Ajax方法改成同步的,我直接阻止你浏览器操作,不能重复操作总不会因为DOM事件重复发了吧?
完全没改进,依然同样的错误。
第三次尝试,挂载全局的Ajax钩子,在ajax完成后打印返回结果到dom里。
打印信息没重复,表明只收到了一个结果,那就是邮箱已存在。
第四次尝试,直接上jQuery的AjaxFilter,将并行的重复ajax请求,直接截断。
完全没结果。
第五次尝试,在Ajax之前,设置了一个alert弹窗警告。
神奇的,重复请求没了……
这特么都是什么跟什么啊。
此时,已经从晚上八点折腾到了十点多。
为了尽快弄明白问题出在哪里,决定抓包测试。拿出自己的手机,用微信访问页面注册,完全没有任何问题。
难道这和手机有关系?那出现这问题的手机也不是一部啊?拿了一部之前一直出问题的手机过来,连wifi设代理用Fiddler抓包。
完全正常,没有任何问题,注册流程很正常。
但是取消代理,就又只会报邮箱已存在的错误。
What the f*ck……
此时,已经是十一点多了。
这时候,他们提出了一个很有建设性(才怪)的建议,就说是不是因为那个alert导致请求延迟了几秒才正常的,或者这是jQuery的问题?
我很不情愿(并觉得他们纯粹是扯淡)地加了一个setTimeout
测试。
涛声依旧啊
到底咋回事?
根据Java的输出,是有返回创建成功的消息的。
然后我将所有的Ajax结果全部显示到DOM里,发现只有错误信息,却没有那个成功的信息。换句话说,如果请求确实重复发了,那么唯一能解释的是,js运行出错导致对应的消息没处理。
然而新版本的安卓版微信自带浏览器内核,不是系统的webview,所以要调试只能用微信自己的工具。不过好歹可以测试。
连上调试器,打断点,发现ajax函数只调用一次,没问题,但是唯一收到的消息就是返回了错误,却没有正常的结果。所以不是出错,而是确实就没有返回那个结果。
看到这样的结果,所有人暴躁了,这特么到底什么事啊。
本来想在微信里抓包,然而微信调试工具里要抓包同样需要设代理,从之前测试的结果看,设代理就无此问题,又一次被卡住了。
此刻,我的内心是崩溃的,难道真的只能洗洗睡了吗。
啊,看到了曙光
此时,已经凌晨十二点多。为了尽快找到问题,后端的同学开始直接连上服务器实时输出Nginx的访问日志。
神奇的,点击一次注册,滚动出了两条日志……(原谅我没截图)……我一眼看过去,哎卧槽这不是我之前看到的那俩IP么,咋这时候还在???
看到的两条日志,除了客户端IP不一样之外,其它信息一模一样,包括地址、方法和UserAgent。客户端IP分别是123.151.42.57
和211.102.210.254
。211.102.210.254
这个没啥问题,是这边的出口IP,那前面一个 123.151.42.57 是什么鬼?在ip138上查了一下,这是个天津的电信IP。然后这个IP的请求是先发出来的,比后面的请求提前了两秒钟,然而响应状态码是499
,后面的是200
。
499状态码是什么错误呢?搜索了一下Nginx的错误码,指出这个(非标)错误是指客户端关闭了连接。
……………………客户端关闭了连接????
而邮箱已存在的错误,正是后面的那一条200请求返回的。
PHP的同学一拍大腿,卧槽这不是说超时了么,你丫去把请求的超时设长一点。
我不想跟他说话并想甩他一脸翔。
『我之前为了避免此问题已经将超时设成5分钟了。』
不过此时,我开始关注那俩IP了……
猜测
事已至此,我唯一的猜测就是,微信的浏览器里发请求,并不是一定直接向服务器发送请求的,而是通过了某些特定的中转服务器进行转发。至于转发的原因,可能是为了重排版(将PC的网页压缩重排版以节约手机流量或适应大小)或加速(压缩)抑或安全检测(中转的时候拦截已知恶意网址)。往前面翻了一下,前面的几步流程都是123.151.42.57
这个IP转发的,而最后一个注册请求发出来了却在2秒后又由211.102.210.254
直接的连接发起……我只能解释为这个中转服务器的超时时间是两秒钟,为了防止服务器网络问题导致用户过长等待,默认如果请求2秒钟没返回则放弃中转改为直接发起。
如果事实真如此,那么就能解释为什么前后会跟着两个请求,而前一个请求却在2秒钟后出499错误的情况了。
那么为什么用代理的时候没有此问题?很容易解释,如果系统已经设置为通过代理服务器访问了,那么软件会直接假定无法直接联网或无法简单可靠地通过反代访问,所以会放弃中转。
从前后间隔2秒的时间判断,大概超时时间就是2秒左右。那么可以做出假设,假定请求2秒钟内就会返回,则不会引发第二次请求。
Java同学改了一下接口,先不做具体操作直接返回成功,测试了一下,果然就不会有此问题顺利走通流程。
这个坑也是……活活把人坑到了凌晨1点。
后来作为改进的创建方案,Java端在确定可以创建后,直接返回成功,然后开后台任务异步创建。
没有去校验创建是否成功,因为他们觉得失败概率很低,就算失败了其实也没有啥可重试的方法,还是先不检查算了。
到此为止,问题顺利解决。
重现
现在问题解决了,但真正的问题还没解决:那就是我需要确定自己的结论。
根据上面的判断,有这样的结论:
- 微信里直接发出ajax请求的话,其实是通过一些特定的服务器中转的
- 这个中转不是全部的,和手机以及系统有关系(因为我的安卓手机微信就没有这现象,而有此情况的手机也不是某个特定的品牌,iPhone也没此问题)
- 这个中转的超时时间很短,一旦超时,会迅速回滚为非中转模式请求
为了确定是否有此问题,我解析了一个测试域名 debug.fishlee.net
,并模拟了一个带有长、短请求的测试页面,并开启跟踪。短请求即时返回,长请求则会阻断当前的操作10秒钟再结束。
测试代码如下。
<%@ Page Language="C#" AutoEventWireup="true" %> <script type="text/c#" runat="server"> void Page_Load(object sender, EventArgs e) { var method = Request.HttpMethod; var action = Request.QueryString["action"]; var startTime = DateTime.Now; if (method == "POST") { if (action == "slow") System.Threading.Thread.Sleep(10 * 1000); Response.Write((DateTime.Now - startTime).TotalMilliseconds.ToString()); Response.End(); } } </script> <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no" /> <title>wxapi</title> <script src="http://static.fishlee.net/libs/jquery/1.12.4/jquery-1.12.4.min.js"></script> </head> <body> <button id="normalRequest" type="button">常规请求</button> <div></div> <button id="slowRequest" type="button">慢速请求</button> <div></div> <script> $(function () { $("#normalRequest").click(function () { $.post("?action=normal").always(function (data, option, xhr) { $("#normalRequest + div").html(xhr.responseText || "empty."); }); }); $("#slowRequest").click(function () { $.post("?action=slow").always(function (data, option, xhr) { $("#slowRequest + div").html(xhr.responseText || "empty."); }); }); }); </script> </body> </html>
并在 web.config
中开启跟踪。
<?xml version="1.0"?> <configuration> <system.web> <httpRuntime targetFramework="4.5" /> <trace enabled="true" requestLimit="100" pageOutput="false" /> </system.web> </configuration>
开启跟踪后,可以通过 /Trace.axd
查看跟踪结果。
首先,我在自己的手机微信里,分别点击了两个按钮。跟踪显示,只有两个记录,并且客户端IP都是和我本机吻合的。
然后我拿来他们测试有问题的手机,并在里面的微信中测试,分别点击。发现点击两次按钮,出现了三条记录。换句话说,问题复现了。
然后我们分别来看三个请求。
第一个请求对应的是短请求,也就是发送后立马成功的那种,相当无脑。
从此图我们很明显可以看出,远程地址是 123.151.42.57
,这并不是本地宽带的出口IP。从上面的判断可知,这是中转服务器,并且是确凿的,为什么呢,因为最下面有个 HTTP_X__FORWARDED_FOR 这个标头。为什么这么说呢,因为这个标头简直是反代服务器或代理服务器的一种标志性特征,它是为了告诉上游服务器,其实它是转发别人的请求来着的。
然后我们来看第二个请求。
第二个请求是慢请求,除了操作参数不一样外,并没有不同,所有信息都和上面的请求完全一致。
最后我们来看第三个请求。
和之前的相比,典型的不同就是,客户端IP已经成为211.102.210.254
,对应的就是本地的出口IP,换句话说,这是没有通过中转服务器而直接访问源服务器了。
因此,你也会看到下面的HTTP_X_FORWARDED_FOR
标头已经不见了:因为不是中转了。
至此,所有推论得到了论证。
总结
复述一下第6节中的结论。
- 微信里直接发出ajax请求的话,其实是通过一些特定的服务器中转的
- 这个中转不是全部的,和手机以及系统有关系(因为我的安卓手机微信就没有这现象,而有此情况的手机也不是某个特定的品牌,iPhone也没此问题)
- 这个中转的超时时间很短,一旦超时,会迅速回滚为非中转模式请求
至于这个中转到底什么情况下有,什么情况下没有,这个没有找到规律,也未知是微信中内嵌的浏览器内核行为还是系统行为。
如果说非要做一点实质性的总结,那就是,如果假定你的请求是关键请求且难以重试还跑在微信中的,最好保证你的接口在两秒钟内返回,否则可能会有很诡异的难以复现的问题,如果时间过长的最好做成异步的。
结案陈词,就是这种中转机制设计真的很糙,也不知道是哪里引入的。
直到后来,用微信调试工具……
事实胜于雄辩,我觉得单看问题就能猜到原因,我也是牛逼啊……23333
涨姿势了
我就是哪个java
我找到解决办法了,检查出微信中转的请求,然后返回一个403状态,微信内置浏览器就会重新发送请求
"`php
function reject_wechat_forwarded()
{
if(
isset($_SERVER["HTTP_X_FORWARDED_FOR"])
&& strpos($_SERVER["HTTP_USER_AGENT"],"MicroMessenger")
&& (!isset($_SERVER["HTTP_CONNECTION"]) || $_SERVER["HTTP_CONNECTION"]!=="keep-alive")
){
header("HTTP/1.1 403 Forbidden");
exit;
}
}
reject_wechat_forwarded();
"`
这样后续微信还会在每个请求都走代理尝试一次吗?如果会的话,似乎不是很完美的方案。
还是每次都会尝试,但起码不会等待几秒钟了。但现在的问题是对AJAX请求不好使
在请求地址后追加 &connect_redirect=1 即可让请求不再重发.
兄弟们,解决了!!过程不表,情况和博主类似。看别的楼有兄弟说退掉360就好了,我退掉没好,但直接卸载重启服务器以后就好了。服务器上装360,这个锅是老板的,over。感谢鱼大的平台,感谢兄弟们的回复
所以是服务器上的360导致的锅?……好奇怪的锅……
我擦,我的项目今天突然碰到这个问题。到现在不知道怎么解决。两次请求间隔时间只有0.2s,前端只返回了第二次错误的结果。电脑,开发者工具,iphone都没问题,只有安卓。安卓的设置成强制直连就好了
在哪里设置啊?
鱼大我今天项目也突然出现这个问题了,问题是还没有超时。。!! 没超过10秒 ,整个项目所有的ajax包括第一次进页面的登陆操作,全部都会在微信浏览器执行两次,时间间隔1秒。。。
请问大神们怎么解决的?我的项目也是,今天突然出现这个问题,之前都是好好的!微信网页请求都执行2次,也是间隔一秒………………
可以查查服务端日志
我的项目跟你的情况完全一样,所有的请求,安卓机上都执行2次,页面跳转和ajax,间隔有时候还不到1秒……请问你的问题解决了吗?
查查服务器日志里的相关请求记录和时间吧啊
鱼大,查了日志。两个请求一样且来自同一IP且不是发送请求方(我手机)的IP,时间间隔只有不到0.1s,安卓手机只能设置强制直连解决。但不能跟客户和用户这样解释吧。今天弄了一天都不知道怎么解决。其他楼关掉360了,也没用。公司域名也是备案过的。微信客服根本联系不上,唉,有点绝望
这两天也莫名其妙出现了这种情况,看看和各位是否一样哈,项目是在微信公众号上开发的;
1.网站会莫名其妙报错,通过加日志调试,发现所有请求会会发送2次,第一次请求,返回数据都正常,eg:通过code获取微信个人信息,第二次就会报错啦,code重复使用;
2.既然已经知道是重复请求导致报错,继续跟进。继续跟进:
1.前端请求通过检测(地址添加随机数),发现2次请求一模一样,可以确定,前端只发送了一次请求;
2.后台所有逻辑代码删光,仅加了日志,查看还是2次请求(想哭啦),可以确定与后台代码无关;
3.那就剩下项目的环境啦;随便一个请求在浏览器上运行正常,在微信上就会2次请求,可以确定,肯定是微信搞事情啦;但是之前一直是正常的,发现是360搞事情啊,把360关了,就正常啦
最后几经辗转,联系到微信浏览器X5内核研发的小伙伴,也说从前没遇到过,第一次听过。本着解决问题的原则,沟通过程中我提到域名实名制了,但备案还在进行中,着急就先开发了。微信的小伙伴说,可能是qq电脑管家类似的安全软件扫描的原因,对于未知域名的访问都要扫一遍安全性。。。。。。原来如此!!!!!!这就解释了所有接口都请求2次问题,也说明了为什么测试阶段微信web开发者工具、UC浏览器、Safari都正常。同时也印证了什么session获取不到数据,cookie呀,sessionid丢失等等,因为安全扫描的服务器是很多台、部署到不同地方,也不确定通过手机发起的请求和安全服务器发起的请求哪个先到达我们的后台,哪个先返回,根本无法判断cookie存到哪里去了。。。。
后来经过测试,直接通过ip地址请求url,后台收到的访问次数更多,5次左右吧。然后换了备案过的域名,第二天神奇的恢复如常了。。。。。
后台收到多次请求,代码逻辑没问题的话,记得检查域名是否备案,域名是否备案,域名是否备案。如果域名没问题,大多是腾讯安全部门在扫码鉴别url的安全性,打印一下请求的ip和user-agent,给微信客服打个电话说明一下你问题,一般问题会解决的。
———————
作者:gotohomebye
来源:CSDN
原文:https://blog.csdn.net/gotohomebye/article/details/78508741
这个安全扫描略奇葩,照这么说的话,客户端发请求他们先过一道手去请求下检测,这和请求劫持也没啥区别了啊。
调用接口超过10S没有响应,准准的10s中就会出现二次请求;啊啊啊啊啊,看了大佬的文章,暂时的想法是,给定一个请求标识放在session中,进入方法后,先判断当前的请求标识是否为空,不为空,放过请求;若是不为空,则拦截此次请求;不知道这样子能否解决呀,明天先试试
时间过长的话,改为异步处理比较好。
所以呢,根本的解决方式还是没有,只能抱怨微信浏览器的坑
大神,同样的苹果6,同一个ios版本和同一个微信版本,为啥会有一个手机出现这问题
跟很多因素有关系,不好说。
鱼大大,请教一下,测试出问题的机器是什么型号,我也遇到了这种情况,但是测试是没出现,上线后出现这种情况,我们没法复现,求告知,谢谢。
很久了。。不记得了。。不过应该是安卓上的问题,因为安卓上微信才是自带的浏览器内核。
膜拜大神,这个问题只能修改后台吗?有办法修改前台请求规避这个问题吗?
这……我觉得可能没啥好的方法吧
好吧。
大神这问题怎么解决啊?
鱼大大~请问 最后的贴图是啥意思?用户手机如果设置了代理,就不会出现中转情况?不设置就会出现么??
设代理就不会出现。
我们也碰到了这个问题,看到第二次请求的时候我就知道肯定是微信做了代理,但是一直没有想到好的方案来解决,看了您的文章后终于有思路来处理了,感谢。
遇到类似的问题,不同的是请求2秒内已响应,但是10秒后还是收到请求(每次重复请求间隔都是10秒)。前后两次请求的客户端地址及user-agent都不同 这又是啥鬼,怎么破
嗯 半懵逼状态看完这一大篇,先心疼作者半秒钟,嗯 然后看完唯一搞懂的意思就是,微信坑壁==
同样遇到了这样的问题,但是由于业务的问题,不能进行先直接返回结果,再异步处理数据。程序执行需要2-4s左右,重复提交的情况并不是每次都出现,但是会偶尔出现一下,使用的是同一步手机,同样的网络环境进行请求的,真的蛋疼,程序里面也进行判断了 如果存在则不进行数据库操作,但是这几次请求间隔时间太短,又有并发问题,这样的if else判断并不起作用,应该怎么办呢
异步Ajax会不会好点?
这跟是不是异步ajax没关系的。
遇见过一次,解决了
您是怎么解决,求指教,我们项目组也遇到了这样的问题,多谢啦!
我也遇到这个蛋疼的问题,因为我ajax后台的那个方法同步调用的4个接口并入库,估计要话7秒以上。开始也以为是程序问题,花了我大半天时间。。。 用了异步和去重复都不行,因为当前ajax调用之后要跳到第二个页面马上显示刚入库的数据,然后跳到第二个页面后台都直接报错(因为ajax那个方法还没执行完。。。)。 解决方案是做后台做判断,如果数据存在就不插入,管TM微信调多少遍。
任务队列+状态轮询?
你又换新手机了啊。。。
。。。
php可以搞么
鱼大,你的网站和软件怎么都被微软的杀毒软件和edge认为是恶意的了。
SmartScreen都不让下你网站所有的软件。
在公司和家里用win10访问都是这样,麻烦查看一下。
我没有遇到这样的情况,所以没法处理呢。
Hi。今天在微信上遇到一个问题,用户提交表单时候,使用Ajax请求发起。请求已经发出去,但是表单并没有保存成功。测试环境并没有任何问题。生产环境中,像你文中提到的,Iphone没有问题。安卓机会出现这种情况
补充 原因是用户填写的数据并没有通过Ajax传递到后台,Ajax POST请求会出现这种问题。目前解决办法是 get
POST没问题啊。。。试着用微信开发工具调试看看
不不不。微信开发工具调试完全没问题。很奇怪的是,测试一直没问题,知道生产环境发布,安卓机开始大批量的出现这种问题。但是!!!奇怪的是,其他页面的请求完全没问题。简直是崩溃。
就只有一个页面出问题?啊哈哈。。到线上才出问题也是够磨人啊。微信的这个内嵌浏览器风评很差。
咦。最后一个怎么没有回复按钮。这个浏览器真是。。。一言难尽!!!!!!
我的get也不行。。。困扰一天了
对于从linux起步的我来讲,起手就先开tcpdump,这样很早就能发现,多数微信浏览的页面都是被代理的,你看到的页面甚至都不是自己服务器上的文件,用tcpdump抓包有时候会发现打开页面时自己的服务器一个对应请求都收不到。同样ajax也是,根本没法判定自己的请求都发到哪里去了。而且因为这是一个客户端级别的劫持,上https都没有用。
作为智能机时代的移动版IE6,微信的坑太多了。
话说你们这测试也是个瞎猫,测试也是糊弄,debug这么迷迷糊糊。
确实很坑,最近做了一些和微信有关的开发才发现,之前并没有接触过。特么谁能想到那问题居然还是微信的锅啊,真是无言了。
所以出现这个问题的手机型号中,有小米3?
有。这是微信的机制,和具体的机型应该没有关系。
楼主,这个问题还有其他的解决方案么
这个坑很早就有了呀,只能说楼主后知后觉了。。。
楼主可以关注以下两个UserAgent:
Mozilla/5.0 (Linux; U; Android 4.4.2; zh-cn; GT-I9500 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko)Version/4.0 MQQBrowser/5.0 QQ-Manager Mobile Safari/537.36
Mozilla/5.0 (iPhone; CPU iPhone OS 8_1_2 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) (Engine, like URL) Mobile/12B440 MicroMessenger/6.0.1 NetType/3G+
我想这TMD一定是微信服务器伪装以上两个UserAgent拉取用户访问的页面作监控
之前并没有做过微信的嵌入页。
这俩UA咋了,看不出问题啊。
遇到同样的问题,请问这个问题怎么破
涨姿势啊
鱼大进腾讯了?
并没有。
心疼鱼大三秒钟,半夜三点还在撸代码