wailsapp/wails · error · Error

${await resp.text()}

Error message

${await resp.text()}

What it means

GlobalAlloc allocates memory from the heap and returns an HGLOBAL handle; NULL means allocation failed. The w32 wrapper panics with 'GlobalAlloc failed' on NULL. With GMEM_MOVEABLE the call allocates a handle plus block; with GMEM_FIXED the handle is a direct pointer. Failure means out-of-memory or, rarely, invalid flags combinations such as requesting GMEM_FIXED together with GMEM_DISCARDABLE.

Source

Thrown at v3/internal/runtime/desktop/@wailsio/runtime/src/runtime.ts:212

async function sendChunked(url: URL, headers: Record<string, string>, bodyStr: string): Promise<Response> {
    const chunkId = nanoid();
    const bodyBytes = new TextEncoder().encode(bodyStr);
    const totalChunks = Math.ceil(bodyBytes.length / CHUNK_THRESHOLD);

    for (let i = 0; i < totalChunks - 1; i++) {
        const chunk = bodyBytes.subarray(i * CHUNK_THRESHOLD, (i + 1) * CHUNK_THRESHOLD);
        const resp = await fetch(url, {
            method: 'POST',
            headers: {
                ...headers,
                'x-wails-chunk-id': chunkId,
                'x-wails-chunk-index': String(i),
                'x-wails-chunk-total': String(totalChunks),
            },
            body: chunk,
        });
        if (!resp.ok) {
            throw new Error(await resp.text());
        }
    }

    return fetch(url, {
        method: 'POST',
        headers: {
            ...headers,
            'x-wails-chunk-id': chunkId,
            'x-wails-chunk-index': String(totalChunks - 1),
            'x-wails-chunk-total': String(totalChunks),
        },
        body: bodyBytes.subarray((totalChunks - 1) * CHUNK_THRESHOLD),
    });
}

/**
 * Android WebView cannot deliver fetch() POST bodies to
 * shouldInterceptRequest, so the default HTTP transport cannot reach Go.

View on GitHub (pinned to 0e754b1b40)

Solutions

  1. Check process memory usage at failure time; if the requested dwBytes is huge, stream the data in chunks instead of one allocation.
  2. Audit for missing GlobalFree on earlier allocations — pair every successful GlobalAlloc with a free via defer.
  3. Validate flags: use GMEM_MOVEABLE for clipboard/HGLOBAL consumers, GMEM_FIXED only when a raw pointer is fine; never mix GMEM_FIXED with discardable flags.
  4. On 32-bit builds, move to 64-bit or reduce payload size if the request itself exceeds address space.
  5. Wrap the allocation in a recover() guard if a graceful degradation (e.g. cancel the copy) is preferable to a crash.

Example fix

// before
h := w32.GlobalAlloc(w32.GMEM_MOVEABLE, uint32(len(data))) // 500MB clipboard blob -> panic

// after
const maxClip = 100 << 20
if len(data) > maxClip {
	return fmt.Errorf("payload too large for clipboard: %d bytes", len(data))
}
h := w32.GlobalAlloc(w32.GMEM_MOVEABLE, uint32(len(data)))
if h == 0 {
	return fmt.Errorf("GlobalAlloc failed for %d bytes", len(data))
}
Defensive patterns

Strategy: validation

Validate before calling

const maxAlloc = 64 << 20
func allocatable(size uint32) bool { return size > 0 && size <= maxAlloc }

Try / catch

func safeGlobalAlloc(flags uint, size uint32) (h w32.HGLOBAL, err error) {
	defer func() {
		if r := recover(); r != nil {
			err = fmt.Errorf("GlobalAlloc(%d): %v", size, r)
		}
	}()
	return w32.GlobalAlloc(flags, size), nil
}

Prevention

When it happens

Trigger: Allocating a buffer for clipboard data (SetClipboardData) or a stream (CreateStreamOnHGlobal) sized in the hundreds of MB on a memory-constrained machine; passing a bogus flag combination; running under a 32-bit process near the 2GB ceiling.

Common situations: Clipboard copy features that stage large images/text; a custom file-dialog or drag-drop data path serializing big payloads into HGLOBAL; a leak of prior GlobalAlloc blocks (missing GlobalFree) driving the process toward the commit limit so the next allocation panics.

Related errors


AI-assisted analysis of wailsapp/wails@0e754b1b40 (2026-08-15). Data as JSON: /api/errors/78ea275c54096b30. Report an issue: GitHub.