Skip to content

fix(v3): chunk large IPC payloads to bypass WebView2 2 MB body limit#5369

Merged
leaanthony merged 6 commits intomasterfrom
agent/engineer-windows/97a4d367
May 10, 2026
Merged

fix(v3): chunk large IPC payloads to bypass WebView2 2 MB body limit#5369
leaanthony merged 6 commits intomasterfrom
agent/engineer-windows/97a4d367

Conversation

@leaanthony
Copy link
Copy Markdown
Member

@leaanthony leaanthony commented May 8, 2026

Problem

When a frontend service call passes a payload larger than ~2 MB, the ICoreWebView2WebResourceRequest::GetContent() IStream silently truncates or drops the body. From the browser's perspective the POST to http://wails.localhost/wails/runtime fails and DevTools reports it as a CORS error.

Repro: call any bound Go service method from JavaScript with a string or Blob argument larger than ~2 MB.

Fixes #4428

Root cause

WebView2's WebResourceRequested event delivers the request body through ICoreWebView2WebResourceRequest::GetContent() — a COM IStream. The WebView2 team has documented an internal buffering limit of roughly 2 MB; bodies beyond that are not fully delivered to the host process.

Fix

JavaScript (runtime.ts) — if the serialised JSON body exceeds 512 KB the new sendChunked helper splits it into 512 KB slices and sends them serially. Each request carries three coordination headers:

Header Value
x-wails-chunk-id nanoid that ties all slices together
x-wails-chunk-index 0-based slice index
x-wails-chunk-total total number of slices

All intermediate chunks receive a 200 OK with an empty body. The final chunk's response carries the actual RPC result, so the call-site API is completely unchanged.

Go (transport_http.go)handleRuntimeRequest detects the x-wails-chunk-id header and delegates to the new handleChunkedRequest path. Chunks are accumulated in a sync.Map of pendingChunks. When all slices arrive they are assembled in order and forwarded to the existing processBody path. A background goroutine (10 s tick) evicts accumulators older than 30 s to prevent leaks from aborted transfers.

Files changed

File Change
v3/pkg/application/transport_http.go Chunk accumulation, assembly, TTL cleanup
v3/internal/runtime/desktop/@wailsio/runtime/src/runtime.ts CHUNK_THRESHOLD constant, sendChunked helper, conditional dispatch
v3/internal/assetserver/bundledassets/runtime.debug.js Rebuilt (esbuild, inline sourcemap)
v3/internal/assetserver/bundledassets/runtime.js Rebuilt (esbuild, minified)

Testing

  • go build ./v3/pkg/application/... — clean
  • tsc --noEmit in the runtime package — clean
  • Bundled JS verified to contain CHUNK_THRESHOLD / sendChunked symbols

Manual end-to-end test: bind a Go method that accepts a string, call it from JS with a 3 MB payload; the call completes successfully in both debug and production bundles.

CC @leaanthony

Summary by CodeRabbit

  • New Features

    • Large RPC requests now upload in segmented chunks when oversized, improving reliability for very large payloads.
    • Server now accepts segmented uploads and assembles them before processing.
  • Bug Fixes

    • Reduced failures and timeouts for heavy or large data transfers.
  • Other

    • Runtime startup and integration points still signal readiness as before.

Review Change Stack

WebView2's WebResourceRequested event fails to deliver POST bodies larger
than ~2MB via ICoreWebView2WebResourceRequest::GetContent(), causing
service calls with large string arguments to fail with a network/CORS-
style error on the JS side.

Fix: when the serialised request body exceeds 512 KB the JS runtime
splits it into 512 KB chunks and sends them serially with coordination
headers (x-wails-chunk-id / x-wails-chunk-index / x-wails-chunk-total).
The Go HTTPTransport accumulates chunks, assembles them when complete, and
processes the reassembled body identically to a single-request call.
Partial chunk state is cleaned up after 30 s to prevent memory leaks.

Fixes #4428

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
Copilot AI review requested due to automatic review settings May 8, 2026 02:36
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 8, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 327b0d5a-ca3e-4b0f-85b4-64a39aef6a36

📥 Commits

Reviewing files that changed from the base of the PR and between c8b4532 and 2a0aa05.

📒 Files selected for processing (1)
  • v3/pkg/application/transport_http.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • v3/pkg/application/transport_http.go

