DSH plugin: file previews break on legacy Chromium engines
In the DSH Web GUI, every single file preview — sidebar Files, file links in chat, the deliverables list — says "file resource service unavailable." DevTools shows no /api/workspaceFiles/* request at all, the host-side logs are clean, and refreshing does not recover it. This is not a broken backend file service: in the same session the host RPCs (workspaceFiles/list, stat, readAll) all return normally. The real break is on the client — protocolOf() in @deepseek-ai/dsh-client-resources uses new URL(address).hostname to read the protocol name of a non-special scheme address like dsh-resource://, and whether the //authority of a non-special scheme counts as a host depends on the browser's implementation of a 2024 WHATWG URL standard change (whatwg/url#731); on Chromium engines that are older or have had URL parsing trimmed, hostname is an empty string, so the address is judged to have "no provider" and the preview never sends a request (#6217, #6437). This article proceeds as "triage first → root cause → three fixes (pure string parsing / fallback that keeps the fast path / decouple the amplifier) → troubleshooting notes", with pasteable code and a one-line reproduction at every step.
Triage first: this is not "the backend file service is broken"
Confirm three things first — whether the RPCs work, whether the console has errors, and whether another browser recovers. The combination points straight at client-side address parsing, not the server.
| Criterion | Client address-parsing incompatibility | Backend file service genuinely broken |
|---|---|---|
Host RPC workspaceFiles/list / stat / readAll | all ok | errors or timeouts |
/api/workspaceFiles/* in DevTools Network | not a single one | requests present and failing |
| Host / server logs | no related records at all | 4xx/5xx or exception stacks |
| Open the same page in a newer engine | recovers | still fails |
| Console errors | clean (this path throws nothing) | usually has errors |
| Blast radius | globally consistent across every preview entry (sidebar / chat links / deliverables) | depends on the specific file / endpoint |
Minimal three-step reproduction
- Open the Web GUI with an affected engine (
http://127.0.0.1:3080; the server version is irrelevant — the original thread verified on0.1.5-rc.1). To reproduce an old engine, usenpx @puppeteer/browsers install chrome@125.0.6422.60. - Open any text file — click a
.mdin the sidebar Files, or click a file link in chat. - Observe: the preview area shows "file resource service unavailable"; DevTools Network shows zero
/api/workspaceFilescalls; the console has no errors. The same server / session / account renders fine on Chromium ≥ 130.
One-line identification command (run in the affected page's console)
const u = new URL('dsh-resource://file/session/s1/a.md');
console.log(u.protocol, JSON.stringify(u.hostname), JSON.stringify(u.pathname));
| Environment | Output |
|---|---|
| Faulty engine (Edge 129, etc.) | dsh-resource: "" "//file/session/s1/a.md" |
| Healthy engine (Chromium ≥ 130 / Node) | dsh-resource: "file" "/session/s1/a.md" |
A control experiment that rules out "it is just a dsh-resource special case":
new URL('foo://bar/baz').hostname // faulty engine → ""; pathname → "//bar/baz"
new URL('http://a/b').hostname // faulty engine → "a" (special schemes are fine)
That is, affected engines do not parse //host into hostname for any non-special scheme, while special schemes (http, ws, file) are fine both ways — which completely separates "the browser's parsing divergence for non-special schemes" from "the provider was never registered".
Root cause: protocolOf() bases the protocol key on WHATWG host parsing
In one sentence: protocolOf() needs a "protocol key" (file / chat / …) to look up the provider registry, and it gets it via new URL(...).hostname; for a non-special scheme like dsh-resource:, an old engine does not parse the //authority into hostname, so it gets an empty string, returns undefined, and the entire preview chain breaks there.
The host-side code (packages/client/resources/src/client/resources.ts:56-68):
export function protocolOf(address: string): string | undefined {
let parsed: URL
try {
parsed = new URL(address)
} catch {
return undefined
}
if (parsed.protocol !== `${RESOURCE_SCHEME}:`) return undefined
// A non-special scheme's host is opaque to the URL parser and keeps its case.
return parsed.hostname === '' ? undefined : parsed.hostname.toLowerCase()
}
RESOURCE_SCHEME = 'dsh-resource' (:46), and protocolOf has only one call site in the whole repo (:120). Note the comment on line 67 — it treats "a non-special scheme's host is opaque to the parser" as a universally true premise, while that only holds on engines that have implemented the 2024 change.
The failure chain (every step)
new URL('dsh-resource://file/…').hostname === ""
→ protocolOf(address) === undefined
→ providers.get(undefined) === undefined
→ the record is created as idle("none") (resources.ts:122)
→ the preview branch sees meta.status === "none"
→ t("resourceUnavailable") (ui-sidebar-documentpreview/.../locales.ts:21, TextPreview.tsx:84/179)
→ "file resource service unavailable."
The host never receives a request, so the server logs show nothing unusual — which is exactly why it is hard to chase. All three preview entries (sidebar Files, chat file links, deliverables) share this one registry, so the failure is global, deterministic on refresh, and invisible to the server.
Version boundaries: from "126" to "130", to "the major version is simply unreliable"
The original thread held that Chrome/Edge supported the parsing from 126; it was later corrected to 130 (chromestatus 5201116810182656, appui#1089 measuring a behavior change at v130+), so 126–129 also fail — and the Edge 129 measurement in #6437 is precisely a counterexample. The corrector also noted that whatwg/url#731 is now 410 Gone and cannot be checked by number, though the spec change itself is real.
More importantly, later data points show you cannot divide by Chromium major version alone.
| Report / environment | new URL('dsh-resource://file/…').hostname | Preview |
|---|---|---|
| Chrome 125.0.6422.60 | "" | fails (recovers after patching) |
| Edge 129 | "" (pathname is "//file/…") | fails |
| Chromium 134 (third-party engine, e.g. Cent Browser) | "" | fails |
| Xiaomi Browser (UA Chrome/122) | "" | fails |
| HarmonyOS ArkWeb 7.0.0.105 (UA claims Chrome/144) | "" (same machine's Chrome/Safari return "file") | fails |
| Edge 148 / Chromium 153 / Node 26 | "file" | fine |
YOUKNOWWHOOO's controlled experiment (same server, same session, same client code, same click sequence, only the engine changed) tells this even more clearly:
| Observation | Chromium 134 (third-party engine) | Chromium 153 |
|---|---|---|
new URL('dsh-resource://file/session/<id>/<path>') | hostname="", pathname="//file/session/…" | hostname="file", pathname="/session/…" |
| Resource record for that address | protocol: null, status: "none" | protocol: "file", status: "live" |
| Preview panel | "file resource service unavailable." | renders the file content normally |
| Registered providers | ["file"] | ["file"] |
| Client plugin loading | all active | all active |
Note the second-to-last row: the provider is clearly registered — only the "address → protocol key" step is broken. That also explains the paradox in the same-symptom thread #7514 of "the provider is registered yet it shows none". The HarmonyOS ArkWeb report also found that refresh does not recover: the registry back-attaches a record to recordsOf(protocol) only at the moment the provider registers, and a record whose protocol is undefined is never among them.
Fix one: pure string parsing, no dependence on the URL parser at all
Idea: the protocol key is simply "the first segment after the dsh-resource:// prefix", so split the string directly. It is semantically equivalent (an empty authority still returns undefined, a non-dsh-resource: still returns undefined) and is from then on immune to any engine's URL-parsing timeline.
function protocolOf(address) {
const match = /^dsh-resource:\/\/([^/?#]+)/i.exec(address);
return match === null ? void 0 : match[1].toLowerCase();
}
A trap you must watch for: case sensitivity
The original thread first gave an address.startsWith("dsh-resource://") form — startsWith is case-sensitive and would break the existing unit test resources.client.spec.ts:98 (which pins that DSH-RESOURCE://File/... should also yield 'file'). So either use the regex with the i flag above, or toLowerCase() first and then compare — do not copy the earliest startsWith snippet verbatim.
Mide69 has implemented it as "case-insensitive + strips userinfo" and pushed a branch: Mide69/deepseek-harness fix/resource-protocol-legacy-url-hosts. Ownership of the repo is his; this article gives the fixing idea, and whether it merges is up to upstream.
Verification (original thread, local)
After applying that snippet to a 0.1.5-rc.1 deployment, driving the real GUI with a headless browser:
- Chrome 125.0.6422.60: before the fix every preview surface showed "file resource service unavailable."; after the fix text / code / markdown previews render normally (
workspaceFiles.stat+readreturn 200). - Edge 148: no regression (fine before and after).
Fix two: keep the new URL fast path, fall back only when the host is unavailable
Idea: if you do not want to change existing behavior on the vast majority of engines, add only a fallback branch for "the engine cannot get a host" — when the parser works, keep going through parsed.hostname.toLowerCase(), and only when hostname === '' (or parsing throws) fall back to string splitting. Zero impact on existing behavior, minimal change.
The community patch shape (wenbin-wb, against protocolOf):
export function protocolOf(address: string): string | undefined {
- let parsed: URL
+ let parsed: URL | undefined
try {
parsed = new URL(address)
} catch {
- return undefined
+ parsed = undefined
}
- if (parsed.protocol !== `${RESOURCE_SCHEME}:`) return undefined
- // A non-special scheme's host is opaque to the URL parser and keeps its case.
- return parsed.hostname === '' ? undefined : parsed.hostname.toLowerCase()
+ if (parsed !== undefined) {
+ if (parsed.protocol !== `${RESOURCE_SCHEME}:`) return undefined
+ if (parsed.hostname !== '') return parsed.hostname.toLowerCase()
+ }
+ return hostOf(address)
+}
+
+/** An opaque host: anything else is a parse error for the URL parser too. */
+const OPAQUE_HOST = /^[A-Za-z0-9._~!$&'()*+,;=%-]+$/
+
+function hostOf(address: string): string | undefined {
+ const prefix = `${RESOURCE_SCHEME}://`
+ if (!address.toLowerCase().startsWith(prefix)) return undefined
+ const rest = address.slice(prefix.length)
+ const end = rest.search(/[/?#]/)
+ const authority = end === -1 ? rest : rest.slice(0, end)
+ const host = authority.slice(authority.lastIndexOf('@') + 1).replace(/:\d*$/, '')
+ if (host === '' || !OPAQUE_HOST.test(host)) return undefined
+ return host.toLowerCase()
+}
Why this way (the patch author's own account)
- Existing semantics are completely unchanged: when the parser works it still goes through
parsed.hostname.toLowerCase(); an address that genuinely has no host (dsh-resource:///no-host,dsh-resource:no-slash) still returnsundefined. - It enters the fallback only when
hostname === '', cleanly separating "the parser cannot get a host" from "the address simply has no host". - The fallback validates against the legal character set for an opaque host and strips userinfo / port, staying consistent with the URL parser's "reject on parse failure" semantics (for example
dsh-resource://a:b/xand a hostname containing spaces still yieldundefined).
Companion unit tests (simulating an engine whose non-special-scheme hostname is always empty)
class QuirkURL extends URL {
get hostname(): string {
return String(this.protocol) === `${RESOURCE_SCHEME}:` ? '' : super.hostname
}
}
vi.stubGlobal('URL', QuirkURL)
onTestFinished(() => { vi.unstubAllGlobals() })
expect(protocolOf('dsh-resource://file/session/s1/home/ys/b.txt')).toBe('file')
expect(protocolOf('DSH-RESOURCE://File/session/s1/a')).toBe('file')
expect(protocolOf('dsh-resource://file:8080/x')).toBe('file')
expect(protocolOf('dsh-resource://user@file/x')).toBe('file')
expect(protocolOf('dsh-resource:///no-host')).toBeUndefined()
expect(protocolOf('dsh-resource:no-slash')).toBeUndefined()
expect(protocolOf('sidebar://guide')).toBeUndefined()
The author transcribed the patched protocolOf line by line into JS and ran it locally: 8 existing upstream spec assertions + 10 "always-empty hostname" engine-simulation assertions + 13 "both paths agree" assertions = 32/32 passing; git apply --check -p1 passes cleanly (the repo's vitest was not run; the new spec needs a CI pass).
Another fallback practice on the same machine
@wenbin_wb/dsh-bridge 2.10.12 has already shipped using "feature detection + patching hostname only for dsh-resource: instances whose native hostname is empty", confirmed recovered on a real HarmonyOS device. In other words, if you cannot change the client package, the bridge/plugin layer can also cover it — a lesson especially useful for readers writing plugins.
Fix three and the second instance: decoupling the amplifier and pathOf's parallel parsing
Fix three: decouple the amplifier — content reading should not be tied to metadata availability
Idea: even with protocolOf fixed, it is worth removing an amplifier while you are there. TextPreview has one line:
const canRead = meta.status !== 'none'
It ties content reading (which goes through remote.workspaceFiles and already has typed failures and retries) to metadata availability. So a small miss like "address parsing did not recognize the protocol" gets amplified into "the entire preview is completely unusable". Decouple the two and the bug degrades to "auto-refresh off" while the content still reads — a one-line change, and valuable defense in depth (#6217).
The reporter's measurement supports this: in the affected engine he applied this decoupling first and the symptom disappeared, and the root-cause patch came later; he recommends keeping this decoupling even after protocolOf is fixed.
The second instance in the same family: pathOf uses new URL().pathname on the same kind of address
The same anti-pattern of "using the URL parser to split a dsh-resource:// address" also appears in the right sidebar tab registry (ui-sidebar-right's tab-registry.ts:189-191):
// Anti-pattern: for a non-special scheme the authority gets counted into pathname
new URL(address).pathname
new URL(address).pathname excludes the authority only when the engine treats the dsh-resource:// authority as a host; on affected engines "/session/…" becomes "//file/session/…", so a glob that matches only the path (like *.png, *.md) will never match a dsh-resource:// address — a second class of silent failure beyond "the preview will not open".
Mide69 fixed this too, pushed on the same branch, and generalized it to any scheme://authority address (pathOf's own contract covers more than dsh-resource:). If your troubleshooting has ever included "some extension-based features mysteriously do not work on old engines", it is worth checking this spot too.
Troubleshooting notes
First principle: the silence of this symptom set is by design — the failure path throws nothing, the host receives no request, and the client logger has no console sink either, so you must proactively run that URL-parsing experiment in the browser rather than waiting for logs.
- Do not trust the server logs: the host never received a request, so of course there is nothing in the logs (#6437, #6217).
- One-line identification: run
new URL('dsh-resource://file/x/y').hostnamein the affected page — an affected engine returns'', Chrome/Safari returns'file'(#6437). - Do not judge by major version alone: third-party / customized engines (Chromium 134, Xiaomi Browser with UA Chrome/122, HarmonyOS ArkWeb) can all be hit, while Chrome on the same machine is fine (#6437).
- Look at the provider registry rather than guessing: when affected,
"file"inprovidersis registered, yet the record for that address hasprotocolofnull/undefinedandstatusstuck atnone(#6437). - Refresh does not recover: a record is back-attached to
recordsOf(protocol)only at the moment the provider registers, and a record whoseprotocolisundefinedis never among them (#6437). - The client's diagnostic capability is itself a trap: the client cordis logger registers only an in-memory ring-buffer exporter (1000 entries), with no console sink; measured, the browser console is clean and the log buffer is
{"total":0,"errs":0}— you have no clue at all while troubleshooting, and can only hand-extract theresourcesservice instance along the React fiber and enumerateprovidersdirectly to locate it. Adding a console exporter in dev builds, or emitting a debug log for "address protocol has no provider", would significantly cut down wasted troubleshooting (#6437). - Temporary workarounds and their cost:
- Switch to a newer-engine Chromium / Edge; or
- Apply fix one as a replacement to
…/dsh-client-resources/lib/client.js. Note DSH is usually launched bynpx @deepseek-ai/dsh, so the code is under%LOCALAPPDATA%\npm-cache\_npx\<hash>\node_modules, and~/.dsh/profiles/node_modules/@deepseek-ai/*are junctions pointing at it — every DSH update overwrites that cache and invalidates the patch (#6437).
- Upstream status: as of the cited reports, both
protocolOfandpathOfare unfixed — in0.1.7-alpha.2,protocolOfindsh-client-resources/lib/client.jsis byte-for-byte identical to the old version;0.1.7-rc.1still reproduces; the version-scan table in#7412can take a row marked "not fixed" (#6217). - Third-party corroboration of the same symptom:
#7514(the paradox of a registered provider showingnone) and#7430(still reproducible on Chromium 126) are consistent with this root-cause family (#6437).
When troubleshooting this kind of problem, use DSH Plugin Hub's installed list to confirm the client plugins are all active (in this issue plugin loading is fine to begin with, which is exactly what lets you rule out the "plugin did not load" layer), check for updates on the settings page, and view the install / update history in the notification center — rule out the plugin layer first, then come back to the browser engine layer.

Source: Discussion #6217, Discussion #6437.
FAQ
Almost certainly not. Do three comparisons first: in the same session, the host RPCs (workspaceFiles/list, stat, readAll) all return normally; in DevTools Network there is **not a single** /api/workspaceFiles/* request; and the host/server logs have no related records. All three holding at once means the request was never sent — the break is in client-side address parsing, not in the server-side file service. Open the same page in a browser with a newer engine and preview the same file to confirm — if it recovers, this root cause is essentially pinned down.
Because this is address-parsing logic in the client package @deepseek-ai/dsh-client-resources, not runtime state. In 0.1.7-alpha.2, protocolOf in dsh-client-resources/lib/client.js is **byte-for-byte identical** to the old version, and 0.1.7-rc.1 still reproduces; the version-scan table in #7412 can take a row marked "not fixed". An upgrade does not change your engine's URL behavior, so the symptom is unchanged.
Run one line in the affected page: new URL('dsh-resource://file/x/y').hostname. An affected engine returns '' (empty string, with pathname being //file/x/y), while Chrome/Safari/newer Chromium returns 'file'. Different engines on the same machine giving different results is the fingerprint of "divergent authority parsing for non-special schemes" (Discussion #6437).
Do not rely on the major version alone. Third-party engines on Chrome 125, Edge 129, and Chromium 134 (such as Cent Browser), Xiaomi Browser (UA Chrome/122), and HarmonyOS ArkWeb 7.0.0.105 (UA claims Chrome/144) have all been hit, while Edge 148 / Chromium 153 / Node 26 are fine. The original thread said Chromium 126 onward supports it, later corrected to 130, but subsequent measurements show that **custom or third-party engines can deviate from that timeline**, so the criterion should be that hostname probe, not the version number (Discussion #6437).
Because DSH is usually launched by npx @deepseek-ai/dsh, the client code is under %LOCALAPPDATA%\npm-cache\_npx\<hash>\node_modules, and ~/.dsh/profiles/node_modules/@deepseek-ai/* are junctions pointing at it. **Every DSH update overwrites that cache, invalidating the patch**, so you must reapply it. For a permanent fix, push upstream to merge fix one / fix two, or — when you cannot change the client package — do a feature-detection fallback in the bridge/plugin layer (Discussion #6437).
Related Terms
- authority parsing for non-special schemes (whatwg/url#731)
- A 2024 change to the URL standard, "Allow non-special schemes to have hosts", makes the `//authority` of a non-special scheme like `dsh-resource:` or `foo:` parse into `hostname`. That change landed at different times on different engines/kernels: older kernels leave the authority in `pathname` (`//file/…`) with an empty `hostname`. The whatwg/url#731 referenced in the original thread is now 410 Gone and cannot be checked by number, but the spec change itself is real.— https://github.com/deepseek-ai/deepseek-harness/discussions/6217
- dsh-resource: and the protocol key
- The unified protocol for resource addresses, `RESOURCE_SCHEME = 'dsh-resource'`, with addresses shaped like `dsh-resource://file/session/<id>/<path>`. The `file` / `chat` segment is the "protocol key", which `ResourceRegistry` uses to look up the corresponding provider via `providers.get(...)`. `protocolOf()`'s sole job is to extract that key from the address.— https://github.com/deepseek-ai/deepseek-harness/discussions/6437
- ResourceRegistry's idle("none")
- When `protocolOf()` returns `undefined`, `providers.get(undefined)` also yields no provider, so the resource record for that address is created as `idle('none')` (`resources.ts:122`). The preview branch sees `meta.status === 'none'` and renders `t('resourceUnavailable')` (`ui-sidebar-documentpreview/.../locales.ts:21`, `TextPreview.tsx:84/179`). The whole process sends no request and throws no exception, so it is entirely silent.— https://github.com/deepseek-ai/deepseek-harness/discussions/6437
- coupling of content reading to metadata availability
- In `TextPreview`, `const canRead = meta.status !== 'none'` ties "content reading" (which goes through `remote.workspaceFiles` and already has typed failures and retries) to "metadata availability". The result is that one address-parsing miss is amplified into "the entire preview is completely unusable". Once decoupled, the same miss degrades only to "auto-refresh off" while the content still reads — a one-line change and valuable defense in depth.— https://github.com/deepseek-ai/deepseek-harness/discussions/6217
Sources
- #6217 — Bug: Web GUI file preview shows "file resource service unavailable" on Chromium <= 125 — protocolOf() relies on WHATWG URL host parsing for a non-special scheme· deepseek-ai (GitHub Discussions)
- #6437 — [Bug] dsh-client-resources' protocolOf relies on new URL().hostname; file preview shows "file resource service unavailable" on Edge 129· deepseek-ai (GitHub Discussions)