DSH plugin: file previews break on legacy Chromium engines

TroubleshootingPublished 2026-10-03Author: DeepSeek Plugin Market
DeepSeek HarnessDSHfile previewdsh-resourceprotocolOfWHATWG URLChromium compatibilitydsh-client-resources
File previews say file resource service unavailable while host logs stay clean: protocolOf() reads it from new URL().hostname, empty on legacy Chromium.

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.

CriterionClient address-parsing incompatibilityBackend file service genuinely broken
Host RPC workspaceFiles/list / stat / readAllall okerrors or timeouts
/api/workspaceFiles/* in DevTools Networknot a single onerequests present and failing
Host / server logsno related records at all4xx/5xx or exception stacks
Open the same page in a newer enginerecoversstill fails
Console errorsclean (this path throws nothing)usually has errors
Blast radiusglobally consistent across every preview entry (sidebar / chat links / deliverables)depends on the specific file / endpoint

Minimal three-step reproduction

  1. Open the Web GUI with an affected engine (http://127.0.0.1:3080; the server version is irrelevant — the original thread verified on 0.1.5-rc.1). To reproduce an old engine, use npx @puppeteer/browsers install chrome@125.0.6422.60.
  2. Open any text file — click a .md in the sidebar Files, or click a file link in chat.
  3. Observe: the preview area shows "file resource service unavailable"; DevTools Network shows zero /api/workspaceFiles calls; 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)

js
const u = new URL('dsh-resource://file/session/s1/a.md');
console.log(u.protocol, JSON.stringify(u.hostname), JSON.stringify(u.pathname));
EnvironmentOutput
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":

js
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):

ts
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)

text
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 / environmentnew URL('dsh-resource://file/…').hostnamePreview
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:

ObservationChromium 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 addressprotocol: null, status: "none"protocol: "file", status: "live"
Preview panel"file resource service unavailable."renders the file content normally
Registered providers["file"]["file"]
Client plugin loadingall activeall 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.

js
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:

  1. 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 + read return 200).
  2. 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):

diff
 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)

  1. 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 returns undefined.
  2. It enters the fallback only when hostname === '', cleanly separating "the parser cannot get a host" from "the address simply has no host".
  3. 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/x and a hostname containing spaces still yield undefined).

Companion unit tests (simulating an engine whose non-special-scheme hostname is always empty)

ts
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:

ts
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):

ts
// 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.

  1. Do not trust the server logs: the host never received a request, so of course there is nothing in the logs (#6437, #6217).
  2. One-line identification: run new URL('dsh-resource://file/x/y').hostname in the affected page — an affected engine returns '', Chrome/Safari returns 'file' (#6437).
  3. 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).
  4. Look at the provider registry rather than guessing: when affected, "file" in providers is registered, yet the record for that address has protocol of null/undefined and status stuck at none (#6437).
  5. Refresh does not recover: a record is back-attached to recordsOf(protocol) only at the moment the provider registers, and a record whose protocol is undefined is never among them (#6437).
  6. 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 the resources service instance along the React fiber and enumerate providers directly 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).
  7. Temporary workarounds and their cost:
    1. Switch to a newer-engine Chromium / Edge; or
    2. Apply fix one as a replacement to …/dsh-client-resources/lib/client.js. Note DSH is usually launched by npx @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).
  8. Upstream status: as of the cited reports, both protocolOf and pathOf are unfixed — in 0.1.7-alpha.2, protocolOf in dsh-client-resources/lib/client.js is byte-for-byte identical to the old version; 0.1.7-rc.1 still reproduces; the version-scan table in #7412 can take a row marked "not fixed" (#6217).
  9. Third-party corroboration of the same symptom: #7514 (the paradox of a registered provider showing none) 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.

DSH Plugin Hub · Confirm update

Source: Discussion #6217, Discussion #6437.

FAQ

The DSH Web GUI says "file resource service unavailable" — is the backend file service down?

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.

Why do restarts and DSH upgrades not help?

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.

What is the fastest way to confirm this is the problem?

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).

Which browsers are affected? Can I judge by Chromium major version?

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).

I applied a community patch to client.js — why did it break again after an upgrade?

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