前几天线上出了一个Vue SSR服务端渲染的Bug。

页面偶尔白屏报错还很难复现。测试环境怎么测都没问题一到线上就偶尔出问题。

我排查了整整一夜才找到原因解决了问题。

今天想记录一下这次排查Bug的完整过程。包括问题现象排查思路尝试的各种方法最终找到的原因以及解决方案。

希望这些经验能帮大家在遇到类似问题的时候,少走弯路。

一、问题现象

先说说问题的现象。

我们的项目用的是Vue 2+Vue SSR服务端渲染架构。前端用Vue服务端用Node.js做SSR渲染,然后把渲染好的HTML返回给浏览器。

上线之后,一直运行正常。但是前几天运营反馈说有用户反映页面偶尔白屏打不开。

我们一开始以为是网络问题,或者用户的浏览器问题。但是后来反馈的用户越来越多我们才意识到可能是系统的问题。

我们自己去访问页面大部分时候都正常,但是偶尔也会遇到白屏。打开浏览器控制台看到报错:"TypeError: Cannot read property 'xxx' of undefined"。

但是这个报错的位置是在服务端渲染的HTML里的内联脚本中不是我们的前端代码。

而且这个问题很难复现。刷新几次可能遇到一次也可能刷新几十次都遇不到。测试环境更是怎么测都没问题。

这就很头疼了。

二、初步排查

一开始我们以为是前端代码的问题。

因为报错是"Cannot read property 'xxx' of undefined"看起来像是,前端代码中访问了一个undefined对象的属性。

我们检查了前端代码找了很久也没找到哪里可能会出现这个问题。

而且这个报错是在服务端渲染的HTML里的内联脚本中不是我们的前端代码文件中。这说明问题可能出在服务端渲染的过程中。

于是我们开始怀疑是SSR的问题。

我们看了Node.js的日志发现有一些请求报错了,但是错误信息很模糊只是说渲染出错没有具体的错误堆栈。

而且这些报错的请求URL都不一样没有规律。有时候是首页有时候是详情页有时候是列表页。

这就更奇怪了。

三、深入排查

我们决定深入排查。

首先,我们在Node.js的SSR渲染代码中加了更详细的错误日志把错误堆栈也打出来。

然后等了一段时间终于抓到了一个完整的错误堆栈。

错误堆栈显示问题出在一个Vue组件的render函数中访问了this.$route.params.xxx但是,this.$route.params是undefined。

这就奇怪了。$route.params怎么会是undefined呢?

我们检查了这个组件的代码发现这个组件是一个通用的列表组件在很多页面都用到了。它会根据$route.params中的参数来加载不同的数据。

按理说$route.params不应该是undefined啊。即使没有参数也应该是一个空对象{}而不是undefined。

我们又检查了路由配置发现路由配置也没问题参数都定义了。

这就更奇怪了。

四、关键线索

就在我们一筹莫展的时候,一个同事发现了一个关键线索。

他发现出问题的请求都是来自搜索引擎的爬虫,比如百度蜘蛛Googlebot等等。

正常用户的请求很少出问题,但是爬虫的请求经常出问题。

这是为什么呢?

我们分析了一下发现爬虫的请求和正常用户的请求有一个区别:爬虫不会执行JavaScript。

正常用户访问页面的时候,浏览器会执行JavaScriptVue会在客户端接管页面进行客户端渲染和路由管理。

但是爬虫不会执行JavaScript它只看服务端返回的HTML。

这和$route.params为undefined有什么关系呢?

我们又仔细看了错误堆栈发现错误不是发生在服务端渲染的过程中而是,发生在服务端渲染完成后注入到HTML中的内联脚本执行的时候。

具体来说,我们的SSR框架会把Vuex的状态序列化注入到HTML中作为window.__INITIAL_STATE__然后客户端的Vue会从这个初始状态恢复。

同时我们的框架还会把当前的路由信息也注入到HTML中作为window.__INITIAL_ROUTE__。

问题就出在这里。

五、找到原因

我们终于找到了原因。

原来我们的SSR框架在注入初始路由信息的时候,有一个bug。

当请求的URL带了一些特殊的查询参数,或者URL编码有问题的时候,服务端的路由解析会失败导致$route对象没有正确初始化。

