The previous article just resolved the Production public network acceptance stability issue for the A Tour of Go multilingual project: I migrated a large number of public HTTP checks from the local cross-border SOCKS link to zgocloud direct.
I originally thought that after fixing the network issues, the first Production of the Italian site would wrap up smoothly.
As it turned out, after machine acceptance fully passed, it got stuck again at the next stage:
[production-browser] FAILED: page did not renderAnd the first failure was on the homepage:
https://it-go-dev.shuijingwanwq.com/After running it again, the homepage passed, but then it failed on:
https://it-go-dev.shuijingwanwq.com/tour/Stranger still, accessing these pages directly from the same machine using curl for 20 consecutive requests all succeeded, each taking less than 1 second.
Ultimately, I determined that this and the previous cross-border public network issue were two completely independent problems.
What actually needed adjusting was Headless Chrome’s:
render readiness criteria.
1. Machine Acceptance has PASSED, but Browser Acceptance failed
In the formal Production workflow, machine acceptance and browser acceptance are two independent stages.
The former primarily verifies:
public routes
HTML identity
sitemap
socket boundary
CDN cacheThe latter actually launches Headless Chrome and uses the Chrome DevTools Protocol to check page rendering, desktop and mobile views, the editor, Run / Format / Reset, SPA, ads, and other browser behaviors.
When entering browser acceptance for the first time, the public machine gate had already succeeded:
PRODUCTION MACHINE ACCEPTANCE: PASS
[首次生产] 公网验收:PASS(71.1s)But immediately after:
[production-browser] FAILED: page did not render:
https://it-go-dev.shuijingwanwq.com/The entire first Production therefore halted at:
stage: browserI then formally resumed it.
The second machine acceptance still passed:
PRODUCTION MACHINE ACCEPTANCE: PASS
[首次生产] 公网验收:PASS(49.1s)This time the homepage seemed fine, but the error moved to /tour/:
[production-browser] FAILED: page did not render:
https://it-go-dev.shuijingwanwq.com/tour/
/ and /tour/ respectivelyThis phenomenon made me start to wonder:
Did the page access really fail?
If Production itself had a persistent failure, the failure location would usually be more stable.
But instead it was:
First time: /
Second time: /tour/This looked more like an issue with some transient check within browser acceptance itself.
2. First, confirm whether ordinary public network access is actually unstable
Since the previous article had just dealt with cross-border network issues, the easiest mistake to make here was to continue attributing the new browser failure to:
zgocloud
Cloudflare
cross-border networkBut this time, I had to first clarify the actual link.
The public network portion of machine acceptance is now:
local maintenance machine
↓ SSH
zgocloud
↓
Cloudflare
↓
ProductionBrowser acceptance, however, does not run on zgocloud.
Headless Chrome actually runs on my local ThinkPad:
ThinkPad
↓
Cloudflare
↓
ProductionTherefore:
A browser acceptance failure does not directly indicate a problem with zgocloud.
To determine whether the local public network link itself was unstable, I continuously tested the two URLs that had failed directly from the same ThinkPad.
Homepage:
https://it-go-dev.shuijingwanwq.com/10 consecutive requests.
/tour/:
https://it-go-dev.shuijingwanwq.com/tour/Also 10 consecutive requests.
Result:
20 / 20 HTTP 200And the total time for most of them was only:
about 0.56 to 0.84 seconds
/ and /tour/ directly from the same ThinkPad returned HTTP 200 for all 20 consecutive requestsThis does not prove that the network will never experience transient issues.
But it at least provides a very important basis for judgment:
Current ordinary HTTP public network access does not exhibit anomalies that could explain the consecutive browser failures.
Therefore, there was no need to modify the zgocloud, Cloudflare, or Production network architecture again.
The investigation should continue into Headless Chrome’s own rendering checks.
3. The old render readiness check was too strict
After continuing to inspect the browser acceptance implementation, I found that the old logic, after navigating to the page, would wait for the page to meet a condition similar to this:
document.readyState == "complete"
+
body has already presented sufficient text contentThe wait time was up to about 30 seconds.
At first glance, this check seems reasonable:
Only when the page enters
completeis it considered fully loaded.
But for real web pages, complete is not necessarily the best condition for determining that a “page is ready for acceptance.”
Browsers typically go through:
loading
↓
interactive
↓
completeWhen the state enters:
interactivethe DOM has finished parsing, and the main body of the page can usually be interacted with normally.
But:
completerequires waiting for more external resources to finish.
And a real Production page contains more than just its own HTML, CSS, and JavaScript.
It may also include:
analytics scripts
ad scripts
third-party resources
network requestsThe loading state of these resources does not necessarily affect whether the course page itself has rendered correctly.
So a situation could arise where:
the main body of the page has already appeared
and the user can actually already use it
↓
but some peripheral resource has not finished yet
↓
readyState does not meet the old condition for a long time
↓
browser acceptance determines:
page did not renderThis produced a false failure.
4. Why does the failing page change?
This also explains a phenomenon that previously seemed rather strange.
First time:
/failed.
Second time:
/passed, but:
/tour/failed again.
If the page itself had a deterministic HTML or JavaScript error, it should usually be stably reproducible on the same route.
However, if the problem stems from:
the page has mostly rendered
+
fluctuation in the completion time of peripheral resources
+
the readiness condition requires completethen which page a navigation timeout occurs on could be coincidental.
This aligns better with the actually observed phenomenon.
5. You can’t simply change the 30-second timeout to 60 seconds
After discovering this issue, the simplest fix seemed to be:
30 seconds
→
60 secondsor even:
120 secondsBut this does not actually solve the problem with the criteria itself.
If a page is in the:
interactivestate:
- the DOM already exists
- the page body has already appeared
- and subsequent semantic checks can also be executed
then hanging the entire browser acceptance for tens of seconds just to wait for a non-critical resource is not very valuable.
Conversely, if the page truly did not render, simply extending the timeout only makes the failure slower.
So this time I did not simply increase the total wait time.
6. New strategy: interactive or complete + actual page content
The adjusted readiness check no longer only accepts:
readyState == completebut allows:
interactive
or
completewhile still requiring:
the page body has already presented valid textThat is to say:
Instead of only looking at the browser state string, it combines the browser lifecycle state with the actual page content.
The logic roughly becomes:
readyState ∈ {interactive, complete}
+
bodyTextLength > minimum thresholdThis avoids a certain type of error:
the DOM is normal
the page is already displayed
only peripheral resources have not all finished
↓
yet it is determined as page did not renderAt the same time, we cannot only check for:
interactiveOtherwise, an almost blank page could also be mistaken for normal.
So the body content check is still retained.
7. One long wait changed to 3 independent navigations
There was another adjustment this time.
The old logic was closer to:
one navigation
↓
maximum wait of about 30 seconds
↓
success / failureNow it is changed to:
navigation 1
wait up to about 10 seconds
↓
if render readiness is not met
navigation 2
wait up to about 10 seconds
↓
still not met
navigation 3
wait up to about 10 seconds
↓
only fail closed if it still failsThat is, a maximum of 3 independent navigations.
An important point here is:
It is not simply retrying one navigation indefinitely.
The overall time budget for render readiness remains roughly at the same scale as before.
The difference is that if a particular navigation happens to encounter a transient state in the browser or resource loading, a clean page navigation can be re-established.
Compared to:
hanging the same abnormal navigation for a full 30 secondsthis approach is more friendly to transient render failures.
8. But true semantic errors cannot be retried into passing
Adding retries easily introduces another risk:
What if a real browser acceptance error is tried a few more times and coincidentally bypassed?
So this time, the retry scope was strictly limited.
What is allowed to be retried is only:
render readinessThat is:
whether the page has entered a state where acceptance can continueOnce page readiness is met, the subsequent formal semantic checks are still:
single run
+
fail closedFor example:
whether canonical is correct
whether html lang is correct
whether the course content is correct
whether Run works
whether Format works
whether Reset works
whether SPA is normal
whether the ad mount is normalIf these checks fail, it is a real Production browser acceptance failure.
You cannot “refresh it into a PASS” by refreshing 3 or 5 times.
This is the boundary I value most in this modification:
Only give retries to parts that might truly be transient; do not give retries to semantic errors.
9. After the modification, the same Production release formally passed
After completing the adjustments, I did not re-publish or re-deploy.
I continued using the previously deployed formal it-IT release and resumed first-production again.
Machine acceptance:
PRODUCTION MACHINE ACCEPTANCE: PASS
[首次生产] 公网验收:PASS(31.1s)Then Browser Acceptance:
[production-browser] desktop routes: PASS
[production-browser] mobile /tour/moretypes/1: PASS
[production-browser] Run / Format / Reset / SPA / ads: PASS
PRODUCTION BROWSER ACCEPTANCE: PASS
[首次生产] 浏览器验收:PASS(41.4s)Finally, the process formally advanced to:
[首次生产] READY FOR HUMAN VISUAL GATE
This result also proves:
This modification was not intended to skip browser acceptance.
The subsequent:
desktop routes
mobile
Run
Format
Reset
SPA
adswere all still actually executed and passed.
It only fixed the initial:
"whether the page is already ready to begin acceptance"check.
10. This issue and the previous network issue are actually completely different
These two issues happened back-to-back, making it easy to conflate them.
But the actual links are completely different.
The previous issue was:
local
↓
cross-border SOCKS
↓
zgocloud
↓
CloudflareA large number of public network requests went through an unnecessarily existing cross-border link, resulting in:
curl 28
curl 97The final solution was:
zgocloud directBut this time:
Machine Acceptance: PASS
↓
local Headless Chrome
↓
page did not renderOrdinary curl also had:
20 / 20 PASSWhat actually needed adjusting was:
Headless Chrome render readinessSo even though both were:
Production FAILEDthe correct path to resolve them was completely different.
The former required fixing:
network execution pointThe latter required fixing:
browser readiness semanticsThis is also why I increasingly dislike seeing FAILED and immediately:
redeploying
re-executing
increasing timeout
increasing retriesFirst, you should still look at:
stage
+
evidence11. The hardest part of automated acceptance is not being “strict”, but being “reasonably strict”
Automated Production gates can easily swing to two extremes.
One is being too lenient:
PASS even when there are issuesThis is obviously meaningless.
The other is:
the stricter the check condition, the saferBut in reality, that’s not necessarily true.
For example:
must have readyState == completelooks stricter than:
interactive or completeBut if this extra restriction has no direct relation to the actual Production semantics that need to be verified, then what it brings may not be higher safety, but:
more false failuresThe truly reasonable standard should be:
whether the page has reached the state required to execute the subsequent formal semantic acceptance.
rather than:
whether all possible peripheral resources have completely finished.
These two goals are not entirely the same.
12. Summary
This issue initially looked very strange:
Machine Acceptance: PASSbut:
Browser Acceptance: FAILED
page did not renderAfter running it again, the failing page even moved from:
/to:
/tour/While the same machine executing ordinary HTTP requests directly had:
20 / 20 HTTP 200Ultimately, I found that the real problem was not in the Production page itself, nor in the zgocloud network path that had just been adjusted in the previous article.
But rather in:
Headless Chrome render readinessThe original check relied too heavily on:
document.readyState == completeIt has now been adjusted to:
interactive or complete
+
valid body content
+
up to 3 independent navigationsWhile continuing to maintain:
formal semantic checks
=
single run
+
fail closedThis both reduced false failures caused by transient browser states and did not lower the standards of Production Browser Acceptance itself.
What left the deepest impression on me this time was actually:
A good automated gate should not only be able to catch errors, but also accurately distinguish between real errors and the acceptance tool’s own transient failures.
Otherwise, the acceptance tool itself will become a new source of instability in the Production workflow.
需要长期技术维护或远程问题排查?
我是拥有 15+ 年经验的 PHP / Go 后端工程师,长期关注已有系统维护、Bug 修复、性能优化、服务器排查、WordPress 网站维护和小功能迭代。
如果你的项目遇到以下情况,可以先从一次小问题排查开始合作:
- ✅ PHP / Laravel / Yii2 老项目无人维护
- ✅ Go / Gin 后端接口需要排查或优化
- ✅ WordPress 网站访问慢、报错或插件冲突
- ✅ Nginx / MySQL / Redis / Linux 服务器异常
- ✅ CDN / Cloudflare / DNS / HTTPS 配置问题
- ✅ 需要长期远程技术支持或兼职维护
更多介绍请查看:关于我 & 合作
微信:13980074657
邮箱:shuijingwanwq@gmail.com
Telegram:@shuijingwan
GitHub:https://github.com/shuijingwan
