Skip to content

fix(assets): serve the asset protocol off the webview thread - #504

Merged
PathGao merged 1 commit into
masterfrom
fix/async-asset-protocol
Aug 6, 2026
Merged

fix(assets): serve the asset protocol off the webview thread#504
PathGao merged 1 commit into
masterfrom
fix/async-asset-protocol

Conversation

@PathGao

@PathGao PathGao commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

What this is

Markpad now serves the asset: protocol itself, off the thread the webview
calls the handler on. Tauri's built-in handler reads the file inline, so a
document containing ![](pic.png) on a share that is down freezes every
window for the length of the share timeout — with no user action beyond
opening the document.

One new module (src-tauri/src/asset_protocol.rs), one registration in the
builder, four dependencies that were already in the lock file.

Nothing on the frontend changes. convertFileSrc, the CSP img-src list,
the sanitizer's scheme allowlist and the export rewriter all keep working,
because this replaces the handler for the same scheme name rather than
introducing a new one.

Mechanism

Two things make this possible and necessary.

Why it blocks. wry invokes a protocol handler on the thread it records as
main_thread_id and hands it a responder — the whole point of that responder
is that the answer may come later, from another thread. Tauri's asset handler
does not use it: protocol/asset.rs calls get_response inline and only then
responds. On the pinned 2.10.2 the wait inside is spelled
async_runtime::safe_block_on, whose "safe" means it will not panic inside a
runtime, not that it does not block — both of its branches park the calling
thread. On today's dev that call is gone, replaced by plain std::fs, and
the read is still inline, so the freeze survived the rewrite.

Why we can replace it. tauri/src/manager/webview.rs installs the built-in
asset protocol only if !registered_scheme_protocols.contains("asset"), and
that list is what the app registered. Registering asset ourselves suppresses
it. So the fix is a handler that hands the request straight to
spawn_blocking and responds from there.

Upstream has had this open for three years as
tauri-apps/tauri#7434
"load image file by asset protocol from smb will hang the entire application",
whose reporter guessed the cause in 2023. This PR does not attempt that fix;
it stops Markpad waiting for it.

Scope

The body is a port, kept close to tauri/src/protocol/asset.rs
(MIT/Apache-2.0) so it stays easy to diff on a tauri upgrade. Range support is
copied rather than skipped because markdown.ts renders .mp4/.mp3 links as
<video>/<audio> on this same scheme, and dropping 206 would stop those
seeking.

Three deliberate differences, two of which are upstream bugs the port's
tests found:

  1. The multipart closing delimiter ends in --, as RFC 2046 requires. Upstream
    writes the opening separator again, leaving multi-range responses
    unterminated.
  2. A multipart answer carries one Content-Type. Builder::header appends
    rather than replaces, so upstream — which sets the file's type before it
    knows the request is multi-range — emits two, with the wrong one first.
  3. Access-Control-Allow-Origin echoes the request's Origin when it has one,
    because the value tauri computes at webview creation is not reachable from
    here. Only our own webview can issue requests on this scheme, and
    subresource loads send no Origin at all, which the fallback covers.

Worth reporting upstream on #7434; not done in this PR.

Left alone: the configured scope is honoured through
Manager::asset_protocol_scope() and passed in as a predicate, so
assetProtocol.scope keeps meaning what it means. MAX_LEN, the 8192-byte
sniff and the read-once-if-small optimisation are unchanged from upstream. The
module does not log, matching the rest of src-tauri — the status code carries
what went wrong.

This touches src-tauri/src/lib.rs, as #502 does, but in different regions
(module list and builder chain, versus four command bodies). The two are
independent and can land in either order.

Tests

Nine tests in the module, covering what a reviewer would otherwise have to
take on trust: whole file with sniffed type and length, 404 for missing, 403
for a path that climbs out with ../, 403 when the scope refuses, single range
206 with the right Content-Range and exactly those bytes, 416 past the end
with bytes */len, multipart for several ranges, HEAD with headers and no
body, and the origin echo.

Being honest about what they establish: they pin the port's behaviour, not
the absence of the freeze.
Proving the freeze is gone needs a slow or dead
share, which I do not have. What they did already do is catch differences 1 and
2 above — the multipart test failed against a faithful port, which is how both
upstream bugs surfaced.

Verification

Everything CI runs, on macOS (arm64):

npm audit      # found 0 vulnerabilities
npm run check  # 653 files, 0 errors, 0 warnings
npm test       # 778 pass, 0 fail  (frontend untouched)
cargo test     # 134 pass, 0 fail  (125 before, +9 here)

rustfmt clean on the new file. The four added dependencies (http,
http-range, percent-encoding, tauri-utils) were already in Cargo.lock
as transitive dependencies of tauri, so the lock diff is four lines in
Markpad's own dependency list and no new package.

Not verified, and worth a reviewer's attention: that the registration
actually suppresses the built-in handler is read from tauri 2.10.2's
manager/webview.rs, not observed at runtime — and the failure mode is quiet,
because if suppression did not happen the built-in would keep serving images
and nothing would look wrong. Anyone running the app can settle it in a minute
by making this handler return a marker header or a 418 and reloading a document
with an image. I also have no Windows, WSL or SMB share, so the unblocking
itself is reasoned from the thread model rather than measured.

🤖 Generated with Claude Code

Tauri's built-in `asset:` handler reads the file inline on the thread
wry invokes it on, so one `![](pic.png)` pointing at a share that is
down freezes every window for the length of the share timeout. No
keystroke is needed — rendering the document is enough. On the pinned
2.10.2 the wait is spelled `async_runtime::safe_block_on`, whose "safe"
means it will not panic inside a runtime rather than that it does not
block; both of its branches park the caller. On tauri's `dev` that call
is gone and the read is plain `std::fs`, still inline, so the freeze
survived that rewrite. Upstream has it open as tauri-apps/tauri#7434,
filed 2023.

`manager/webview.rs` installs the built-in handler only when the app has
not registered one for the scheme, so registering `asset` here replaces
it and no frontend code changes: `convertFileSrc`, the CSP `img-src`
list, the sanitizer allowlist and the export rewriter all keep working
against the same name. The handler hands the request to `spawn_blocking`
and responds from there.

The body is a port of `tauri/src/protocol/asset.rs` (MIT/Apache-2.0),
kept close to the original so it stays easy to diff on an upgrade. Range
support is copied rather than skipped: `markdown.ts` renders `.mp4` and
`.mp3` links as `<video>`/`<audio>` on this scheme, and without 206 they
would not seek. The configured scope still applies, passed in as a
predicate so the module is testable without an app.

Three differences from upstream, two of them bugs the port's own tests
found: the multipart closing delimiter now ends in `--` as RFC 2046
requires; a multipart answer now carries one `Content-Type` rather than
two with the wrong one first (`Builder::header` appends); and
`Access-Control-Allow-Origin` echoes the request's `Origin`, since the
value tauri computes at webview creation is not reachable here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@PathGao
PathGao merged commit 4b67aad into master Aug 6, 2026
4 checks passed
@PathGao
PathGao deleted the fix/async-asset-protocol branch August 7, 2026 15:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant