`importLibrary` loops forever (tab freeze / OOM) when the bootstrap callback receives an error payload
维护者通常 1 天内回复
还没有人认领这个 Issue。
评估
- 难度
- 3/5
- 预计耗时
- 1-2 天
- 新手友好度
- 72/100
- Issue 类型
- 缺陷
- 描述清晰度
- 描述清楚
- 活跃度
- 活跃
- 技术栈
- javascript, typescript
调研方向
Start in dist/index.js by tracing bootstrap(), namespace.ib, and namespace.importLibrary when the callback receives the shown error payload. Run the browser or Node reproduction from the linked gist, then verify that the error path settles without freezing or growing memory while the ?ok control still resolves.
由索引模型根据 Issue 内容生成。
描述
Environment
@googlemaps/js-api-loader2.1.3 (also 2.0.2, as vendored by vue3-google-map 0.27.2)- Maps JS
v=weekly(served3.66.6c), Chrome 154, Electron 138, and Firefox 156.
Bug
maps.googleapis.com/maps/api/js intermittently responds 200 with a body that calls the callback with an error instead of loading the API:
google.maps.__ib__({ "error": { "code": 503, "message": "The service is currently unavailable.", "status": "UNAVAILABLE" } });
We observed it in CI browser runs and measured it with curl from a separate network: ~1–4% of uncached requests, with or without a key, often in bursts. The error response has no caching headers, unlike the normal bootstrap. The same finding has been reported to the Maps JS API issue tracker: https://issuetracker.google.com/issues/566284337.
In dist/index.js, bootstrap() sets:
namespace[PENDING_BOOTSTRAP_KEY] = resolve; // ignores the callback argument
...
namespace[IMPORT_API_NAME] = (libraryName, ...args) =>
libraries.add(libraryName) && triggerBootstrap().then(() => namespace[IMPORT_API_NAME](libraryName, ...args));
When the callback carries an error, triggerBootstrap() resolves anyway. The .then then calls namespace.importLibrary, which is still this function, because the API never loaded. That call gets back the same resolved promise and chains again: an infinite microtask loop. The page's main thread never yields, memory grows several hundred MB/s, and the renderer is killed at the V8 heap limit. The importLibrary() promise never settles, so callers can't catch it, and error reporting never sees it.
Reproduction
Runnable repro: https://gist.github.com/mattico/360de14756ae2c96d912ebe475301194. It has a single-file browser page (no install, no API key) and a Node + happy-dom script, each with a control mode. Both replace the network with a stub that answers the bootstrap <script> the way the real endpoint does. The browser version, for reference:
<!doctype html>
<!--
Open this file in a browser (file:// is fine). No install, no API key.
repro.html the fake server returns the 503 error callback -> tab freezes
repro.html?ok control: the fake server returns the API -> resolves
-->
<meta charset="utf-8">
<title>js-api-loader error-callback repro</title>
<p id="status">Starting in 1s…</p>
<p>Heartbeat (stops when the main thread is stuck): <b id="beat">0</b></p>
<script type="module">
import { setOptions, importLibrary } from "https://cdn.jsdelivr.net/npm/@googlemaps/[email protected]/+esm";
const serverOk = new URLSearchParams(location.search).has("ok");
const status = (text) => (document.getElementById("status").textContent = text);
let beats = 0;
setInterval(() => (document.getElementById("beat").textContent = ++beats), 100);
// Stand in for maps.googleapis.com: answer the bootstrap <script> the loader
// appends the way the real endpoint does, either with the API (control) or
// with the error callback it intermittently serves.
const append = document.head.append.bind(document.head);
document.head.append = (node) => {
if (!(node instanceof HTMLScriptElement && node.src.includes("/maps/api/js"))) return append(node);
setTimeout(() => {
if (serverOk) {
google.maps.importLibrary = async (name) => ({ name }); // the real API replaces the stand-in
google.maps.__ib__();
} else {
google.maps.__ib__({ error: { code: 503, message: "The service is currently unavailable.", status: "UNAVAILABLE" } });
}
}, 0);
};
setTimeout(() => {
setOptions({ key: "YOUR_API_KEY", v: "weekly" });
status(`importLibrary("maps") with the server ${serverOk ? "OK" : "returning the 503 error callback"}…` +
(serverOk ? "" : " The heartbeat should stop now and the tab freeze."));
importLibrary("maps").then(
(lib) => status(`resolved: ${JSON.stringify(lib)}`),
(err) => status(`rejected: ${err.message}`),
);
}, 1000);
</script>
repro.html: after 1 s the heartbeat stops and the tab freezes. In Chrome 154 that's 100% CPU with memory climbing until an out-of-memory crash.repro.html?ok: control. The stub returns the API, andimportLibrary("maps")resolves.
In Node (node --max-old-space-size=256 repro.mjs from the gist), the control resolves. With the error callback no heartbeat ever prints, and the process dies with Reached heap limit … JavaScript heap out of memory in under a second.
Against the real endpoint: load a page that calls importLibrary("maps") with the cache disabled and reload repeatedly. A few percent of loads freeze (assuming that maps.googleapis.com/maps/api/js continues to return errors at that rate).
Reproduced in desktop browsers by auto-reloading a page containing Google's documented snippet verbatim (the same loop this package vendors) (a unique throwaway URL parameter per load, so the HTTP cache never answers) until the error callback was served:
- Chrome 154: the tab froze at 100% CPU with memory climbing until it crashed with an OOM.
- Firefox 156: the web console logged an out-of-memory error; as far as we could tell the tab itself did not crash.
Suggested fix
- Treat an error payload as a failed load:
namespace.__ib__ = (r) => r?.error ? fail(r.error) : resolve(). ResetbootstrapPromiseasonerrordoes, and remove the failed script, so the next call can retry.
This is safe: on success__ib__is called with no arguments, and an invalid key or disallowed referrer still gets the normal bootstrap (those surface later viagm_authFailure, not__ib__). So anerrorargument only means the transient failure, and a retry can't loop on a configuration error. Retrying with a short backoff recovered 100% of affected loads in our testing (27 of 900 cold loads hit the error, and all loaded after one to three retries). - Independently, never let the stand-in call itself after the bootstrap settles. If
namespace.importLibraryis still the stand-in, reject, or wait on a macrotask with a bound, rather than recursing through microtasks. That turns any future mismatch between the callback and the API install into an error instead of a frozen tab.
We're running a patch that does both and can open a PR if that's useful.
- 主要语言
- TypeScript
- 星标
- 451
- 派生
- 75
- 平均合并
- 2 分钟
- 30 天内合并 PR
- 31
环境准备
- 没有 Dockerfile 或 Docker Compose 文件
- 没有 Pull Request 模板
- 阅读贡献指南
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
googlemaps/js-api-loader 的其他 Issue
-
难度 2/5 1-3 小时 新手友好度 72/100
googlemaps/js-api-loader#1002 · 7 条评论 · 23 个 reaction ·
维护者通常 1 天内回复
-
type: feature request wontfix
难度 4/5 3-5 天 新手友好度 30/100
googlemaps/js-api-loader#1143 · 1 条评论 ·
维护者通常 1 天内回复
-
Dynamic Api Keys未关闭type: question wontfix
难度 5/5 一周以上 新手友好度 25/100
googlemaps/js-api-loader#871 · 4 条评论 ·
维护者通常 1 天内回复
-
type: feature request wontfix
难度 5/5 一周以上 新手友好度 30/100
googlemaps/js-api-loader#210 · 18 条评论 · 24 个 reaction ·
维护者通常 1 天内回复
查看 googlemaps/js-api-loader 的全部 Issue
相似的 Issue
-
难度 2/5 1-3 小时 新手友好度 72/100
duolicious/duolicious#1943 ·
维护者通常 1 天内回复
-
bug needs-triage
难度 2/5 1-3 小时 新手友好度 80/100
nightly-labs/openbot#6451 ·
维护者通常 1 天内回复
-
complexity: easy good first issue impact: low
难度 2/5 1-3 小时 新手友好度 72/100
ueberdosis/hocuspocus#1211 ·
维护者通常 1 天内回复
-
e2e-failure ready-to-code
难度 2/5 1-3 小时 新手友好度 64/100
redhat-developer/rhdh-plugin-export-overlays#4733 · 1 条评论 ·
维护者通常 1 天内回复
-
S-Needs triage
难度 2/5 1-3 小时 新手友好度 68/100
维护者通常 2 天内回复