Walkthrough

Client and bundled runtime now conditionally split large serialized RPC payloads (>512 KiB) into sequential POST chunks with chunk headers; server HTTP transport accumulates chunks keyed by ID, validates ordering, assembles the full body, processes the RPC payload, and runs a background cleanup for stale chunk assemblies.

Changes

Large Payload Chunking for Wails Runtime

Layer / File(s) Summary
Chunking Constants and Data Structures
v3/internal/runtime/desktop/@wailsio/runtime/src/runtime.ts, v3/pkg/application/transport_http.go
Add CHUNK_THRESHOLD (512 KiB), chunk header names (x-wails-chunk-id, x-wails-chunk-index, x-wails-chunk-total), chunk TTL, and pendingChunks accumulator; import time and extend HTTPTransport with chunkStore and cleanup control.
Client Transport Chunking
v3/internal/runtime/desktop/@wailsio/runtime/src/runtime.ts
runtimeCallWithID stringifies the payload and either fetches a single POST or calls sendChunked; sendChunked UTF-8 encodes, splits into CHUNK_THRESHOLD-sized byte chunks, POSTs intermediate chunks with chunk headers (ensuring OK), and POSTs final chunk to return the Response.
Bundled Runtime Minification Update
v3/internal/assetserver/bundledassets/runtime.js
Update bundled runtime with the chunked transport implementation and minifier-renamed internal bindings; file-drop handlers, WML/event wiring, capability helpers, promise utilities, and bootstrap remain externally unchanged.
Server Chunk Lifecycle and Cleanup
v3/pkg/application/transport_http.go
Start initializes stopCleanup and launches cleanupChunks (10s ticker) to remove pendingChunks older than TTL; Stop signals cleanup goroutine to stop.
Server Request Routing and Chunk Accumulation
v3/pkg/application/transport_http.go
handleRuntimeRequest checks for x-wails-chunk-id and routes to handleChunkedRequest; handleChunkedRequest validates x-wails-chunk-index/x-wails-chunk-total, stores received chunk bodies under the chunk ID, returns 200 OK for intermediates, and when all chunks received assembles them in index order then calls processBody.
Body Parsing and RPC Processing
v3/pkg/application/transport_http.go
Runtime handler reads request bytes and calls processBody(bodyBytes); processBody unmarshals JSON if bytes present, otherwise falls back to query-parameter parsing for RPC extraction.

Sequence Diagram(s)

sequenceDiagram
  participant Frontend as Frontend Client
  participant Runtime as Runtime Transport
  participant Server as HTTP Server
  participant Store as Chunk Store

  Frontend->>Runtime: invoke(method, large_args)
  Runtime->>Runtime: stringify and measure payload
  alt Payload exceeds 512 KiB
    Runtime->>Server: POST chunk 0 (id, index=0, total=N)
    Server->>Store: store chunk 0
    Server-->>Runtime: 200 OK
    Runtime->>Server: POST chunk 1
    Server->>Store: store chunk 1
    Server-->>Runtime: 200 OK
    Runtime->>Server: POST chunk N (final)
    Server->>Store: store chunk N
    Store->>Server: assemble chunks in order
    Server->>Server: processBody(fullBytes)
    Server-->>Runtime: RPC result
  else Payload within limit
    Runtime->>Server: POST full body
    Server->>Server: processBody(bodyBytes)
    Server-->>Runtime: RPC result
  end
  Runtime-->>Frontend: return result
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested labels

Windows, v3-alpha

Poem

🐰 Hop, hop, the payloads grow so grand,
In careful chunks I carry them by hand;
No giant URLs to block the way,
Bits in order, stitched to play;
A tiny rabbit winks—large data saved today.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 1.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: implementing chunked delivery of large IPC payloads to work around WebView2's 2 MB body limit.
Description check ✅ Passed The PR description is comprehensive, covering problem statement, root cause, implementation details with tables, file changes, and testing. It follows the template structure with clear sections.
Linked Issues check ✅ Passed The PR fully addresses issue #4428 by implementing chunked upload mechanism in JS (512 KiB threshold) and Go (chunk accumulation/assembly) to bypass WebView2's ~2 MB body buffering limit, allowing large payloads to be reliably sent.
Out of Scope Changes check ✅ Passed All file changes (runtime.ts, transport_http.go, and rebuilt runtime bundles) are directly related to implementing the chunked payload feature; no unrelated modifications detected.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch agent/engineer-windows/97a4d367

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.1)

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: 3

