convertFileSrc 的图片加载失败
主线位置:不在 invoke 上,是启动主线
prepare_pending_webview挂上的asset协议,页面直接按 URL 读本地文件。协议本身(URL 怎么编码、作用域怎么匹配、Range 请求)见 asset:// 协议与文件作用域;本页把一张图片显示不出来的几种原因逐个复现,给出区分它们的办法。
flowchart TB
img["img onerror"] --> f{"fetch 同一个 URL"}
f -->|"Load failed 无违规"| off["protocol-asset 没开"]
f -->|"Load failed 有违规"| csp["CSP 没放行 asset"]
f -->|403| scope["路径不在作用域"]
f -->|404| missing["文件不存在"]
实验 crate exp-tauri/pit3-asset:从模板 base 复制,files/a.png 和 files/sub/b.png 是两张 32×32 的 PNG。命令 paths 返回两张图的绝对路径,页面对每个路径 convertFileSrc,先当 <img> 加载(记 onload / onerror),再 fetch 一次记状态码,并收集 securitypolicyviolation:
for (const p of await T.invoke('paths')) {
const url = T.convertFileSrc(p);
const name = p.split('/files/')[1];
out[`img ${name}`] = await img(url);
try { const r = await fetch(url); out[`fetch ${name}`] = `${r.status} ${r.headers.get('content-type')}`; }
catch (e) { out[`fetch ${name}`] = String(e); }
}
四种配置依次构建、运行。tauri =2.12.5,--features tauri/custom-protocol,隐藏窗口,debug 构建,平台 M2 Max、macOS 27、rustc 1.99.0(WKWebView)。
现象
一:没开 protocol-asset。 tauri 依赖没有这个 feature,配置里也没有 assetProtocol,不设 CSP:
REPORT {
"img a.png": "error",
"fetch a.png": "TypeError: Load failed",
"img sub/b.png": "error",
"fetch sub/b.png": "TypeError: Load failed",
"url": "asset://localhost/%2Fx%2Fa.png",
"violations": []
}
convertFileSrc 照样给出了 URL(它在 core.js 里,和 feature 无关),但 WKWebView 里没有注册 asset 这个 scheme,请求直接失败,没有状态码。
二:开了协议,CSP 没放行 asset:。 feature 和 assetProtocol 都开了,作用域是 files/*,CSP 是一份常见的严格写法:
"security": {
"csp": "default-src 'self'; connect-src 'self' ipc: http://ipc.localhost",
"assetProtocol": { "enable": true, "scope": ["~/exp-tauri/pit3-asset/files/*"] }
}
REPORT {
"img a.png": "error",
"fetch a.png": "TypeError: Load failed",
"img sub/b.png": "error",
"fetch sub/b.png": "TypeError: Load failed",
"url": "asset://localhost/%2Fx%2Fa.png",
"violations": [
"img-src asset://localhost/%2FUsers%2Fhal%2Fbuild%2Fui-ecosystem%2Fexp-tauri%2Fpit3-asset%2Ffiles%2Fa.png",
"connect-src asset://localhost/%2FUsers%2Fhal%2Fbuild%2Fui-ecosystem%2Fexp-tauri%2Fpit3-asset%2Ffiles%2Fa.png",
"img-src asset://localhost/%2FUsers%2Fhal%2Fbuild%2Fui-ecosystem%2Fexp-tauri%2Fpit3-asset%2Ffiles%2Fsub%2Fb.png",
"connect-src asset://localhost/%2FUsers%2Fhal%2Fbuild%2Fui-ecosystem%2Fexp-tauri%2Fpit3-asset%2Ffiles%2Fsub%2Fb.png"
]
}
页面上的结果和第一种一模一样,区别只在违规列表:<img> 被 img-src(回落到 default-src 'self')拦下,fetch 被 connect-src 拦下。请求没有到达 Rust。
三:CSP 放行了,图片在子目录里。 CSP 加上 img-src 'self' asset: http://asset.localhost,connect-src 也加上 asset: 和 http://asset.localhost,作用域还是 files/*:
REPORT {
"img a.png": "loaded 32x32",
"fetch a.png": "200 image/png",
"img sub/b.png": "error",
"fetch sub/b.png": "403 null",
"url": "asset://localhost/%2Fx%2Fa.png",
"violations": []
}
a.png 出来了,sub/b.png 是 403,正文为空。
出处
三种失败落在三个地方:
- 协议没注册。
asset协议的注册在prepare_pending_webview末尾,整段在#[cfg(feature = "protocol-asset")]里;assetProtocol.enable运行时不读,只在编译期由tauri-build要求和 feature 一致(asset:// 协议与文件作用域)。 - CSP。tauri 改写 CSP 时只碰
script-src和style-src(set_csp),img-src、media-src、connect-src由你写什么就是什么。没写img-src时回落到default-src,'self'只覆盖tauri://localhost,asset://localhost是另一个源。 - 作用域。
get_response在读文件之前查作用域,不通过就回空的 403:
if !scope.is_allowed(&safe_path) {
log::error!("asset protocol not configured to allow the path: {path}");
return resp.status(403).body(Vec::new().into()).map_err(Into::into);
}
作用域的 glob 用 require_literal_separator: true 编译(scope/fs.rs),* 不匹配 /,所以 files/* 只覆盖 files/ 下一层。Unix 上通配符也不匹配以 . 开头的文件名;..、拒绝表也都回同样的空 403。
原因写在 log::error! 里,而 tauri 自己不装 logger:没有 tauri-plugin-log 或别的 log 实现时,这行字哪里也看不到。在实验里装一个最小的 log::Log(把 Error 级别的记录 println! 出来),第三种配置再跑一次:
[ERROR tauri::protocol::asset] asset protocol not configured to allow the path: ~/exp-tauri/pit3-asset/files/sub/b.png
[ERROR tauri::protocol::asset] asset protocol not configured to allow the path: ~/exp-tauri/pit3-asset/files/sub/b.png
两行:一行是 <img>,一行是 fetch。
修法
三处都要对:
tauri = { version = "=2.12.5", features = ["protocol-asset"] }
"security": {
"csp": "default-src 'self'; img-src 'self' asset: http://asset.localhost; connect-src 'self' ipc: http://ipc.localhost asset: http://asset.localhost",
"assetProtocol": { "enable": true, "scope": ["~/exp-tauri/pit3-asset/files/**"] }
}
REPORT {
"img a.png": "loaded 32x32",
"fetch a.png": "200 image/png",
"img sub/b.png": "loaded 32x32",
"fetch sub/b.png": "200 image/png",
"url": "asset://localhost/%2Fx%2Fa.png",
"violations": []
}
- CSP 里同时写
asset:和http://asset.localhost:前者是 macOS 上的 URL,后者是 Windows 上convertFileSrc生成的形式(convertFileSrc在windows/android上拼http://asset.localhost/;Windows 没有实测)。只用<img>、<video>时connect-src可以不加,页面要fetch文件就得加。 - 作用域要递归就写
**。实际项目里常写$APPDATA/**这类带变量的路径;变量展开失败的条目会被静默跳过(见 asset:// 协议与文件作用域的「作用域」),也是一个 403 的来源。 - 路径只在运行时才知道时,用
app.asset_protocol_scope().allow_file(..)/allow_directory(.., true)临时放行,不要把作用域写成**:asset 协议不经过 ACL,作用域就是它唯一的权限。
怎么确认没中招
- 不要只看
<img>,它对所有原因都只给一个onerror。对同一个 URLfetch一次,按上面的流程图分:TypeError: Load failed且没有违规事件,是协议没注册;有img-src/connect-src违规,是 CSP;403 是作用域(或含..、点文件);404 是文件不存在。 - 403 的具体原因只在
log::error!里,开发构建装上tauri-plugin-log(或任意log实现)就能看到。 - Rust 侧用
app.asset_protocol_scope().is_allowed(path)对一个真实路径先问一次,能把作用域问题和前端问题分开。
小结
convertFileSrc只负责拼 URL,加载成不成要看三样:protocol-assetfeature(加assetProtocol.enable)、CSP 的img-src/connect-src放行asset:和http://asset.localhost、路径在assetProtocol.scope里。- 前两种在页面上都是
TypeError: Load failed,靠securitypolicyviolation区分;作用域问题是空正文的 403,原因只写进日志。 - 作用域里
*不跨目录、不匹配点文件,子目录要写**。