INVALID_STATE_ERR Debug 记录

项目前端使用 SAPUI5 开发,运行集成测试时,所有的 AJAX 请求都会被 Mock。今天遇到一个问题,一些 URL 改动后集成测试运行失败, 报错信息如下:

1
2
3
4
5
6
7
8
9
10
11
Uncaught Error: INVALID_STATE_ERR - 4
at k (sinon.js:184)
at F.setResponseHeaders (sinon.js:184)
at F.respond (sinon.js:184)
at F.window.sinon.FakeXMLHttpRequest.respondJSON (MockServer-dbg.js:3834)
at mockserver.js:298
at Array.some (<anonymous>)
at Object.response (mockserver.js:284)
at F.b (sinon.js:196)
at F.processRequest (sinon.js:196)
at F.respond (sinon.js:196)

本文记录了该错误的排查过程,方便今后查阅。

尝试 Google Search,关键词 INVALID_STATE_ERR,只搜到一些不是很相关的答案

转化思路 - Bug 是发生在 SAPUI5 的 MockServer 中,由于我对 MockServer 并不了解,因此我决定深入一下 MockServer

查阅 API 文档

打开 SAPUI5 API 文档,找到 MockServer, 这个对象继承自 ManagedObject,至于 ManagedObject 是干嘛的我目前还不太清楚,但是我决定先跳过这个细节。

MockServer 用来 Mock HTTP 请求。往下看 Constructor 用于创建一个 mocked server,可以 Mock XHR 和 OData/JSON Models。虽然我对 OData 还不是很懂,但是这应该与这个 BUG 关系不大,所以我跳过这个细节。Constructor 接受一些配置参数,

  • rootUri
  • requests
  • recordRequests

rootUri 是根路径,具体怎么用暂时比较模糊,稍后通过查阅源码来确定。requests 是一个数组,存放了被 mock 的请求(大致理解,稍后确认)。至于 recordRequests 是请求性能分析相关的,暂时忽略。

基于以上理解,一个 MockServer demo 如下:

1
2
3
4
5
6
7
8
var aReqs = [];
// TODO: add mocked requests
var oMockServer = new MockServer({
rootUri: './',
requests: aReqs
});

oMockServer.start();

继续阅读 API 文档,Events 部分暂时跳过,来到 Methods 部分,项目中使用到了 sap.ui.core.util.MockServer.config 方法,该方法接受一个配置对象,其中几个属性如下:

  • autoRespond
  • autoRespondAfter
  • fakeHTTPMethod

项目中的配置是

1
2
3
4
5
{
autoRespond: true,
autoRespondAfter: (oUriParameters.get("serverDelay")
|| Math.floor(Math.random() * (1501 - 200) + 200))
}

并有注释说明,autoRespondAfter 加随机数是用于模拟真实环境的网络时延,其中 oUriParameters 如下定义:

1
var oUriParameters = new UriParameters(window.location.href);

看个 Demo

看完文档,还是比较模糊,下一步我需要看个 Demo,本来我可以直接看项目中的 MockServer,只不过项目中的 MockServer 是比较复杂的,不利于理解。

根据官方 Demo 画了个流程图,

其中用到了 simulate 的方法。

深入源码

至此,我对 MockServer 有了一知半解,但是我还不知道 rootUri, requestsautoRespond 的具体逻辑,所以我决定去看看 MockServer 的源码。克隆 openui5

1
$ git clone https://github.com/SAP/openui5.git

找到 src/sap.ui.core/src/sap/ui/core/util/MockServer.js

从 start 方法开始分析, 我们可以发现 start 方法会调 sinon.fakeServer.create 创建一个 FakeServer,然后再注册 mocked requests. 所以接下来我需要了解一下 sinon.fakeServer.

打开 sinon API 文档,找到FakeServer,使用很简单,直接看如下 demo,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
setUp: function () {
this.server = sinon.createFakeServer();
},

tearDown: function () {
this.server.restore();
},

"test should fetch comments from server" : function () {
this.server.respondWith("GET", "/some/article/comments.json",
[200, { "Content-Type": "application/json" },
'[{ "id": 12, "comment": "Hey there" }]']);

var callback = sinon.spy();
myLib.getCommentsFor("/some/article", callback);
this.server.respond();

sinon.assert.calledWith(callback, [{ id: 12, comment: "Hey there" }]);

assert(server.requests.length > 0)
}
}

其中两个 respondWithrespond 这两个方法需要我重点关注。回到 MockServer.js 的源码发现,_addRequestHanlder 方法最终调用 respondWith (3446 行附近),

1
this._oServer.respondWith(sMethod, oRegExp, fnResponse);

从 API 文档得知,正则匹配到的 HTTP 方法将调用 fnResponse,由该方法手动处理响应。

1
2
3
server.respondWith("GET", /\/todo-items\/(\d+)/, function (xhr, id) {
xhr.respond(200, { "Content-Type": "application/json" }, '[{ "id": ' + id + " }]");
});

When the response is a Function, it will be passed the request object. You must manually call respond on it to complete the request.

文档还说,如果 respondeWith 中传递的response是一个回调函数,那么执行回调时会传递 xhr 对象,并且需要回调手动调用 xhr.respond 方法发动响应。

现在,回头看项目中的 MockServer 的代码,

1
2
3
4
5
6
7
8
var aReqs = [];
// TODO: add mocked requests
var oMockServer = new MockServer({
rootUri: './',
requests: aReqs
});

oMockServer.start();

其中还有一个循环将所有的 request 添加到 aReqs 中,

1
aReqs.push(createRequest(data));

再看 createRequest(data) 方法,该方法返回一个对象

1
2
3
4
5
6
function createRequest(data) {
var oRequest = {
// ... some code here
};
return oRequest;
}

这个对象被 MockerServer.js 的 start 方法消费,看源码

该对象提供了 respondWith 中需要的三个参数,即 method, path 和 response。

至此, 我已经看懂了整个 MockServer 的过程了,再回头看错误信息,

看懂错误信息

错误由 XMLHttpRequest 对象引起,因此需要查阅 XHR 的 API 文档,搜索关键词 INVALID_STATE_ERR 共发现 8 处,

Throws an INVALID_STATE_ERR exception if the state is not OPENED or if the send() flag is true.

也就是说如果 XHR 的状态如果是 not opend 或者是已经 send 后的状态,那么就会抛出该异常。

结合错误信息给出的函数调用栈很容易定位出该错误发生在 setRespinseHeaders 之后,打开 sinon 源码去看 setResponseHeader 的实现,发现有个 verifyRequestOpend 方法。

再进入 verifyRequestOpend 方法一看,果然是从这里抛出的异常,异常原因就是 xhr 的状态已经变成 4 了,4 代表 DONE,详细状态参考这里

1
2
3
4
5
function verifyRequestOpened(xhr) {
if (xhr.readyState != FakeXMLHttpRequest.OPENED) {
throw new Error("INVALID_STATE_ERR - " + xhr.readyState);
}
}

说明 298 行执行 xhr.resondJSON 之前已经执行过 xhr.send 方法了。

DEBUG

基于以上理解,打断点追踪一下就很容易发现代码逻辑有问题了(当然,这个代码不是本人写的,甩锅……)。

以上,记录一次 Debug 的过程。

本文标题:INVALID_STATE_ERR Debug 记录

文章作者:Pylon, Syncher

发布时间:2020年05月28日 - 21:05

最后更新:2020年05月29日 - 23:05

原始链接:https://0x400.com/2020-05-28-dev-invalid-state-error-debug-log.html

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。