wailsapp/wails · error
invalid stream batch acknowledgement
Error message
invalid stream batch acknowledgement
What it means
LockResource maps loaded resource data into a pointer; it returns NULL on failure. The w32 wrapper panics with 'LockResource failed' on NULL. The documented contract is that LockResource fails when the HGLOBAL passed in was not produced by LoadResource, or the underlying resource data could not be mapped. Notably, resources loaded via LoadResource do not need unlocking — there is no UnlockResource — so the failure almost always means the handle is wrong or the load failed earlier.
Source
Thrown at v3/internal/runtime/desktop/@wailsio/runtime/src/stream.ts:782
[HDR_KIND]: String(KIND_DATA),
[HDR_BATCH]: String(frames.length - sent),
"Content-Type": "application/octet-stream",
},
body: body as BodyInit,
signal,
});
if (resp.ok) return;
if (resp.status !== 429) {
throw new Error(await resp.text());
}
// The receiver took a prefix of the batch. Resend only what is left,
// which keeps ordering without re-delivering anything.
const accepted = Number(resp.headers.get(HDR_BATCH) ?? 0);
const remaining = frames.length - sent;
if (!Number.isInteger(accepted) || accepted < 0 || accepted >= remaining) {
// Zero is valid (no progress), but an out-of-range acknowledgement
// is a protocol error. Keep zero on the retry path below.
if (accepted !== 0) throw new Error("invalid stream batch acknowledgement");
}
sent += accepted;
body = buildBatch(frames.slice(sent));
await sleep(wait, signal);
wait = Math.min(wait * 2, 50);
}
}
function buildBatch(frames: Uint8Array[]): Uint8Array {
let size = 4;
for (const f of frames) size += 4 + f.byteLength;
const out = new Uint8Array(size);
const view = new DataView(out.buffer);
view.setUint32(0, frames.length);
let off = 4;
for (const f of frames) {
view.setUint32(off, f.byteLength);
off += 4;View on GitHub (pinned to 0e754b1b40)
Solutions
- Check LoadResource's return (non-zero) before LockResource, and FindResource before that — validate the whole chain.
- Ensure the HGLOBAL comes from LoadResource(hModule, hResInfo), never from GlobalAlloc.
- Keep the owning module loaded (defer FreeLibrary) for as long as the locked resource pointer is in use.
- Remember there is no unlock: treat the pointer as valid for the module's lifetime.
Example fix
// before
hRes := w32.LoadResource(hMod, hrs)
p := w32.LockResource(hRes) // hRes==0 unchecked in some path -> panic
// after
hRes := w32.LoadResource(hMod, hrs)
if hRes == 0 {
return errors.New("LoadResource failed")
}
p := w32.LockResource(hRes)
if p == nil {
return errors.New("LockResource failed")
} Defensive patterns
Strategy: validation
Prevention
- Validate FindResource and LoadResource returns (non-zero) before LockResource.
- Only pass HGLOBALs that came from LoadResource, never GlobalAlloc.
- Keep the owning module loaded while the resource pointer is in use.
When it happens
Trigger: Passing an HGLOBAL from GlobalAlloc instead of LoadResource; using the HGLOBAL after the module that owns the resource was freed by FreeLibrary; chaining from a FindResource whose failure was ignored so LoadResource got a null HRSRC and returned 0 (panicking first at LoadResource).
Common situations: Custom asset loaders reading embedded RCDATA where the LoadResource step's return was not checked in an older code path; unloading a plugin DLL while its icon data pointer is still in use.
Related errors
- no element matches selector: ' + target.selector
- Invalid JSON passed to callback: ${e.message}. Message: ${in
- Callback '${callbackID}' not registered!!!
- Invalid JSON passed to Notify: ${notifyMessage}
- CancellablePromise does not support transparent subclassing.
AI-assisted analysis of wailsapp/wails@0e754b1b40 (2026-08-15).
Data as JSON: /api/errors/cee632dadf624b45.
Report an issue: GitHub.