fix(v3): fix Android runtime crash when calling Go bindings#5510
fix(v3): fix Android runtime crash when calling Go bindings#5510amlwwalker wants to merge 1 commit into
Conversation
Android's WebViewClient.shouldInterceptRequest cannot access POST request bodies. When the JS runtime called Go bindings via fetch POST /wails/runtime, the Go-side request handler received a nil Body, causing a panic in io.Copy(buf, r.Body). This commit fixes three issues: 1. transport_http.go: Guard against nil r.Body in handleRuntimeRequest to prevent panic on platforms where POST bodies are unavailable. 2. application_android.go: Initialize globalMessageProc in platformRun() and implement handleMessageForAndroid to properly route runtime calls through the MessageProcessor, handling both string messages (e.g. wails:runtime:ready) and JSON-encoded binding calls. 3. index.ts: Set a custom transport on Android that routes runtime calls through window.wails.invoke() (the JNI bridge) instead of HTTP fetch, bypassing the shouldInterceptRequest POST body limitation entirely. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
WalkthroughThis PR implements Android JNI bridge support for Wails runtime calls. The changes add a global message processor to handle Android messages on the backend, expand the Android message handler to parse and dispatch RuntimeRequests, add client-side transport configuration to route calls through the JNI bridge, and harden HTTP body reading with nil-safety checks. ChangesAndroid JNI Runtime Integration
Sequence DiagramsequenceDiagram
participant Client as JavaScript Client
participant AndroidHandler as Android Message Handler
participant MessageProcessor as MessageProcessor
participant Backend as Backend Service
Client->>AndroidHandler: window._wails.invoke(ready signal or RuntimeRequest)
alt Ready Signal
AndroidHandler->>AndroidHandler: Mark WebviewWindow runtime loaded
AndroidHandler->>AndroidHandler: Flush queued JavaScript
else RuntimeRequest
AndroidHandler->>AndroidHandler: Unmarshal JSON to RuntimeRequest
AndroidHandler->>AndroidHandler: Auto-fill missing WebviewWindowID
AndroidHandler->>MessageProcessor: HandleRuntimeCallWithIDs
MessageProcessor->>Backend: Execute runtime call
Backend-->>MessageProcessor: Return result
MessageProcessor-->>AndroidHandler: Result or error
AndroidHandler-->>Client: JSON-serialized response
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 golangci-lint (2.12.2)level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies" Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@v3/pkg/application/application_android.go`:
- Around line 660-662: The error branches in handleMessageForAndroid that
currently return fmt.Sprintf(`{"error":"%s"}`, err.Error()) produce invalid JSON
when the error text contains quotes or control characters; replace these
branches (the json.Unmarshal failure at the shown block and the other similar
branches around lines 685–687 and 694–697) with logic that builds an error
envelope (e.g., a small map or struct like map[string]string{"error":
err.Error()}), json.Marshal()s that envelope and returns the marshaled string,
and if json.Marshal fails fall back to a safe literal like `{"error":"internal
error"}` while still logging the full marshal error via androidLogf; update the
branches where req is used so they return the marshaled JSON string instead of
fmt.Sprintf output.
- Around line 690-691: The Android path in application_android.go currently
returns `{"success":true}` when `result == nil`, which diverges from the HTTP
transport behavior that emits `{}` for nil/void results; update the nil-result
branch in the function handling runtime results (the block checking `if result
== nil`) to return an empty JSON object `{}` instead of `{"success":true}` so
Android and HTTP transports resolve void-style calls identically.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 0ad916af-c2cb-48b1-9c96-315287c6b36a
📒 Files selected for processing (3)
v3/internal/runtime/desktop/@wailsio/runtime/src/index.tsv3/pkg/application/application_android.gov3/pkg/application/transport_http.go
| if err := json.Unmarshal([]byte(message), &req); err != nil { | ||
| androidLogf("error", "🤖 [handleMessageForAndroid] Failed to parse: %v", err) | ||
| return fmt.Sprintf(`{"error":"%s"}`, err.Error()) |
There was a problem hiding this comment.
Marshal error payloads instead of interpolating err.Error() into JSON.
These fmt.Sprintf branches produce invalid JSON as soon as the message contains " or control characters, which makes Android error handling brittle on parse/dispatch failures. Use json.Marshal for the error envelope the same way the HTTP transport centralizes response encoding.
Proposed fix
+ encodeError := func(err error) string {
+ payload, _ := json.Marshal(map[string]string{
+ "error": err.Error(),
+ })
+ return string(payload)
+ }
+
var req RuntimeRequest
if err := json.Unmarshal([]byte(message), &req); err != nil {
androidLogf("error", "🤖 [handleMessageForAndroid] Failed to parse: %v", err)
- return fmt.Sprintf(`{"error":"%s"}`, err.Error())
+ return encodeError(err)
}
@@
result, err := messageProc.HandleRuntimeCallWithIDs(ctx, &req)
if err != nil {
androidLogf("error", "🤖 [handleMessageForAndroid] Error: %v", err)
- return fmt.Sprintf(`{"error":"%s"}`, err.Error())
+ return encodeError(err)
}
@@
resp, err := json.Marshal(result)
if err != nil {
androidLogf("error", "🤖 [handleMessageForAndroid] Marshal error: %v", err)
- return fmt.Sprintf(`{"error":"%s"}`, err.Error())
+ return encodeError(err)
}Also applies to: 685-687, 694-697
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@v3/pkg/application/application_android.go` around lines 660 - 662, The error
branches in handleMessageForAndroid that currently return
fmt.Sprintf(`{"error":"%s"}`, err.Error()) produce invalid JSON when the error
text contains quotes or control characters; replace these branches (the
json.Unmarshal failure at the shown block and the other similar branches around
lines 685–687 and 694–697) with logic that builds an error envelope (e.g., a
small map or struct like map[string]string{"error": err.Error()}),
json.Marshal()s that envelope and returns the marshaled string, and if
json.Marshal fails fall back to a safe literal like `{"error":"internal error"}`
while still logging the full marshal error via androidLogf; update the branches
where req is used so they return the marshaled JSON string instead of
fmt.Sprintf output.
| if result == nil { | ||
| return `{"success":true}` |
There was a problem hiding this comment.
Keep nil-result payloads aligned with the HTTP transport.
HTTPTransport.json() currently emits {} when a runtime call returns nil, but this Android path returns {"success":true}. That changes the resolved value of void-style runtime calls only on Android.
Proposed fix
if result == nil {
- return `{"success":true}`
+ return `{}`
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if result == nil { | |
| return `{"success":true}` | |
| if result == nil { | |
| return `{}` | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@v3/pkg/application/application_android.go` around lines 690 - 691, The
Android path in application_android.go currently returns `{"success":true}` when
`result == nil`, which diverges from the HTTP transport behavior that emits `{}`
for nil/void results; update the nil-result branch in the function handling
runtime results (the block checking `if result == nil`) to return an empty JSON
object `{}` instead of `{"success":true}` so Android and HTTP transports resolve
void-style calls identically.
Summary
r.Bodyintransport_http.go— Android'sWebViewClient.shouldInterceptRequestcannot access POST request bodies, so when the JS runtime calls Go bindings viafetch POST /wails/runtime, the Go-side handler receives a nil Body causingio.Copy(buf, r.Body)to panic.handleMessageForAndroidproperly — The stubhandleMessageForAndroidreturned{"success":true}for all messages. This replaces it with a real implementation that routes runtime calls through theMessageProcessor(binding calls, clipboard, events, etc.) and handles thewails:runtime:readylifecycle message to flush pending JS.globalMessageProcinplatformRun()— TheMessageProcessorwas never created for Android, so even if calls reachedhandleMessageForAndroid, they would fail.index.ts— Detectswindow.wails.invoke(theaddJavascriptInterfaceJNI bridge) and callssetTransport()to route allruntimeCallWithIDcalls through it instead of HTTP fetch, completely bypassing theshouldInterceptRequestPOST body limitation.Root cause
Android's
WebResourceRequestAPI does not provide access to POST request bodies. The Wails JS runtime usesfetch('POST', '/wails/runtime', { body: JSON.stringify(...) })for all binding calls. On Android,shouldInterceptRequestintercepts this but can only see the URL/headers — the body is lost. The Go handler then creates a request withnilbody, andio.Copypanics.How it works now
Test plan
Summary by CodeRabbit
New Features
Bug Fixes