From 451b0c0928a6dbfaeec71856a729c64300895371 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=9F=83=E5=8D=9A=E6=8B=89=E9=85=B1?= Date: Sun, 8 Mar 2026 13:17:58 +0800 Subject: [PATCH 1/2] fix: route plugin API requests through native HTTP Work around the plugin API CORS mismatch seen in the Android WebView. The upstream API currently responds with Access-Control-Allow-Origin: https://localhost while the app runs from http://localhost, which causes fetch-based plugin requests to fail even though the API is reachable. Add helpers.requestJson() so remote JSON requests prefer cordova-plugin-advanced-http instead of a fetch fallback. For the plugin API workload the bridge overhead is negligible compared with network latency, while the native path avoids the WebView CORS policy mismatch entirely. Update checkAPIStatus() to use the shared helper, await the async API status check in the sidebar extension panel, encode plugin search queries, and route plugin list, search, explore, and filtered requests in both plugin entry points through the shared JSON helper. --- src/pages/plugins/plugins.js | 23 ++++++++++------------- src/sidebarApps/extensions/index.js | 27 ++++++++++++--------------- src/utils/helpers.js | 24 +++++++++++++++++++++++- 3 files changed, 45 insertions(+), 29 deletions(-) diff --git a/src/pages/plugins/plugins.js b/src/pages/plugins/plugins.js index 92c5e433b..98a99b860 100644 --- a/src/pages/plugins/plugins.js +++ b/src/pages/plugins/plugins.js @@ -341,16 +341,16 @@ export default function PluginsInclude(updates) { async function searchRemotely(query) { if (!query) return []; + const encodedQuery = encodeURIComponent(query); + const searchUrl = withSupportedEditor( + `${constants.API_BASE}/plugins?name=${encodedQuery}`, + ); try { - const response = await fetch( - withSupportedEditor(`${constants.API_BASE}/plugins?name=${query}`), - ); - const plugins = await response.json(); + const plugins = await helpers.requestJson(searchUrl); // Map the plugins to Item elements and return return plugins.map((plugin) => ); } catch (error) { $list.all.setAttribute("empty-msg", strings["error"]); - window.log("error", "Failed to search remotely:"); window.log("error", error); return []; } @@ -421,21 +421,20 @@ export default function PluginsInclude(updates) { if (filterState.type === "orderBy") { const page = filterState.nextPage || 1; try { - let response; + let items; if (filterState.value === "top_rated") { - response = await fetch( + items = await helpers.requestJson( withSupportedEditor( `${constants.API_BASE}/plugins?explore=random&page=${page}&limit=${LIMIT}`, ), ); } else { - response = await fetch( + items = await helpers.requestJson( withSupportedEditor( `${constants.API_BASE}/plugin?orderBy=${filterState.value}&page=${page}&limit=${LIMIT}`, ), ); } - const items = await response.json(); if (!Array.isArray(items)) { return { items: [], hasMore: false }; } @@ -470,10 +469,9 @@ export default function PluginsInclude(updates) { try { const page = filterState.nextPage; - const response = await fetch( + const data = await helpers.requestJson( withSupportedEditor(`${constants.API_BASE}/plugins?page=${page}&limit=${LIMIT}`), ); - const data = await response.json(); filterState.nextPage = page + 1; if (!Array.isArray(data) || !data.length) { @@ -567,10 +565,9 @@ export default function PluginsInclude(updates) { $list.all.setAttribute("empty-msg", strings["loading..."]); - const response = await fetch( + const newPlugins = await helpers.requestJson( withSupportedEditor(`${constants.API_BASE}/plugins?page=${currentPage}&limit=${LIMIT}`), ); - const newPlugins = await response.json(); if (newPlugins.length < LIMIT) { hasMore = false; diff --git a/src/sidebarApps/extensions/index.js b/src/sidebarApps/extensions/index.js index 83545cdcf..a027d6efa 100644 --- a/src/sidebarApps/extensions/index.js +++ b/src/sidebarApps/extensions/index.js @@ -145,12 +145,11 @@ async function loadMorePlugins() { isLoading = true; startLoading($explore); - const response = await fetch( + const newPlugins = await helpers.requestJson( withSupportedEditor( `${constants.API_BASE}/plugins?page=${currentPage}&limit=${LIMIT}`, ), ); - const newPlugins = await response.json(); if (newPlugins.length < LIMIT) { hasMore = false; @@ -218,7 +217,7 @@ async function searchPlugin() { $searchResult.onscroll = null; $searchResult.content = ""; - const status = helpers.checkAPIStatus(); + const status = await helpers.checkAPIStatus(); if (!status) { $searchResult.content = ( {strings.api_error} @@ -228,14 +227,15 @@ async function searchPlugin() { const query = this.value; if (!query) return; + const encodedQuery = encodeURIComponent(query); try { $searchResult.classList.add("loading"); - const plugins = await fsOperation( + const plugins = await helpers.requestJson( withSupportedEditor( - Url.join(constants.API_BASE, `plugins?name=${query}`), + Url.join(constants.API_BASE, `plugins?name=${encodedQuery}`), ), - ).readFile("json"); + ); installedPlugins = await listInstalledPlugins(); $searchResult.content = plugins.map(ListItem); @@ -409,7 +409,7 @@ async function loadInstalled() { async function loadExplore() { if (this.collapsed) return; - const status = helpers.checkAPIStatus(); + const status = await helpers.checkAPIStatus(); if (!status) { $explore.$ul.content = {strings.api_error}; return; @@ -420,12 +420,11 @@ async function loadExplore() { currentPage = 1; hasMore = true; - const response = await fetch( + const plugins = await helpers.requestJson( withSupportedEditor( `${constants.API_BASE}/plugins?page=${currentPage}&limit=${LIMIT}`, ), ); - const plugins = await response.json(); if (plugins.length < LIMIT) { hasMore = false; @@ -463,21 +462,20 @@ async function getFilteredPlugins(filterState) { if (filterState.type === "orderBy") { const page = filterState.nextPage || 1; try { - let response; + let items; if (filterState.value === "top_rated") { - response = await fetch( + items = await helpers.requestJson( withSupportedEditor( `${constants.API_BASE}/plugins?explore=random&page=${page}&limit=${LIMIT}`, ), ); } else { - response = await fetch( + items = await helpers.requestJson( withSupportedEditor( `${constants.API_BASE}/plugin?orderBy=${filterState.value}&page=${page}&limit=${LIMIT}`, ), ); } - const items = await response.json(); if (!Array.isArray(items)) { return { items: [], hasMore: false }; } @@ -512,12 +510,11 @@ async function getFilteredPlugins(filterState) { try { const page = filterState.nextPage; - const response = await fetch( + const data = await helpers.requestJson( withSupportedEditor( `${constants.API_BASE}/plugins?page=${page}&limit=${LIMIT}`, ), ); - const data = await response.json(); filterState.nextPage = page + 1; if (!Array.isArray(data) || !data.length) { diff --git a/src/utils/helpers.js b/src/utils/helpers.js index a925bfabf..6924bf527 100644 --- a/src/utils/helpers.js +++ b/src/utils/helpers.js @@ -344,9 +344,31 @@ export default { func(...args, resolve, reject); }); }, + // Plugin API requests are all remote HTTPS calls. For this workload, the + // JS-to-native bridge cost is negligible compared to network latency, so we + // use native HTTP unconditionally instead of keeping a fetch fast path. + // This also avoids the WebView CORS mismatch that caused false + // "API unavailable" failures against the plugin API. + async requestJson(url, headers = {}) { + const nativeHttp = window.cordova?.plugin?.http; + if (typeof nativeHttp?.get !== "function") { + throw new Error("Native HTTP plugin is unavailable"); + } + + const response = await new Promise((resolve, reject) => { + nativeHttp.get(url, {}, headers, resolve, reject); + }); + + if (typeof response?.data === "string") { + return JSON.parse(response.data); + } + + return response?.data; + }, async checkAPIStatus() { + const statusUrl = Url.join(constants.API_BASE, "status"); try { - const { status } = await ajax.get(Url.join(constants.API_BASE, "status")); + const { status } = await this.requestJson(statusUrl); return status === "ok"; } catch (error) { window.log("error", error); From f6e5d521a31ba54a4564d3020c5ea2f0f1564a9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=9F=83=E5=8D=9A=E6=8B=89=E9=85=B1?= Date: Sun, 8 Mar 2026 16:16:31 +0800 Subject: [PATCH 2/2] fix: improve plugin API request diagnostics Attach endpoint context to native HTTP JSON parsing failures so malformed 200 responses from the plugin API no longer surface as context-free SyntaxError exceptions. Restore actionable logging for remote plugin search failures by including both the query text and resolved search URL in the error path. Keep the plugin API transport strictly on the native HTTP path and document why fetch fallback is intentionally rejected in this codepath, since the WebView CORS mismatch is the bug this branch exists to avoid. Record the current npm lockfile state produced by the branch after dependency installation so the submodule remains internally consistent. --- package-lock.json | 32 ++++++++++++++++++++++++++------ src/pages/plugins/plugins.js | 6 +++++- src/utils/helpers.js | 13 ++++++++++++- 3 files changed, 43 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 889339b63..dbbd8b82c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -153,6 +153,7 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -1978,6 +1979,7 @@ "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.0.tgz", "integrity": "sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg==", "license": "MIT", + "peer": true, "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", @@ -1990,6 +1992,7 @@ "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.2.tgz", "integrity": "sha512-vvX1fsih9HledO1c9zdotZYUZnE4xV0m6i3m25s5DIfXofuprk6cRcLUZvSk3CASUbwjQX21tOGbkY2BH8TpnQ==", "license": "MIT", + "peer": true, "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.4.0", @@ -2026,6 +2029,7 @@ "resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.1.tgz", "integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==", "license": "MIT", + "peer": true, "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.0.0", @@ -2052,6 +2056,7 @@ "resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.11.tgz", "integrity": "sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw==", "license": "MIT", + "peer": true, "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/lang-css": "^6.0.0", @@ -2279,6 +2284,7 @@ "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.1.tgz", "integrity": "sha512-Fa6xkSiuGKc8XC8Cn96T+TQHYj4ZZ7RdFmXA3i9xe/3hLHfwPZdM+dqfX0Cp0zQklBKhVD8Yzc8LS45rkqcwpQ==", "license": "MIT", + "peer": true, "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.23.0", @@ -2371,6 +2377,7 @@ "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.4.tgz", "integrity": "sha512-8y7xqG/hpB53l25CIoit9/ngxdfoG+fx+V3SHBrinnhOtLvKHRyAJJuHzkWrR4YXXLX8eXBsejgAAxHUOdW1yw==", "license": "MIT", + "peer": true, "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } @@ -2392,6 +2399,7 @@ "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.39.15.tgz", "integrity": "sha512-aCWjgweIIXLBHh7bY6cACvXuyrZ0xGafjQ2VInjp4RM4gMfscK5uESiNdrH0pE+e1lZr2B4ONGsjchl2KsKZzg==", "license": "MIT", + "peer": true, "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", @@ -4204,6 +4212,7 @@ "integrity": "sha512-Iax6UhrfZqJajA778c1d5DBFbSIqPOSrI34kpNIiNpWd8Jq7mFIa+Z60SQb5ZQDZuUxcCZikjz5BxinFjTkg7Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@module-federation/runtime-tools": "0.22.0", "@rspack/binding": "1.7.6", @@ -4493,8 +4502,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/markdown-it": { "version": "14.1.2", @@ -4511,8 +4519,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/mime": { "version": "1.3.5", @@ -4929,7 +4936,8 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz", "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@xtuc/ieee754": { "version": "1.2.0", @@ -5006,6 +5014,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5054,6 +5063,7 @@ "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -5570,6 +5580,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -5732,6 +5743,7 @@ "integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@chevrotain/cst-dts-gen": "11.0.3", "@chevrotain/gast": "11.0.3", @@ -8965,6 +8977,7 @@ "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz", "integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==", "license": "MIT", + "peer": true, "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", @@ -10063,6 +10076,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -10183,6 +10197,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -10326,6 +10341,7 @@ "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -10596,6 +10612,7 @@ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -12022,7 +12039,8 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "dev": true, - "license": "0BSD" + "license": "0BSD", + "peer": true }, "node_modules/tuf-js": { "version": "4.1.0", @@ -12090,6 +12108,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -12392,6 +12411,7 @@ "integrity": "sha512-dRXm0a2qcHPUBEzVk8uph0xWSjV/xZxenQQbLwnwP7caQCYpqG1qddwlyEkIDkYn0K8tvmcrZ+bOrzoQ3HxCDw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", diff --git a/src/pages/plugins/plugins.js b/src/pages/plugins/plugins.js index 98a99b860..b10d4d19d 100644 --- a/src/pages/plugins/plugins.js +++ b/src/pages/plugins/plugins.js @@ -351,7 +351,11 @@ export default function PluginsInclude(updates) { return plugins.map((plugin) => ); } catch (error) { $list.all.setAttribute("empty-msg", strings["error"]); - window.log("error", error); + window.log( + "error", + `Remote plugin search failed for query "${query}" at URL "${searchUrl}"`, + error, + ); return []; } } diff --git a/src/utils/helpers.js b/src/utils/helpers.js index 6924bf527..1368c5859 100644 --- a/src/utils/helpers.js +++ b/src/utils/helpers.js @@ -352,6 +352,10 @@ export default { async requestJson(url, headers = {}) { const nativeHttp = window.cordova?.plugin?.http; if (typeof nativeHttp?.get !== "function") { + // Do not fall back to fetch here: these callers exist specifically to + // bypass the Android WebView CORS mismatch for the plugin API. Missing + // native HTTP support is a runtime/configuration error, not a case for + // silently re-enabling the broken transport path. throw new Error("Native HTTP plugin is unavailable"); } @@ -360,7 +364,14 @@ export default { }); if (typeof response?.data === "string") { - return JSON.parse(response.data); + try { + return JSON.parse(response.data); + } catch (error) { + const bodyPreview = response.data.slice(0, 200); + throw new Error( + `Failed to parse JSON response from ${url}: ${bodyPreview}`, + ); + } } return response?.data;