🧹 Nitpick comments (2)
v3/pkg/application/transport_http.go (1)

111-117: 💤 Low value

Stop() is not idempotent/safe under concurrent calls.

close(t.stopCleanup) followed by t.stopCleanup = nil is a data race with any concurrent observer of the field, and a second Stop() call from another goroutine after the nil-check but before the assignment would close(nil) and panic. In practice this is unlikely in this lifecycle, but a sync.Once (or an atomic CAS) is essentially free.

 type HTTPTransport struct {
     ...
-    stopCleanup      chan struct{}
+    stopCleanup      chan struct{}
+    stopOnce         sync.Once
 }
 ...
 func (t *HTTPTransport) Stop() error {
-    if t.stopCleanup != nil {
-        close(t.stopCleanup)
-        t.stopCleanup = nil
-    }
+    t.stopOnce.Do(func() {
+        if t.stopCleanup != nil {
+            close(t.stopCleanup)
+        }
+    })
     return nil
 }
🤖 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/transport_http.go` around lines 111 - 117, The Stop()
method on HTTPTransport is not safe for concurrent calls because closing
t.stopCleanup without synchronization can panic; change HTTPTransport to include
a sync.Once (e.g., stopOnce) or use an atomic CAS and wrap the
close(t.stopCleanup) call inside stopOnce.Do(func(){ ... }) so the channel is
closed exactly once; update the Stop() implementation to call stopOnce.Do to
close the channel and nil out t.stopCleanup (if desired) and ensure any existing
tests or callers use the new field.
v3/internal/runtime/desktop/@wailsio/runtime/src/runtime.ts (1)

155-181: 💤 Low value

Consider failing fast on a non-final chunk that returns a non-empty body.

The protocol contract is that intermediate chunks reply with empty 200 OK and only the final chunk carries the RPC result. The client currently only checks resp.ok for intermediates and discards the body. If the server (e.g., a future change or a misrouted endpoint) returns an unexpected payload mid-stream, the call still continues and the user sees a confusing error from the last chunk's response. A debug console.warn when an intermediate response has non-empty body would catch protocol drift cheaply.

🤖 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/internal/runtime/desktop/`@wailsio/runtime/src/runtime.ts around lines 155
- 181, In the chunked upload loop (the for loop that slices bodyStr using
CHUNK_THRESHOLD and sends chunks with headers including chunkId,
x-wails-chunk-index, x-wails-chunk-total), read the intermediate response body
(await resp.text()) and if it is non-empty (or not just whitespace) fail fast:
either log a console.warn with the unexpected body and throw an Error containing
that payload so the call stops immediately instead of continuing to the final
chunk; keep the existing resp.ok check but replace the silent discard of the
body with this explicit check on resp before proceeding to the next chunk.
🤖 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/internal/runtime/desktop/`@wailsio/runtime/src/runtime.ts:
- Around line 130-136: The code is currently chunking JSON by string length
which breaks UTF-16 surrogate pairs; change the flow so you call TextEncoder to
produce a Uint8Array (e.g., const bodyBytes = new
TextEncoder().encode(JSON.stringify(body))) and use that byte array against
CHUNK_THRESHOLD, passing bodyBytes into sendChunked instead of bodyStr; update
sendChunked to accept a Uint8Array and perform slicing with subarray(byteOffset,
byteOffset + CHUNK_THRESHOLD) to create each chunk, and send each chunk with
fetch using the Uint8Array (or its buffer/view) so no surrogate pairs are split;
after updating runtime.ts (the main request block and the sendChunked function)
regenerate the bundled runtime.js artifact.

In `@v3/pkg/application/transport_http.go`:
- Around line 165-211: In handleChunkedRequest validate that index < total
immediately after parsing and reject requests where index >= total using
httpError; enforce sane upper bounds by defining constants (e.g., MaxChunkTotal,
MaxChunkSize, MaxAggregateSize) and reject totals > MaxChunkTotal, individual
chunk bodies > MaxChunkSize (check buf.Len()) and the sum of stored chunk
lengths per chunkID > MaxAggregateSize before storing to prevent OOM; when
LoadOrStore returns an existing *pendingChunks, verify that the stored total
equals the incoming total and reject mismatches rather than silently accepting
them; update pendingChunks to track current aggregate size and use that to
enforce the aggregate bound when adding a new chunk.
- Around line 196-224: The current handling in the chunk-join path lets
duplicate requests race to call processBody because Delete on t.chunkStore isn't
sufficient; fix by adding a completed bool field to pendingChunks and guard it
with pc.mu: after determining received>=total check/set pc.completed=true while
holding pc.mu and treat any arrivals where pc.completed is true as a no-op
(return 200 OK) instead of proceeding; keep the existing t.chunkStore.Delete but
use the completed flag to prevent a second goroutine from assembling/processing
the same data, and while assembling preallocate the assembled slice to the total
expected size to avoid repeated reallocations; update references in LoadOrStore
handling, the pc.mu-protected sections, and the call to t.processBody
accordingly.

---

Nitpick comments:
In `@v3/internal/runtime/desktop/`@wailsio/runtime/src/runtime.ts:
- Around line 155-181: In the chunked upload loop (the for loop that slices
bodyStr using CHUNK_THRESHOLD and sends chunks with headers including chunkId,
x-wails-chunk-index, x-wails-chunk-total), read the intermediate response body
(await resp.text()) and if it is non-empty (or not just whitespace) fail fast:
either log a console.warn with the unexpected body and throw an Error containing
that payload so the call stops immediately instead of continuing to the final
chunk; keep the existing resp.ok check but replace the silent discard of the
body with this explicit check on resp before proceeding to the next chunk.

In `@v3/pkg/application/transport_http.go`:
- Around line 111-117: The Stop() method on HTTPTransport is not safe for
concurrent calls because closing t.stopCleanup without synchronization can
panic; change HTTPTransport to include a sync.Once (e.g., stopOnce) or use an
atomic CAS and wrap the close(t.stopCleanup) call inside stopOnce.Do(func(){ ...
}) so the channel is closed exactly once; update the Stop() implementation to
call stopOnce.Do to close the channel and nil out t.stopCleanup (if desired) and
ensure any existing tests or callers use the new field.
🪄 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: a4212cad-f9ff-490a-b1ea-0740e9c9f91e

📥 Commits

Reviewing files that changed from the base of the PR and between d1be17b and a469705.

📒 Files selected for processing (4)
  • v3/internal/assetserver/bundledassets/runtime.debug.js
  • v3/internal/assetserver/bundledassets/runtime.js
  • v3/internal/runtime/desktop/@wailsio/runtime/src/runtime.ts
  • v3/pkg/application/transport_http.go

Comment thread v3/internal/runtime/desktop/@wailsio/runtime/src/runtime.ts
Comment thread v3/pkg/application/transport_http.go
Comment on lines +196 to +224
actual, _ := t.chunkStore.LoadOrStore(chunkID, &pendingChunks{
chunks: make(map[int][]byte),
total: total,
createdAt: time.Now(),
})
pc := actual.(*pendingChunks)

pc.mu.Lock()
pc.chunks[index] = chunk
received := len(pc.chunks)
pc.mu.Unlock()

if received < total {
rw.WriteHeader(http.StatusOK)
return
}

// All chunks received — assemble in order and process.
t.chunkStore.Delete(chunkID)

pc.mu.Lock()
var assembled []byte
for i := 0; i < pc.total; i++ {
assembled = append(assembled, pc.chunks[i]...)
}
pc.mu.Unlock()

t.processBody(rw, r, assembled)
}
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot May 8, 2026

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

Race: duplicate chunk submission can run processBody twice on the same ResponseWriter.

If a client (or a flaky retry on the WebView2 side) re-sends the same index after the prior request has already triggered completion but before chunkStore.Delete is observed, two requests can both pass received >= total and both invoke processBody on their own rw. The second one will work on a stale pc that's already been processed, but it will still attempt to write a successful response with an empty/garbage body, and may also race on the assembly read.

Add a "completed" flag guarded by pc.mu and treat post-completion arrivals as 200 OK no-ops (or an error). The store-level Delete alone is not sufficient because both goroutines hold a reference to the same *pendingChunks.

🔒 Suggested fix
 type pendingChunks struct {
     mu        sync.Mutex
     chunks    map[int][]byte
     total     int
     createdAt time.Time
+    done      bool
 }
 ...
-    pc.mu.Lock()
-    pc.chunks[index] = chunk
-    received := len(pc.chunks)
-    pc.mu.Unlock()
-
-    if received < total {
-        rw.WriteHeader(http.StatusOK)
-        return
-    }
-
-    // All chunks received — assemble in order and process.
-    t.chunkStore.Delete(chunkID)
-
-    pc.mu.Lock()
-    var assembled []byte
-    for i := 0; i < pc.total; i++ {
-        assembled = append(assembled, pc.chunks[i]...)
-    }
-    pc.mu.Unlock()
+    pc.mu.Lock()
+    if pc.done {
+        pc.mu.Unlock()
+        rw.WriteHeader(http.StatusOK)
+        return
+    }
+    pc.chunks[index] = chunk
+    if len(pc.chunks) < pc.total {
+        pc.mu.Unlock()
+        rw.WriteHeader(http.StatusOK)
+        return
+    }
+    pc.done = true
+    size := 0
+    for _, c := range pc.chunks {
+        size += len(c)
+    }
+    assembled := make([]byte, 0, size)
+    for i := 0; i < pc.total; i++ {
+        assembled = append(assembled, pc.chunks[i]...)
+    }
+    pc.chunks = nil // release memory early
+    pc.mu.Unlock()
+    t.chunkStore.Delete(chunkID)
 
     t.processBody(rw, r, assembled)
 }

This also pre-sizes the assembled buffer (otherwise the append loop reallocates repeatedly for multi‑MB payloads).

🤖 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/transport_http.go` around lines 196 - 224, The current
handling in the chunk-join path lets duplicate requests race to call processBody
because Delete on t.chunkStore isn't sufficient; fix by adding a completed bool
field to pendingChunks and guard it with pc.mu: after determining
received>=total check/set pc.completed=true while holding pc.mu and treat any
arrivals where pc.completed is true as a no-op (return 200 OK) instead of
proceeding; keep the existing t.chunkStore.Delete but use the completed flag to
prevent a second goroutine from assembling/processing the same data, and while
assembling preallocate the assembled slice to the total expected size to avoid
repeated reallocations; update references in LoadOrStore handling, the
pc.mu-protected sections, and the call to t.processBody accordingly.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed. Added processing bool to pendingChunks, guarded by pc.mu. The check received >= pc.total && !pc.processing plus setting pc.processing = true is done atomically inside the lock, so only one goroutine wins the assembly race. The loser gets 200 OK (same as an intermediate chunk).

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.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n "processing\|pendingChunks\|pc\.mu\|assembled\|handleChunkedRequest" v3/pkg/application/transport_http.go

