Skip to content

feat(plugin): implement chunked body fetching and error handling for … - #18

Merged
pikann merged 2 commits into
masterfrom
feature/implement-chunked-body-fetching
Nov 3, 2026
Merged

pikann merged 2 commits into
masterfrom
feature/implement-chunked-body-fetching

Conversation

@pikann

@pikann pikann commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Summary

A WASM plugin could not fetch or return anything larger than about 1 MiB, and when it hit that limit it failed silently: Fetch reported plugin: fetch: empty response from host, which reads like a network outage, and a plugin's own oversized result came back to the host as nothing at all.

This PR removes both limits and, where a limit genuinely applies, turns the silent failure into an explicit, typed error. It is the SDK half of #16 (the host half lives in Paca core's plugin runtime).

Why

The SDK has one fixed 1 MiB arena (mallocBuffer) that the host writes request data and host-function results into. Everything shared it:

  • Fetch responses. The JSON envelope ({"status","body","headers"}) had to fit in what was left of the arena. When it did not, the host had no room even for an error message and returned an empty response. A 1,168,331-byte GitHub issue page produced a 1,238,310-byte envelope and failed; the same code with a smaller page worked.
  • Plugin results. packWASMResult copied a plugin's response back into the same arena and returned 0 when it did not fit. For an HTTP-shaped response the body is base64-encoded, so a 902 KB provider result became a 1.2 MB response and was silently dropped. Core then reported the provider as "temporarily unavailable" (HTTP 503).
  • Prior allocations. Only Fetch and GetBranding released the arena after reading a result. DB, KV, cache and config reads never did, so earlier host calls in the same request ate the space a later one needed.

Changes

Chunked fetch (fetch_chunks.go, wasm_backends.go)

  • Fetch now sends "chunked": true. If the whole envelope fits, nothing changes (one round trip, same result). If not, the host parks the body and answers with the status, headers, a body_handle and body_size; the SDK then reads the body in 256 KiB pieces (base64 in the JSON envelope, so it is binary-safe) and returns the complete FetchResponse.Body.
  • If a piece does not fit because the arena is partly used, the chunk size is halved and retried, down to 4 KiB.
  • Fetch keeps its signature and result. Callers get large responses without code changes.

Explicit errors (host_limits.go)

  • New ErrResponseTooLarge, returned wrapped (errors.Is(err, plugin.ErrResponseTooLarge)), for a result that cannot be delivered. FetchResponse gains a Code field carrying the host's response_too_large classification.
  • The old "empty response from host" message is replaced by one that says the result may not have fit or the host call failed, and that this does not necessarily mean the network is down.

Large plugin results (wasm_exports.go, wasm_result.go)

  • packWASMResult now returns the result from the guest heap instead of the arena. The host reads a result from whatever memory address it is given, so results are no longer bounded by the arena (this works with existing hosts too).
  • A result over 16 MiB is replaced by an explicit error in that export's own shape (an HTTP 500 response, or the Error field of a condition or action result) instead of an empty reply.

Arena hygiene

  • DB, KV, cache and config reads now release the arena after copying their result out, as Fetch already did.
  • HandleEvent copies the event payload, since a handler's host calls may now reset the arena while Event.Payload still pointed into it.

Compatibility

SDK Host Result
this updated core large fetches work; real failures are explicit
this older core chunked is ignored; an oversized fetch fails with the clearer "host returned no data" message instead of the misleading one. Large plugin results already work
older updated core no chunked flag, so an oversized fetch gets an explicit response_too_large error, not an empty response

No wire-contract change to the Git provider SPI, and no new required host function.

Notes and limits

  • The whole body is held in plugin memory (and the host keeps its existing 50 MiB per-response cap), so a very large response costs plugin memory.
  • The host keeps at most two unread parked bodies per plugin for 60 seconds.
  • Chunks travel as base64, about 33% larger than the raw bytes.

Tests

  • fetch_chunks_test.go: reassembling a body across many chunks, shrinking the chunk size when the arena is partly full, giving up at the minimum with ErrResponseTooLarge, empty or short chunks not looping forever, and the wire shapes.
  • host_limits_test.go: error classification and the new wording.
  • wasm_result_test.go: results at the 16 MiB limit pass; one byte over is replaced by an explicit error.
  • go vet, go test -race ./... and GOOS=wasip1 GOARCH=wasm go vet . all pass.
  • Verified end to end by running an SDK-built plugin through Paca's real plugin runtime: a 5 MB fetched body, the real 1.1 MB GitHub issue page, and a 4 MB plugin result all arrived intact, including on repeated calls.

@pikann pikann linked an issue Nov 3, 2026 that may be closed by this pull request
Validate announced body_size, reject overruns, build the body without a
second copy, retry chunk reads on empty host replies, drop the doubled
op prefix in host errors, and reuse errEmptyHostResponse in
CallHostFunction.
@pikann
pikann merged commit fb2b014 into master Nov 3, 2026
4 checks passed
@pikann
pikann deleted the feature/implement-chunked-body-fetching branch November 3, 2026 10:33
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.

Handle oversized WASM host responses without misleading fetch errors

1 participant