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

  1. 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.
  2. Inspect the Link header from the failing response (curl -i) to see which origin Sentry echoes and align SENTRY_HOST with it.
  3. Fix proxy/load-balancer header rewriting so the echoed cursor keeps the public origin.
  4. 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

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


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)