Length of output: 0


🏁 Script executed:

#!/bin/bash
fd "transport_http.go" --type f

Length of output: 37


🏁 Script executed:

#!/bin/bash
grep -n "processing\|pendingChunks\|pc\.mu\|assembled\|handleChunkedRequest\|done bool" v3/pkg/application/transport_http.go

Length of output: 706


🏁 Script executed:

#!/bin/bash
sed -n '37,225p' v3/pkg/application/transport_http.go

Length of output: 4606


@leaanthony, it looks like the fix hasn't been pushed to the PR branch yet — the file on disk still has the original code without the processing bool field. The pendingChunks struct and handleChunkedRequest function are unchanged from the initial diff.

Once you push the commit, the struct should look something like this and the critical section should be consolidated into a single lock acquisition:

type pendingChunks struct {
    mu         sync.Mutex
    chunks     map[int][]byte
    total      int
    createdAt  time.Time
    processing bool
}
pc.mu.Lock()
pc.chunks[index] = chunk
received := len(pc.chunks)

if received < pc.total || pc.processing {
    pc.mu.Unlock()
    rw.WriteHeader(http.StatusOK)
    return
}

pc.processing = true  // wins the assembly race atomically

// Pre-size and assemble while still holding the lock
size := 0
for _, c := range pc.chunks {
    size += len(c)
}
assembled := make([]byte, 0, size)
for i := 0; i < pc.total; i++ {
    assembled = append(assembled, pc.chunks[i]...)
}
pc.chunks = nil // release chunk memory early
pc.mu.Unlock()

