fix(assets): serve the asset protocol off the webview thread - #504
Merged
Conversation
Tauri's built-in `asset:` handler reads the file inline on the thread wry invokes it on, so one `` 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this is
Markpad now serves the
asset:protocol itself, off the thread the webviewcalls the handler on. Tauri's built-in handler reads the file inline, so a
document containing
on a share that is down freezes everywindow 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 thebuilder, four dependencies that were already in the lock file.
Nothing on the frontend changes.
convertFileSrc, the CSPimg-srclist,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_idand hands it a responder — the whole point of that responderis that the answer may come later, from another thread. Tauri's asset handler
does not use it:
protocol/asset.rscallsget_responseinline and only thenresponds. On the pinned 2.10.2 the wait inside is spelled
async_runtime::safe_block_on, whose "safe" means it will not panic inside aruntime, not that it does not block — both of its branches park the calling
thread. On today's
devthat call is gone, replaced by plainstd::fs, andthe read is still inline, so the freeze survived the rewrite.
Why we can replace it.
tauri/src/manager/webview.rsinstalls the built-inasset protocol only
if !registered_scheme_protocols.contains("asset"), andthat list is what the app registered. Registering
assetourselves suppressesit. So the fix is a handler that hands the request straight to
spawn_blockingand 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.tsrenders.mp4/.mp3links as<video>/<audio>on this same scheme, and dropping 206 would stop thoseseeking.
Three deliberate differences, two of which are upstream bugs the port's
tests found:
--, as RFC 2046 requires. Upstreamwrites the opening separator again, leaving multi-range responses
unterminated.
Content-Type.Builder::headerappendsrather 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.
Access-Control-Allow-Originechoes the request'sOriginwhen 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
Originat 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, soassetProtocol.scopekeeps meaning what it means.MAX_LEN, the 8192-bytesniff 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 carrieswhat 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 range206 with the right
Content-Rangeand exactly those bytes, 416 past the endwith
bytes */len, multipart for several ranges,HEADwith headers and nobody, 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):
rustfmtclean on the new file. The four added dependencies (http,http-range,percent-encoding,tauri-utils) were already inCargo.lockas 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