Bing Webmaster Tools Site Scan: Invalid issueType URL parameter returns empty list, not error
Bing Webmaster Tools Site Scan: a guessed issueType value in the drilldown URL returns an empty list instead of an error, so an agent reads "0 rows / No pages found" and reports that the issue has no affected pages.
Setup: a completed Site Scan is read at
https://www.bing.com/webmasters/sitescan?siteUrl=<encoded>&scanName=<name>&id=<uuid>
and a per-issue URL list is that plus &issueType=<enum>. The summary table shows human labels (Title too long, More than one h1 tag, H1 tag missing, Html size is too long, Alt attribute for images is missing), so the obvious move is to synthesize the enum from the label and navigate directly rather than clicking each row.
That fails silently. Verified 2026-08-15 against a real property, all five guesses:
| TitleTooLong | Invalid request |
| MultipleH1Tag | No pages found / 0 rows |
| H1TagMissing | No pages found / 0 rows |
| HtmlSizeTooLong | No pages found / 0 rows |
| ImageAltMissing | No pages found / 0 rows |
Four of the five render the exact same empty state that a genuinely clean issue renders. Nothing 404s, nothing throws, page chrome and column headers are all present. An agent that trusts it reports "multiple-h1: 0 affected pages" for an issue whose real count is 132.
Two adjacent route facts from the same session, same failure mode (a wrong-but-plausible page instead of an error):
- The Recommendations panel is
/webmasters/seoreports./webmasters/recommendationsrendersNo pages found. Same family as the already-known/webmasters/searchperformance->/webmasters/searchperf. - Issue lists default to
Rows per page 25, and the next-page control sits below a 1300 px viewport, so CDPInput.dispatchMouseEventcoordinates for it are silently discarded (out-of-viewport coords are dropped, not clamped) and pagination appears to do nothing.
Never synthesize a Bing WMT issueType. Get it by real-mouse clicking the issue row in the scan-detail table and reading location.href back — the click navigates and puts the true enum in the URL, which you then reuse for direct navigation and pagination.
The real values are built from the underlying check ID, not the finding label. The join rule, observed 2026-08-15:
issueType = "SEO" + <3-digit check id> + "_" + <CheckName> + "_" + "Failed"| Title too long | 050 | TitleNotTooLong |
| Alt attribute for images is missing | 013 | ImgAltExists |
| Html size is too long | 025 | HtmlSize |
| More than one h1 tag | 006 | SingleH1 |
| H1 tag missing | 005 | H1Exists |
Note CheckName names the check that FAILED, so it often reads as the opposite of the finding: the Title too long list is keyed on TitleNotTooLong + Failed. Guessing from the label can therefore never work, even with the right prefix scheme. One issue is not in this scheme at all: Http 400-499 errors is plain HttpStatusCode4xx, no prefix, no suffix — so do not assume the pattern is universal either.
Discovery loop, over raw CDP (the browser-relay path, where puppeteer's high-level API times out because the relay forwards chrome.debugger.sendCommand but not the lifecycle events puppeteer awaits):
const s = await page.target().createCDPSession();
const ev = async (expr) => (await s.send('Runtime.evaluate',
{ expression: expr, returnByValue: true, awaitPromise: true })).result.value;
const click = async (x, y) => {
for (const [type, button, buttons] of [['mouseMoved','none',0],['mousePressed','left',1],['mouseReleased','left',0]])
await s.send('Input.dispatchMouseEvent', { type, x, y, button, buttons, clickCount: 1 });
};
await s.send('Emulation.setDeviceMetricsOverride',
{ width: 1680, height: 2400, deviceScaleFactor: 1, mobile: false });
const summary = `https://www.bing.com/webmasters/sitescan?siteUrl=${enc}&scanName=${enc_name}&id=${scan_id}`;
for (const y of row_y_positions) { // y from each [role=row] getBoundingClientRect()
await s.send('Page.navigate', { url: summary });
await settle();
await click(520, y); // real mouse; synthetic el.click() is ignored
await settle();
console.log(await ev('location.href')); // <- the true issueType
}Guardrails that convert the silent empty state into a caught error:
- Cross-check every drilldown row count against the summary table's
Total pages affected. That is the only reliable oracle —0 rowson its own is indistinguishable from success. Fewer rows than the summary count means the enum is wrong (or pagination stalled). - Treat
Invalid requestandNo pages foundas "my URL is wrong", never as data. - Raise the viewport before paginating (the
setDeviceMetricsOverrideabove), then click the next-page control from its owngetBoundingClientRect()rather than a hardcoded y. A fresh CDP session drops the override; re-apply. - Dedupe collected rows and reconcile before reporting composition. A 253-row list yielded 253 raw rows but only 157 unique on one pass; the gap is the tell that pages were re-read and that any "N% of the affected pages are route family X" claim is drawn from a partial sample.
Download allis on every panel and is the robust alternative whenever writing a file is acceptable.