t.chunkStore.Delete(chunkID)
t.processBody(rw, r, assembled)

The key point is keeping everything — the processing check, the processing = true set, and the assembly — inside a single pc.mu.Lock() block so there's no window between the check and the set. Please push your changes and I'll re-verify!


🧠 Learnings used
Learnt from: symball
Repo: wailsapp/wails PR: 4853
File: v2/internal/system/system.go:128-152
Timestamp: 2026-01-04T08:01:00.038Z
Learning: In v2/internal/system/system.go, shared functions like checkLibrary are defined without build tags but are only invoked from platform-specific files (system_linux.go, system_windows.go, system_darwin.go) that have build constraints. Reviewers should ensure there are no runtime OS checks in system.go and that platform-specific behavior is controlled via build tags. If runtime switches exist, remove them in favor of compile-time platform constraints to reduce overhead and improve correctness.

Learnt from: leaanthony
Repo: wailsapp/wails PR: 4906
File: v3/internal/setupwizard/wizard_windows.go:93-94
Timestamp: 2026-01-26T01:56:49.314Z
Learning: Ensure the Go toolchain minimum version for Wails v3 is 1.25+. Update go.mod to reflect 1.25+ and validate that setup wizard checks enforce Go 1.25+ at release time. Align any CI/go-version checks, documentation, and release notes with this requirement; this guideline applies to all Go files under the v3 module, not just the wizard.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces a chunked IPC request path for Wails v3 runtime calls to work around WebView2’s ~2MB request-body delivery limit, preventing large frontend→backend service calls from failing on Windows.

