From 880f7044d4b05cd2febca9697411eecb2f96475c Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 7 Nov 2026 17:04:31 +0000 Subject: [PATCH] Fix connection to Part-DB instances on non-standard ports in Firefox Firefox does not support port numbers in host permission match patterns, so the requested pattern (e.g. http://192.168.1.10:8080/*) never matched and all requests failed with "NetworkError when attempting to fetch resource". Request the permission for the host without port instead, which works in both Chrome and Firefox. Also request permissions for all configured instances in a single permissions.request() call, as Firefox only allows it directly inside a user input handler, and show a clearer error in the popup when host permission is missing. See https://github.com/Part-DB/Part-DB-server/discussions/1525 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017TWRopcP8FAsxdixm9p4Hu --- README.md | 2 +- src/_locales/de/messages.json | 4 +++- src/_locales/en/messages.json | 4 +++- src/background/service-worker.js | 13 +++++++++++-- src/options/options.js | 24 +++++++++++++++++------- src/popup/popup.js | 4 ++++ src/shared/host-storage.js | 21 +++++++++++++++++++++ 7 files changed, 60 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 48d9ac5..97b4378 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ If you configure more than one instance, the popup shows an **Instance** dropdow | `storage` | Required | Saves your configured Part-DB instances and settings | | `activeTab` | Required | Reads the page HTML on demand when you click Submit | | `scripting` | Required | Executes the page-reading code in the active tab | -| Host access to your Part-DB URL(s) | Optional — granted when you save a server URL | Required to reach your Part-DB instance(s); scoped to exactly the URLs you configure | +| Host access to your Part-DB URL(s) | Optional — granted when you save a server URL | Required to reach your Part-DB instance(s); scoped to the hosts of the URLs you configure (all ports of that host, as Firefox does not support ports in host permissions) | No host permissions are requested at install time. The extension has no passive presence on web pages — it reads page content only at the moment you click Submit. diff --git a/src/_locales/de/messages.json b/src/_locales/de/messages.json index 42f86f9..35be741 100644 --- a/src/_locales/de/messages.json +++ b/src/_locales/de/messages.json @@ -207,5 +207,7 @@ }, "options_error_permission_denied": { "message": "Browser-Berechtigung für diese URL wurde verweigert. Die Erweiterung benötigt sie, um Ihren Part-DB-Server zu erreichen." - } + }, + "popup_error_missing_host_permission": {"message": "Die Erweiterung darf nicht auf diesen Part-DB-Server zugreifen. Öffnen Sie die Einstellungen und klicken Sie auf „Einstellungen speichern“, um die Berechtigung erneut zu erteilen."}, + "options_error_permission_failed": {"message": "Browser-Berechtigung konnte nicht angefordert werden: $ERROR$", "placeholders": {"error": {"content": "$1", "example": "Invalid pattern"}}} } diff --git a/src/_locales/en/messages.json b/src/_locales/en/messages.json index 6686f13..8b9dd71 100644 --- a/src/_locales/en/messages.json +++ b/src/_locales/en/messages.json @@ -207,5 +207,7 @@ }, "options_error_permission_denied": { "message": "Browser permission for this URL was denied. The extension needs it to reach your Part-DB server." - } + }, + "popup_error_missing_host_permission": {"message": "The extension is not allowed to access this Part-DB server. Open the settings and click “Save Settings” to grant the permission again."}, + "options_error_permission_failed": {"message": "Could not request browser permission: $ERROR$", "placeholders": {"error": {"content": "$1", "example": "Invalid pattern"}}} } diff --git a/src/background/service-worker.js b/src/background/service-worker.js index 5af165d..7044413 100644 --- a/src/background/service-worker.js +++ b/src/background/service-worker.js @@ -51,7 +51,7 @@ async function handleGetInfo(hostId) { await chrome.storage.local.set({ infoCacheByHost }); return { success: true, data }; } catch (err) { - return { success: false, error: 'network_error', message: err.message }; + return networkErrorResult(baseUrl, err); } } @@ -99,6 +99,15 @@ async function handleSubmit(hostId, html, url, title, provider) { } return { success: true }; } catch (err) { - return { success: false, error: 'network_error', message: err.message }; + return networkErrorResult(baseUrl, err); } } + +async function networkErrorResult(baseUrl, err) { + // Without host permission the browser blocks the request and only reports a generic network error. + // Detect this case to give the user an actionable hint. + if (!(await hasHostPermission(baseUrl))) { + return { success: false, error: 'missing_host_permission' }; + } + return { success: false, error: 'network_error', message: err.message }; +} diff --git a/src/options/options.js b/src/options/options.js index 062908f..ce6c41a 100644 --- a/src/options/options.js +++ b/src/options/options.js @@ -64,14 +64,24 @@ function addHostRow(host, { focus = false } = {}) { return rowEl; } -async function requestHostPermission(url) { - let origin; +/** + * Requests host permissions for all given URLs with a single permissions.request() call. + * This must be called synchronously from the user input handler (before any await), as Firefox only allows + * permissions.request() while handling a user action, and that context is lost after the first await. + */ +async function requestHostPermissions(urls) { + let origins; try { - origin = new URL(url).origin + '/*'; + origins = [...new Set(urls.map(hostPermissionPattern))]; } catch { return { ok: false, error: chrome.i18n.getMessage('options_error_invalid_url') }; } - const granted = await chrome.permissions.request({ origins: [origin] }); + let granted; + try { + granted = await chrome.permissions.request({ origins }); + } catch (err) { + return { ok: false, error: chrome.i18n.getMessage('options_error_permission_failed', [err.message]) }; + } if (!granted) { return { ok: false, error: chrome.i18n.getMessage('options_error_permission_denied') }; } @@ -100,7 +110,7 @@ async function testHostRow(rowEl) { return; } - const perm = await requestHostPermission(url); + const perm = await requestHostPermissions([url]); if (!perm.ok) { showRowTestResult(rowEl, 'error', perm.error); return; @@ -155,8 +165,8 @@ async function saveAllSettings() { newHosts.push(host); } - for (const host of newHosts) { - const perm = await requestHostPermission(host.url); + if (newHosts.length > 0) { + const perm = await requestHostPermissions(newHosts.map((h) => h.url)); if (!perm.ok) { showSaveStatus('error', perm.error); return; diff --git a/src/popup/popup.js b/src/popup/popup.js index 0cdf023..64b5ca0 100644 --- a/src/popup/popup.js +++ b/src/popup/popup.js @@ -162,6 +162,8 @@ function infoErrorMessage(result) { switch (result.error) { case 'network_error': return chrome.i18n.getMessage('popup_error_network', [result.message || 'network error']); + case 'missing_host_permission': + return chrome.i18n.getMessage('popup_error_missing_host_permission'); case 'no_permission': return chrome.i18n.getMessage('popup_error_no_permission'); case 'feature_disabled': @@ -244,6 +246,8 @@ function errorMessage(result) { return chrome.i18n.getMessage('popup_error_page_too_large_limit'); case 'network_error': return chrome.i18n.getMessage('popup_error_network', [result.message || 'network error']); + case 'missing_host_permission': + return chrome.i18n.getMessage('popup_error_missing_host_permission'); case 'no_permission': return chrome.i18n.getMessage('popup_error_no_permission'); case 'feature_disabled': diff --git a/src/shared/host-storage.js b/src/shared/host-storage.js index 6c58466..aa43af2 100644 --- a/src/shared/host-storage.js +++ b/src/shared/host-storage.js @@ -61,3 +61,24 @@ async function getLastHostId() { async function setLastHostId(id) { await chrome.storage.local.set({ lastHostId: id }); } + +/** + * Builds the host permission match pattern for a Part-DB base URL. + * + * The port is deliberately left out: Firefox does not support ports in match patterns, so a pattern like + * "http://192.168.1.10:8080/*" never matches any request there, and fetches to Part-DB instances on + * non-standard ports fail with "NetworkError when attempting to fetch resource". + * A pattern without a port matches every port of that host in both Chrome and Firefox. + */ +function hostPermissionPattern(url) { + const parsed = new URL(url); + return `${parsed.protocol}//${parsed.hostname}/*`; +} + +async function hasHostPermission(url) { + try { + return await chrome.permissions.contains({ origins: [hostPermissionPattern(url)] }); + } catch { + return false; + } +}