正常用户的请求URL一般都比较规范不会有问题。但是爬虫的请求URL有时候会带一些奇怪的参数,或者URL编码不规范就会触发这个bug。

当路由解析失败的时候$route.params就会是undefined而不是空对象。

然后在渲染组件的时候,组件访问this.$route.params.xxx就会报错"Cannot read property 'xxx' of undefined"。

但是为什么测试环境复现不了呢?

因为测试环境的请求都是我们自己发的URL都比较规范不会有奇怪的参数。而线上有各种爬虫会发各种奇怪的URL请求,所以才会偶尔出问题。

而且这个问题,还有一个隐蔽的地方:当服务端渲染报错的时候,我们的错误处理中间件会捕获错误,然后返回一个错误页面。但是我们的错误处理中间件有一个问题它会先尝试用SSR渲染错误页面,如果错误页面也渲染失败就会返回一个空白页面。

所以用户看到的就是白屏。

六、解决方案

找到原因之后,解决方案就简单了。

我们从几个方面修复了这个问题:

1. 修复路由解析的bug

首先,我们修复了SSR框架中路由解析的bug。

当URL解析失败的时候,我们会给$route一个默认值确保$route.params至少是一个空对象{}而不是undefined。

同时我们也加强了URL的容错处理对于不规范的URL也能正确解析,或者友好地报错。

2. 加强组件的防御性编程

其次,我们加强了组件的防御性编程。

在访问$route.params之前,先判断一下$route.params是否存在。如果不存在就给一个默认值。

比如:

const params = this.$route.params || {};
const id = params.id;

这样,即使$route.params是undefined也不会报错。

我们把所有访问$route.params的地方都加了这样的防御性判断。

3. 修复错误处理中间件

然后我们修复了错误处理中间件的问题。

当SSR渲染报错的时候,不再尝试用SSR渲染错误页面而是直接返回一个静态的错误页面,或者返回500状态码让CDN或者反向代理返回错误页面。

这样,即使渲染出错用户也不会看到白屏而是看到一个友好的错误提示。

4. 加监控和告警

最后我们加了更完善的监控和告警。

对于SSR渲染的错误我们会记录详细的错误日志包括错误堆栈请求URL请求头等等方便排查。

同时我们也加了告警当错误率超过阈值的时候,会及时通知我们。

七、经验总结

这次排查Bug花了整整一夜,但是也积累了很多经验。

总结一下主要有这几点:

1. SSR问题要区分服务端和客户端

SSR的问题比较复杂,因为代码既在服务端跑也在客户端跑。出问题的时候,要先区分是服务端渲染的问题还是客户端渲染的问题。

可以通过查看页面源代码看服务端返回的HTML是否正常来判断。如果HTML正常,但是页面白屏可能是客户端的问题。如果HTML本身就有问题那就是服务端的问题。

2. 难复现的问题要关注特殊场景

很难复现的问题往往出在一些特殊的场景。比如爬虫的请求特殊的浏览器特殊的网络环境等等。

要关注这些特殊场景的请求和正常请求有什么区别往往能找到线索。

3. 防御性编程很重要

不管框架多么完善都可能有bug。所以在写组件代码的时候,要做好防御性编程。

访问可能为undefined的对象属性之前,先判断一下。不要假设框架一定会给你正确的数据。

4. 错误处理要完善

错误处理很重要。即使出了bug也要保证用户能看到一个友好的错误提示而不是白屏。

错误处理中间件要简单可靠不要在错误处理中又可能出错。

5. 监控和告警要到位

线上问题往往是用户先发现的这很被动。要有完善的监控和告警在问题刚出现的时候,就能发现及时处理。

不要等用户反馈了才知道出问题了。

八、写在最后

以上就是这次排查Vue SSR Bug的完整过程。

虽然花了整整一夜很辛苦,但是找到原因解决问题的那一刻还是很有成就感的。

而且通过这次排查我们也发现了系统中的很多不足修复了很多潜在的问题让系统更稳定了。

做技术就是这样不断遇到问题不断解决问题不断成长。

希望我的这些经验能帮大家在遇到类似问题的时候,少走弯路更快地找到原因解决问题。

最后用一句话结束这篇文章:"Bug不可怕可怕的是找不到Bug的原因。只要耐心排查总能找到答案。"

愿大家都能写出没有Bug的代码也能从容应对各种线上问题。