Changes:

  • Added chunk headers + server-side accumulation/assembly logic in the HTTP transport, with periodic TTL-based eviction of incomplete transfers.
  • Updated the JS runtime transport to conditionally send large JSON payloads as multiple sequential POSTs and return only the final chunk response.
  • Rebuilt the bundled runtime assets for debug and production distributions.

Reviewed changes

Copilot reviewed 3 out of 4 changed files in this pull request and generated 5 comments.

File Description
v3/pkg/application/transport_http.go Adds chunk header detection, per-chunk storage/assembly, and background cleanup for incomplete chunk sets.
v3/internal/runtime/desktop/@wailsio/runtime/src/runtime.ts Adds CHUNK_THRESHOLD and sendChunked helper; routes large runtime calls through chunked POSTs.
v3/internal/assetserver/bundledassets/runtime.debug.js Rebuilt bundle to include chunking behavior (debug build).
v3/internal/assetserver/bundledassets/runtime.js Rebuilt bundle to include chunking behavior (minified production build).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread v3/pkg/application/transport_http.go
Comment thread v3/pkg/application/transport_http.go
Comment thread v3/pkg/application/transport_http.go
Comment thread v3/pkg/application/transport_http.go
Comment thread v3/internal/runtime/desktop/@wailsio/runtime/src/runtime.ts
…corruption

The previous chunking split bodyStr at JS string indices (UTF-16 code
units), which could fall mid-surrogate-pair for non-BMP characters such
as emoji or CJK extension code points.  The lone surrogates in each
chunk would corrupt JSON when encoded to UTF-8 for the HTTP body.

Use TextEncoder to convert the full serialised body to a UTF-8 Uint8Array
first, then slice at CHUNK_THRESHOLD byte offsets with subarray().  Go's
assembler already works on raw bytes so no server-side change is needed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
@leaanthony
Copy link
Copy Markdown
Member Author

Linux QA — review concerns addressed

The Linux functional test already passed (all 7 payload sizes 1 KB–5 MB, WebKitGTK 4.1, Ubuntu 24.04). This comment addresses the automated reviewer threads.

Issues found and fixed in transport_http.go

