gildas-lormeau/SingleFile · error · Error

Unauthorized sender

Error message

Unauthorized sender

What it means

SingleFile's background message handler for external capture (onMessage in external-capture-permissions.js) only accepts messages originating from the extension's own options page. If the sender's tab/url does not match the options page, it throws 'Unauthorized sender'. This guards the permission-setting API from being invoked by arbitrary web pages or other extension contexts.

Source

Thrown at src/core/bg/external-capture-permissions.js:68

		throw new Error("Cannot identify the extension requesting SingleFile capture");
	}
	const permissions = await config.getExternalCapturePermissions();
	if (permissions.allowedExtensionIds.includes(extensionId)) {
		return true;
	}
	if (permissions.deniedExtensionIds.includes(extensionId)) {
		return false;
	}
	const { request, created } = getOrCreatePendingRequest(extensionId, sender, message);
	if (created) {
		await openOptionsPage(request);
	}
	return request.promise;
}

async function onMessage(message, sender) {
	if (!isOptionsPageSender(sender)) {
		throw new Error("Unauthorized sender");
	}
	if (message.method.endsWith(".getPermissions")) {
		return config.getExternalCapturePermissions();
	}
	if (message.method.endsWith(".setPermissions")) {
		await config.setExternalCapturePermissions(message.permissions);
		return {};
	}
	if (message.method.endsWith(".getPendingRequest")) {
		return getPendingRequest(message.requestId);
	}
	if (message.method.endsWith(".respondPendingRequest")) {
		return respondPendingRequest(message.requestId, message.approved);
	}
}

function isOptionsPageSender(sender) {
	return Boolean(sender) && sender.id == browser.runtime.id &&

View on GitHub (pinned to 517fb7c5cf)

Solutions

  1. Send the message from the extension's options page context (or open the options page and run the call there).
  2. If calling programmatically, obtain the options page's tab and use browser.tabs.sendMessage to it so isOptionsPageSender passes.
  3. In tests, construct a sender object matching what isOptionsPageSender expects (correct url/id fields).

Example fix

// before (from a content script)
browser.runtime.sendMessage({ method: 'externalCapture.setPermissions', permissions: [...] });
// after (from the options page itself)
const tabs = await browser.tabs.query({ url: browser.runtime.getURL('options/index.html') });
await browser.tabs.sendMessage(tabs[0].id, { method: 'externalCapture.setPermissions', permissions: [...] });
Defensive patterns

Strategy: validation

Validate before calling

function isOptionsPageSender(sender) {
  const optionsUrl = browser.runtime.getURL('options/index.html');
  return sender && sender.tab && sender.url && sender.url.startsWith(optionsUrl);
}
// only sendMessage when this returns true

Type guard

function isFromOptionsPage(sender) {
  return typeof sender?.url === 'string' &&
    sender.url.startsWith(browser.runtime.getURL('options/'));
}

Try / catch

try {
  await browser.runtime.sendMessage({ method: 'externalCapture.setPermissions', permissions });
} catch (e) {
  if (e.message === 'Unauthorized sender') {
    // move the call into the options page context
  }
}

Prevention

When it happens

Trigger: Calling browser.runtime.sendMessage from a context whose sender is not the options page (e.g. a content script, popup, or another extension) with a method ending in .getPermissions or .setPermissions.

Common situations: Developers wiring up external capture automation call setPermissions from a script or console in the wrong context; tests invoke the handler directly with a fabricated sender object lacking options-page URL/id.

Understand the failure class

Related errors


AI-assisted analysis of gildas-lormeau/SingleFile@517fb7c5cf (2026-09-01). Data as JSON: /api/errors/50758e33c9a5b259. Report an issue: GitHub.