koala73/worldmonitor · error · Error
Sentry pagination cursor left ${SENTRY_HOST}: ${new URL(next
Error message
Sentry pagination cursor left ${SENTRY_HOST}: ${new URL(next).origin} What it means
Sentry echoes the next-page cursor back as a URL in a response header, and the script then sends the bearer token to that URL. To prevent a malicious or compromised header from leaking the token to another host, sameOriginCursor pins the cursor to the origin of SENTRY_HOST and throws if the cursor's origin differs.
Solutions
- Set SENTRY_HOST so its origin exactly matches the origin in the returned cursor URL (scheme, host, and port), e.g. both https://sentry.io.
- Inspect the Link header from the failing response (curl -i) to see which origin Sentry echoes and align SENTRY_HOST with it.
- Fix proxy/load-balancer header rewriting so the echoed cursor keeps the public origin.
- Never disable the check — it protects the bearer token; change SENTRY_HOST, not the validation.
Example fix
// before SENTRY_HOST=http://sentry.internal.example // after SENTRY_HOST=https://sentry.internal.example # must match the cursor's origin exactly
Defensive patterns
Strategy: validation
Validate before calling
const next = extractCursor(response.headers.get('Link'));
if (next && new URL(next).origin !== new URL(SENTRY_HOST).origin) {
throw new Error(`Cursor origin mismatch: ${new URL(next).origin}`);
} Type guard
const isSameOriginCursor = (next) => { try { return new URL(next).origin === new URL(SENTRY_HOST).origin; } catch { return false; } }; Try / catch
try {
url = sameOriginCursor(next);
} catch (err) {
if (err.message.includes('pagination cursor left')) {
console.error(`Refusing cross-origin cursor (token-exfiltration guard): ${err.message}. Align SENTRY_HOST with the echoed origin.`);
process.exitCode = 1;
} else throw err;
} Prevention
- Configure SENTRY_HOST with the exact scheme+host+port Sentry echoes in its Link header.
- Check proxy/load-balancer rewriting of Link headers when self-hosting.
- Never follow cursor URLs to other hosts — the bearer token would be sent with them.
- Wrap new URL() parsing in try/catch when handling untrusted header values.
When it happens
Trigger: Calling sameOriginCursor with a `next` URL whose origin (scheme+host+port) differs from SENTRY_HOST — e.g. the Link header points at http:// while SENTRY_HOST is https://, a different subdomain or mirror host, or a URL that new URL() resolves to another origin.
Common situations: SENTRY_HOST is overridden to a self-hosted Sentry whose pagination headers still advertise sentry.io; a reverse proxy or load balancer rewrites the Link header to an internal hostname; an http-vs-https scheme mismatch between the configured host and the echoed cursor.
Understand the failure class
Background: "Invalid URL" / "URL cannot be empty": fix the malformed or missing URL behind request-construction failures — this error's family across 50 libraries.
Related errors
- serverUrl hostname is blocked
- Sentry issues pagination exceeded ${MAX_PAGES} pages
- UNTRUSTED_SOURCE_HOST
- callbackUrl is not a valid URL
- callbackUrl hostname is a blocked metadata endpoint
AI-assisted analysis of koala73/worldmonitor@7d06c8633d (2026-09-15).
Data as JSON: /api/errors/ef8aaf80fa6ecbcc.
Report an issue: GitHub.
Appendix: source
Thrown at scripts/audit-sentry-resolve-pins.mjs:214
allowPositionals: false,
strict: true,
});
return values;
}
export function issuesUrl(org, project) {
const url = new URL(`${SENTRY_HOST}/api/0/projects/${org}/${project}/issues/`);
url.searchParams.set('query', 'is:resolved');
url.searchParams.set('limit', String(PAGE_SIZE));
url.searchParams.set('statsPeriod', '');
return url.toString();
}
// The cursor URL is echoed straight back from a response header and is then
// sent the bearer token, so it is pinned to the host we chose to trust.
function sameOriginCursor(next) {
if (new URL(next).origin !== new URL(SENTRY_HOST).origin) {
throw new Error(`Sentry pagination cursor left ${SENTRY_HOST}: ${new URL(next).origin}`);
}
return next;
}
export async function fetchResolvedIssues(token, org, project, fetchImpl = globalThis.fetch) {
const issues = [];
let url = issuesUrl(org, project);
for (let page = 0; page < MAX_PAGES; page += 1) {
const response = await fetchImpl(url, {
headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' },
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
});
if (response.status === 403) {
throw new Error(
'Sentry returned 403 for the issues endpoint. A release-scoped `sntrys_` upload '
+ 'token cannot read issues, no matter which project it was minted for. Export a '
+ '`sntryu_` user token carrying `event:read` and `project:read` as SENTRY_AUTH_TOKEN 'View on GitHub (pinned to 7d06c8633d)