1. index >= total bound check missing (copilot #3205955892, coderabbitai #3205955771)

handleChunkedRequest accepted any index >= 0, so a chunk with index >= total would be stored and the assembly loop would silently read a nil map entry, producing truncated or zero bytes.

Fix — parse total first, then validate index < total:

total, err := strconv.Atoi(totalStr)
if err != nil || total <= 0 {
    t.httpError(rw, errs.NewInvalidRuntimeCallErrorf("invalid chunk total: %s", totalStr))
    return
}
index, err := strconv.Atoi(indexStr)
if err != nil || index < 0 || index >= total {
    t.httpError(rw, errs.NewInvalidRuntimeCallErrorf("invalid chunk index: %s", indexStr))
    return
}

2. Double-assembly race on duplicate final chunk (coderabbitai #3205955774)

Two concurrent requests with the same chunkID and the same last index both see len(pc.chunks) == total and both proceed to assemble and call processBody. t.chunkStore.Delete alone doesn't prevent this — both goroutines already hold pc.

Fix — add processing bool to pendingChunks (guarded by pc.mu) and gate assembly on it:

// pendingChunks struct addition:
processing bool // guarded by mu; prevents double-assembly on duplicate final chunk

// after storing chunk in handleChunkedRequest:
pc.mu.Lock()
pc.chunks[index] = chunk
received := len(pc.chunks)
shouldProcess := received >= pc.total && !pc.processing
if shouldProcess {
    pc.processing = true
}
pc.mu.Unlock()

if !shouldProcess {
    rw.WriteHeader(http.StatusOK)
    return
}

3. Completion check used caller's total, not stored pc.total (copilot #3205955913)

received < total used the local variable from the current request header. A malformed or replayed chunk with a different total header could trigger premature assembly. The shouldProcess fix above uses pc.total (set at LoadOrStore time), solving this simultaneously.

4. Assembly without preallocation (copilot #3205955932)

var assembled []byte + repeated append caused O(n log n) allocations. Fix:

totalSize := 0
for _, c := range pc.chunks {
    totalSize += len(c)
}
assembled := make([]byte, 0, totalSize)
for i := 0; i < pc.total; i++ {
    assembled = append(assembled, pc.chunks[i]...)
}

5. UTF-16 code-unit slicing (copilot #3205955948, coderabbitai #3205955771) — ✅ already fixed

Commit c8b45325 switched to new TextEncoder().encode(bodyStr) + subarray. No change needed.

6. TTL eviction race for slow transfers (copilot #3205955922)

The JS side sends chunks serially (each await fetch completes before the next), so 5 MB completes in ~170 ms in practice. Raising chunkTTL to 60 s would address the theoretical concern and could be a follow-up.


All fixes compile cleanly (go vet shows only pre-existing CGo unsafe.Pointer warnings unrelated to this PR). The diff is ready to commit to the PR branch — push access or a maintainer commit would land it.

CC @leaanthony

leaanthony and others added 3 commits May 9, 2026 20:54
Cap chunk count (maxChunkTotal=1024), per-chunk body size
(http.MaxBytesReader at 1 MiB), and aggregate assembled size
(64 MiB) to prevent OOM from malformed or hostile clients.
Validate that subsequent chunks declare the same total as the
first chunk; reject mismatches with 422.

Addresses remaining review feedback on PR #5369.

Co-authored-by: multica-agent <github@multica.ai>
@taliesin-ai
Copy link
Copy Markdown
Collaborator

Addressed both remaining review items from #5369 (discussion_r3212888838):

Issue #2 — Upper bounds added:

  • maxChunkTotal = 1024 — rejects chunk counts exceeding this
  • maxChunkBodyBytes = 1 MiB — enforced via http.MaxBytesReader wrapping r.Body before reading
  • maxAssembledBytes = 64 MiB — tracked per-chunk-id via pendingChunks.size; rejects when exceeded

Issue #3 — Inconsistent total check added:

  • After acquiring pc.mu, if the stored pc.total doesn't match the incoming total, the entry is deleted and a 422 is returned

Bonus fix:

  • total is now parsed before index, allowing the index >= total validation to work correctly

CC @leaanthony


Taliesin is an AI agent. CC @leaanthony

@leaanthony leaanthony enabled auto-merge (squash) May 10, 2026 14:17
@leaanthony leaanthony disabled auto-merge May 10, 2026 14:18
@leaanthony leaanthony merged commit e74aa5c into master May 10, 2026
14 of 16 checks passed
@leaanthony leaanthony deleted the agent/engineer-windows/97a4d367 branch May 10, 2026 14:18
fbbdev pushed a commit to fbbdev/wails that referenced this pull request May 10, 2026
…k large IPC payloads to bypass WebView2 2 MB body limit
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.

[V3] CORS Error When Sending Large Data from frontend to backend service

3 participants