Skip to content

[v2] WASM Content-Type Header Missing for wails:// Served Assets leading to WASM API Failures #4274

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
tassa-yoniso-manasi-karoto opened this issue May 10, 2025 · 0 comments
Labels
Bug Something isn't working

Comments

@tassa-yoniso-manasi-karoto
Copy link

tassa-yoniso-manasi-karoto commented May 10, 2025

Description

Affected Wails Version: v2.10 (and likely other v2 versions)
Frontend Setup: Vite (v5) with TypeScript, Rust/WASM via wasm-pack --target web.

Setup:

  • npm 10.8.1
  • gcc-13.2.0
  • webkit2gtk-2.44.2 and devel (⚠️2.48 seems to be the latest version but my distro doesn't have a more up-to-date version)
  • libwebkit2gtk41-2.46.6 and devel
  • gtk+3-3.24.49 and devel (⚠️same here, repology says latest version is 4.18 (!), looks like I am using a dead distro, sadge)
  • Void Linux 6.12

Note: Most of the text of this issue is written by Gemini 2.5 Pro with the context of my debugging attempts for the last 2 days + all the code of assetserver of wails' master branch.

Problem Description:
In wails dev mode, when frontend:dev:serverUrl is configured to point to a Vite development server, WebAssembly (.wasm) files fetched by wasm-bindgen's JavaScript glue are ultimately served to the WebKit webview via the wails:// protocol without the necessary Content-Type: application/wasm header. This occurs even if Vite serves the .wasm file with the correct Content-Type to Wails's ExternalAssetHandler proxy.

The absence of the correct Content-Type header on the response received by the webview causes WebAssembly.instantiateStreaming to fail. While wasm-bindgen's glue code may fall back to WebAssembly.instantiate(ArrayBuffer), this often results in an incompletely initialized WASM instance where standard WebAssembly JavaScript APIs, particularly access to WebAssembly.Memory (and thus wasm_bindgen::memory() from Rust), are non-functional.

Observed Flow in wails dev:
* The Wails webview requests the initial index.html and subsequently src/lib/wasm.ts and src/wasm-generated/pkg/app.js. Wails's AssetHandler proxies these requests to Vite's HTTP dev server. The content is served back to the webview, but the import.meta.url context within these loaded scripts becomes wails://wails.localhost:34115/src/....
* The app.js (glue) attempts to fetch app_bg.wasm using a URL resolved to wails://wails.localhost:34115/src/wasm-generated/pkg/app_bg.wasm.
* Wails's AssetHandler intercepts this wails:// request for app_bg.wasm.
* Its ExternalAssetHandler component then successfully fetches http://localhost:34115/src/wasm-generated/pkg/app_bg.wasm from Vite. Vite serves this with Content-Type: application/wasm to Wails's proxy.
* Wails's AssetHandler then serves the received .wasm binary content back to the webview in response to the wails://.../app_bg.wasm request.
* Crucially, inspecting this final response to the webview (e.g., via WebKit Network DevTools) shows "No response headers" or that the Content-Type header from Vite's response was not propagated.
* The wasm-bindgen JS glue logs a warning: "WebAssembly.instantiateStreaming failed because your server does not serve Wasm with application/wasm MIME type."
* Subsequent attempts to use wasm_bindgen::memory() from within Rust fail, indicating the WebAssembly.Memory object was not correctly linked or initialized.

To Reproduce

Setup:
* Wails v2 project with Vite frontend (for Svelte/TS)
* Rust/WASM module built with wasm-pack --target web --out-dir frontend/src/some-dir/pkg.
* Vite configured with vite-plugin-wasm, vite-plugin-top-level-await, and optimizeDeps.exclude for the WASM package.
* Wails wails.json has frontend:dev:serverUrl pointing to Vite's HTTP dev server (e.g., http://localhost:34115).
* Frontend JavaScript (e.g., in src/lib/wasm.ts) statically imports the wasm-bindgen JS glue (e.g., import initApp from '../wasm-generated/pkg/app.js';).
* initApp() is called, which then (internally, via new URL('./app_bg.wasm', import.meta.url)) attempts to fetch the companion .wasm file.

Expected behaviour

Wails's ExternalAssetHandler, when proxying a response from the Vite dev server (which includes a Content-Type: application/wasm header for .wasm files), should preserve or correctly set this Content-Type header on the response it ultimately sends to the webview, even when serving via the wails:// scheme. This would allow WebAssembly.instantiateStreaming to succeed and ensure full WASM API functionality.

Screenshots

Image

Attempted Fixes

The location of the bug hasn't been pinpointed exactly so for any other wails devs who want to use WASM, here is the workaround I am considering: inlining WASM binary or using AssetServer.Middleware.
I briefly got inlined WASM to work with functional WASM browser APIs so this one is confirmed to work but I originally ruled against it because it looked too brittle.

1. Manual WASM Inlining (JS Glue Modification)

  • Approach:
    1. The .wasm binary is Base64 encoded.
    2. The wasm-bindgen-generated JavaScript glue file (e.g., log_engine.js) is modified by a build script (e.g., a Node.js or shell script).
    3. This modification involves:
      • Embedding the Base64 string and a base64ToArrayBuffer helper function within the JS glue.
      • Surgically altering wasm-bindgen's primary initialization function (typically the default export, async function __wbg_init(module_or_path)) to bypass its internal fetch logic and instead use the embedded ArrayBuffer (e.g., by setting module_or_path = WASM_BINARY_BUFFER; before __wbg_load is called).
    4. This self-contained, inlined JS glue file is then imported and initialized by the application.
  • Status & Outcome:
    • This approach successfully bypasses the network fetch for the .wasm file, thereby avoiding the wails:// Content-Type issue for the binary itself.
    • When implemented carefully to ensure wasm-bindgen's internal state (particularly wasm.memory which wasm_bindgen::memory() relies on) is correctly initialized after instantiating from the buffer, this method has been shown to result in functional WebAssembly browser APIs.
  • Drawbacks & Considerations for Developers:
    • Complexity of Patching: Correctly and robustly modifying wasm-bindgen's generated JS glue is non-trivial. The glue code is auto-generated and its internal structure can change between wasm-bindgen versions, making such patches fragile and hard to maintain.
    • Increased JS Bundle Size: Embedding the WASM binary (even Base64 encoded) directly into the JavaScript significantly increases the size of that JS file.
    • Build Process Overhead: Requires an additional, custom build step to perform the encoding and patching.
    • Debugging: Source mapping for the original Rust code might become more challenging with a heavily modified JS glue.
  • Note for Wails Developers: This workaround highlights that if the WASM binary can be delivered to WebAssembly.instantiate as an ArrayBuffer without relying on wails:// for fetching the binary itself with correct headers, the core WASM instantiation and API linkage can work. The difficulty lies in achieving this without breaking wasm-bindgen's own setup logic.

2. Wails AssetServer.Middleware (Theoretical, Untested for this Specific WASM Case)
Revised Assessment → Likely Ineffective for API Access Issue): Since the middleware operates on the Go http.ResponseWriter before this final translation to the webview for wails:// requests, if the Wails-to-webview interface or the webview itself is the point of failure for header recognition for custom schemes, the middleware alone won't fix the WASM API access issue. Therefore, this middleware approach is now considered unlikely to be a complete solution for enabling full WASM API functionality in the current wails dev environment.

