Skip to content

fix(v3): fix Android runtime crash when calling Go bindings#5510

Open
amlwwalker wants to merge 1 commit into
wailsapp:masterfrom
amlwwalker:fix/android-runtime-crash
Open

fix(v3): fix Android runtime crash when calling Go bindings#5510
amlwwalker wants to merge 1 commit into
wailsapp:masterfrom
amlwwalker:fix/android-runtime-crash

Conversation

@amlwwalker
Copy link
Copy Markdown

@amlwwalker amlwwalker commented May 26, 2026

Summary

  • Guard nil r.Body in transport_http.go — Android's WebViewClient.shouldInterceptRequest cannot access POST request bodies, so when the JS runtime calls Go bindings via fetch POST /wails/runtime, the Go-side handler receives a nil Body causing io.Copy(buf, r.Body) to panic.
  • Implement handleMessageForAndroid properly — The stub handleMessageForAndroid returned {"success":true} for all messages. This replaces it with a real implementation that routes runtime calls through the MessageProcessor (binding calls, clipboard, events, etc.) and handles the wails:runtime:ready lifecycle message to flush pending JS.
  • Initialize globalMessageProc in platformRun() — The MessageProcessor was never created for Android, so even if calls reached handleMessageForAndroid, they would fail.
  • Set Android JNI transport in index.ts — Detects window.wails.invoke (the addJavascriptInterface JNI bridge) and calls setTransport() to route all runtimeCallWithID calls through it instead of HTTP fetch, completely bypassing the shouldInterceptRequest POST body limitation.

Root cause

Android's WebResourceRequest API does not provide access to POST request bodies. The Wails JS runtime uses fetch('POST', '/wails/runtime', { body: JSON.stringify(...) }) for all binding calls. On Android, shouldInterceptRequest intercepts this but can only see the URL/headers — the body is lost. The Go handler then creates a request with nil body, and io.Copy panics.

How it works now

JS binding call → setTransport.call() → window.wails.invoke(JSON.stringify({...}))
  → WailsJSBridge.invoke() (Java) → nativeHandleMessage (JNI)
  → handleMessageForAndroid (Go) → MessageProcessor.HandleRuntimeCallWithIDs()
  → response returned synchronously back through the JNI bridge

Test plan

  • Build and run on Android emulator — verify app loads without crash
  • Tap any Go binding button (e.g. "Random Person") — verify data returns to UI
  • Test counter increment/decrement on Events tab — verify stateful service works
  • Verify Go→JS events (time ticker) continue working
  • Verify no regression on desktop platforms (nil body guard is defensive only)

Summary by CodeRabbit

  • New Features

    • Enhanced Android integration with improved JNI bridge communication for runtime calls
    • Better message processing and error handling for platform-specific runtime operations
  • Bug Fixes

    • Improved HTTP transport robustness in request handling

Review Change Stack

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>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 26, 2026

Walkthrough

This 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.

Changes

Android JNI Runtime Integration

Layer / File(s) Summary
Android Runtime Infrastructure Setup
v3/pkg/application/application_android.go
Adds context package import, declares globalMessageProc with accompanying RW lock, and initializes the message processor during App.platformRun() for thread-safe Android message routing.
Android Message Handler & HTTP Transport Safety
v3/pkg/application/application_android.go, v3/pkg/application/transport_http.go
Rewrites handleMessageForAndroid to detect "ready" signaling, unmarshal JSON into RuntimeRequest, auto-fill window IDs, dispatch through MessageProcessor.HandleRuntimeCallWithIDs, and return marshaled results or error JSON. Adds nil-check for r.Body before reading HTTP request bodies to prevent copying from nil streams.
Client-Side Android Transport Configuration
v3/internal/runtime/desktop/@wailsio/runtime/src/index.ts
Imports setTransport and conditionally configures Android JNI bridge transport: when window._wails.invoke exists, routes runtime calls through the JNI bridge with JSON payload construction and response parsing.

Sequence Diagram

sequenceDiagram
  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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • leaanthony
  • makew0rld

Poem

🐰 Hop, hop, the Android bridge is born,
JNI calls at dawn and morn.
Messages flow through global locks so true,
Ready signals flush what's overdue.
From JS to Go, the journey's fleet, 💫

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description provides a thorough summary of changes, root cause analysis, and test plan. However, the required template sections (issue reference, type of change checkbox, testing details, and checklist) are not completed. Add 'Fixes #' reference, check the 'Bug fix' checkbox under 'Type of change', document test configuration, and complete the required checklist items.
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main fix—resolving an Android runtime crash when calling Go bindings. It's concise, specific, and clearly conveys the primary change.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 1133fdc and 8704f90.

📒 Files selected for processing (3)
  • v3/internal/runtime/desktop/@wailsio/runtime/src/index.ts
  • v3/pkg/application/application_android.go
  • v3/pkg/application/transport_http.go

Comment on lines +660 to 662
if err := json.Unmarshal([]byte(message), &req); err != nil {
androidLogf("error", "🤖 [handleMessageForAndroid] Failed to parse: %v", err)
return fmt.Sprintf(`{"error":"%s"}`, err.Error())
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Comment on lines +690 to +691
if result == nil {
return `{"success":true}`
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Suggested change
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.

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.

2 participants