System Details

# System
WARNING: failed to read int from file: open /sys/devices/system/cpu/cpu0/online: no such file or directory
┌─────────────────────────────────────────────────────────────────────────────────┐
| OS           | Void                                                             |
| Version      | Unknown                                                          |
| ID           | void                                                             |
| Go Version   | go1.23.4                                                         |
| Platform     | linux                                                            |
| Architecture | amd64                                                            |
| CPU          | Intel(R) Pentium(R) Silver N5000 CPU @ 1.10GHz                   |
| GPU          | GeminiLake [UHD Graphics 605] (Intel Corporation) - Driver: i915 |
| Memory       | 8GB                                                              |
└─────────────────────────────────────────────────────────────────────────────────┘

# Dependencies
┌──────────────────────────────────────────────┐
| Dependency | Package Name | Status | Version |
|                                              |
└──────────────────────────────────────────────┘

# Diagnosis
 SUCCESS  Your system is ready for Wails development!

Additional context

Possible Root Causes for Missing Content-Type on wails:// Response:

The issue where the Content-Type: application/wasm header (received from the Vite dev server by Wails's ExternalAssetHandler) is not present on the final response to the WebKitGTK webview for wails:// scheme requests appears to stem from the interaction between Wails's Go code and the underlying WebKitGTK/libsoup APIs for custom scheme handling.

  1. Issue in Wails's CGO Bridging or WebKitGTK/libsoup API Usage for Custom Schemes (Linux):

    • Wails's ExternalAssetHandler uses httputil.ReverseProxy, which should preserve headers from the upstream Vite server (including Content-Type: application/wasm) onto the Go http.ResponseWriter it's given.
    • This ResponseWriter is a Wails wrapper (webview.ResponseWriter) which, on Linux, is implemented in v2/pkg/assetserver/webview/responsewriter_linux.go.
    • The WriteHeader method in responsewriter_linux.go calls webkit_uri_scheme_request_finish (defined in v2/pkg/assetserver/webview/webkit2_36+.go).
    • The webkit_uri_scheme_request_finish function explicitly attempts to set the Content-Type and all other HTTP headers on the WebKitURISchemeResponse using CGO calls to WebKitGTK/libsoup functions:
      • C.webkit_uri_scheme_response_set_content_type(resp, cMimeType)
      • C.soup_message_headers_append(hdrs, cName, cValue) followed by C.webkit_uri_scheme_response_set_http_headers(resp, hdrs).
    • The Problem: Despite these explicit attempts by Wails's Go code to set the headers, the WebKitGTK DevTools show "No response headers" for the .wasm file served via wails://. This points to a potential issue in:
      • The CGO marshalling/unmarshalling of header data between Go and C.
      • The specific way these WebKitGTK/libsoup C functions (webkit_uri_scheme_response_set_content_type, webkit_uri_scheme_response_set_http_headers, soup_message_headers_append) are being used by Wails for custom scheme responses.
      • A bug or limitation within WebKitGTK/libsoup itself when handling headers (especially Content-Type for binary types like application/wasm) for requests made over custom URL schemes (like wails://). Historical Wails issues with other media types on custom schemes (e.g., Wails issue [v2, linux] custom AssetsHandler ignores HTML video's src on production #1568) suggest this layer can be problematic.
  2. Overriding/Ignoring of Headers by WebKitGTK for Custom Schemes:

    • It's possible that even if Wails correctly calls the C APIs to set the headers, WebKitGTK might ignore, override, or not fully expose these headers to the web engine's loader when the response is for a custom wails:// scheme, particularly for binary content that initiates a specific loader path (like the WASM compiler). The "No response headers" in devtools is a strong indicator that the headers set via C API are not making it to the inspection layer.
  3. Less Likely: contentTypeSniffer Interference:

    • The contentTypeSniffer wraps the webview.ResponseWriter. It only attempts to http.DetectContentType and set it if no Content-Type is already present on the underlying writer's Header() map. If httputil.ReverseProxy correctly populates this map from Vite's response, the sniffer should not interfere. The issue appears to be downstream from this, in the actual delivery to WebKitGTK.
@tassa-yoniso-manasi-karoto tassa-yoniso-manasi-karoto added the Bug Something isn't working label May 10, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug Something isn't working
Projects
None yet
Development

No branches or pull requests

1 participant