Skip to content

fix(v3): make asset server flush semantics honest - #5931

Merged
leaanthony merged 4 commits into
masterfrom
fix/assetserver-flush-semantics
Aug 9, 2026
Merged

fix(v3): make asset server flush semantics honest#5931
leaanthony merged 4 commits into
masterfrom
fix/assetserver-flush-semantics

Conversation

@leaanthony

@leaanthony leaanthony commented Aug 9, 2026

Copy link
Copy Markdown
Member

Description

Three problems in the asset server, found while investigating Go→JS event dispatch (#5930) and independent of it. Split out so they can be reviewed and land on their own.

1. contentTypeSniffer.Flush() never completed itself

Flush delegated straight to the wrapped writer. But until 512 bytes have been seen the sniffer is deliberately holding the body back so it can detect a Content-Type — so a flush before that point sent nothing and reported success.

Any response that writes a short chunk and then waits — every streaming format — silently delivered nothing to the page.

Flush now completes first, sniffing from the short prefix. That's the right reading of an explicit flush: send what has been written so far.

2. The platform response writers didn't implement http.Flusher

None of the five did, so http.ResponseController(w).Flush() could never reach them.

On macOS, iOS and Linux that was only a missing declaration — Write already hands each chunk to the URL scheme task (didReceiveData) or into the pipe WebKitGTK consumes, so a flush has no work to do. Those now declare a documented no-op.

Windows deliberately does not, and this is the interesting part. Its Write accumulates into an in-memory bytes.Buffer that is handed to WebView2 only in Finish, so nothing can reach the page mid-response. A no-op Flush there would claim a capability that doesn't exist and make ResponseController.Flush report success while nothing is delivered. Left unimplemented, it keeps returning ErrNotSupported — which is the truth.

That also means streaming cannot work on Windows today regardless of what WebView2 supports. Worth knowing before designing around it: this is host-side, not an engine limitation.

3. dispatchWorkers is declared and read but never assigned

It reads like an oversight. It isn't — it must stay 0.

At 0, every request gets its own goroutine. Any positive value switches ServeWebViewRequest to a fixed worker pool, where a long-lived request (a streaming response, or anything blocking on the frontend) holds its worker for its entire lifetime. Enough of those starve the pool: later requests — including the page's own assets — queue behind them, and the app appears to hang at startup with no error anywhere.

Documented rather than removed, since the pooled path is still useful for a request-heavy workload known to be short-lived. It needs a bound on request lifetime before it can be switched on.

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • WEP (proposal only; no implementation)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

How Has This Been Tested?

Six new tests in content_type_sniffer_test.go; three fail without the fix:

test covers
FlushReleasesShortPrefix the bug — a sub-512-byte prefix reaches the client on flush
WritesPassThroughAfterFlush later writes aren't re-buffered
FlushBeforeWrite flushing with nothing written is a no-op
FlushWithExplicitContentType an explicit type disables sniffing and survives flush
FullPrefixStillCompletesWithoutFlush existing 512-byte behaviour unchanged
SupportsResponseController http.ResponseController finds the flusher
go test ./internal/assetserver/...

Builds verified for linux, windows and ios in addition to the host, since four of the eight files are platform-specific.

  • Windows (cross-compiled; the change there is a comment plus the deliberate absence of Flush)
  • macOS
  • Linux (cross-compiled)

Test Configuration

Wails CLI    : v3.0.0-dev
Go           : go1.26.2
Platform     : darwin/arm64, macOS 26.4.1 (25E253)

Checklist:

  • (v2 only) I have updated website/src/pages/changelog.mdx with details of this PR (v3 changelog entries are added automatically)
  • My code follows the general coding style of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes

Summary by CodeRabbit

  • Bug Fixes

    • Improved response flushing so buffered content is delivered promptly when supported.
    • Improved handling of partial writes and response-stream errors.
    • Enhanced compatibility with response-flushing controls across supported platforms.
    • Preserved existing platform behavior where responses are intentionally buffered.
  • Documentation

    • Clarified worker-pool configuration guidance and the risks of long-lived requests exhausting available workers.
  • Tests

    • Added coverage for content detection, flushing, explicit content types, partial writes, failures, and response-controller compatibility.

Three related problems in the asset server, all found while investigating
Go->JS event dispatch and all independent of it.

contentTypeSniffer.Flush delegated to the wrapped writer without first
completing itself. Until 512 bytes have been seen the sniffer deliberately
holds the body back so it can detect a Content-Type, so a flush before that
point sent nothing and reported success. Any response that writes a short
chunk and then waits - every streaming format - silently delivered nothing.
Flush now completes first, sniffing from the short prefix, which is the
right reading of an explicit flush: send what has been written so far.

The platform response writers did not implement http.Flusher at all, so
http.ResponseController(w).Flush() could never reach them. On macOS, iOS
and Linux that was only a missing declaration: Write already pushes each
chunk straight to the URL scheme task or into the pipe WebKitGTK reads,
so a flush has no work to do. Those now declare a documented no-op.

Windows deliberately does not, and this is the interesting one. Its Write
accumulates into an in-memory bytes.Buffer that is handed to WebView2 only
in Finish, so nothing can reach the page mid-response. A no-op Flush there
would claim a capability that does not exist. Left unimplemented,
ResponseController.Flush keeps returning ErrNotSupported, which is true.
It also means streaming cannot work on Windows today regardless of what
WebView2 supports - worth knowing before designing around it.

dispatchWorkers is declared and read but never assigned, which reads like
an oversight and is not: it must stay 0. At 0 each request gets its own
goroutine. Any positive value switches to a fixed worker pool, where a
long-lived request holds its worker for its whole lifetime and enough of
them starve the pool - later requests, including the page's own assets,
queue behind them and the app appears to hang at startup with no error
anywhere. Documented rather than removed, since the pooled path is still
useful for workloads known to be short-lived.

Tests cover the sniffer: a short prefix is released on flush, later writes
pass through, flushing before any write is a no-op, an explicit
Content-Type is untouched, the full-prefix path still completes on its own,
and http.ResponseController finds the flusher. Three of them fail without
the fix.
Copilot AI lite review requested due to automatic review settings August 9, 2026 14:05
@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 2d8c6a05-4a52-4e8f-b413-cb782adb1dee

📥 Commits

Reviewing files that changed from the base of the PR and between 73e3f18 and 05ca219.

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

Walkthrough

The asset server now preserves content-type sniffer errors and unwritten prefixes during flushing. Supported webview writers expose http.Flusher. Windows documents unsupported streaming. The dispatchWorkers field documents fixed-pool constraints.

Changes

Asset server response flow

Layer / File(s) Summary
Preserve sniffer errors and flush buffered data
v3/internal/assetserver/content_type_sniffer.go, v3/internal/assetserver/content_type_sniffer_test.go
contentTypeSniffer retains errors and unwritten prefixes. Flush completes sniffing, writes buffered data, and delegates to the wrapped flusher. Tests cover successful, failed, and short writes.
Expose platform-specific response flushing
v3/internal/assetserver/webview/responsewriter_*.go
Darwin, iOS, Linux, and GTK3 add no-op Flush methods. Windows documents buffered responses and unsupported streaming.
Document dispatch worker behavior
v3/internal/assetserver/assetserver_webview.go
The documentation describes zero-worker defaults, fixed worker pools, and blocking from long-lived requests.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant HTTPHandler
  participant contentTypeSniffer
  participant WrappedWriter
  HTTPHandler->>contentTypeSniffer: Flush()
  contentTypeSniffer->>contentTypeSniffer: Complete content-type sniffing
  contentTypeSniffer->>WrappedWriter: Write buffered prefix
  contentTypeSniffer->>WrappedWriter: Flush()
Loading

Possibly related PRs

Poem

A rabbit guards the buffered stream,
Errors stay where writes have been.
Flush sends prefixes on their way,
Webviews mark the stream-ready day.
Windows waits for Finish to bloom.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the primary asset server flush-semantics fix.
Description check ✅ Passed The description covers the changes, motivation, testing, platforms, configuration, and checklist; the issue link and wails doctor output remain unspecified.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ 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 fix/assetserver-flush-semantics

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.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

🤖 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/assetserver/content_type_sniffer.go`:
- Around line 149-155: Update contentTypeSniffer.Flush and the
contentTypeSniffer state to retain any error returned by complete instead of
discarding it, and have subsequent Write calls and completion attempts return
the stored failure before proceeding. Preserve normal flushing behavior while
adding a writer test whose prefix write fails only during Flush and verifies the
error is later surfaced.
🪄 Autofix

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 Plus

Run ID: c1a20fa7-e68a-4268-bf0a-b6470f4a8827

📥 Commits

Reviewing files that changed from the base of the PR and between 07cbabf and 3632de5.

📒 Files selected for processing (8)
  • v3/internal/assetserver/assetserver_webview.go
  • v3/internal/assetserver/content_type_sniffer.go
  • v3/internal/assetserver/content_type_sniffer_test.go
  • v3/internal/assetserver/webview/responsewriter_darwin.go
  • v3/internal/assetserver/webview/responsewriter_ios.go
  • v3/internal/assetserver/webview/responsewriter_linux.go
  • v3/internal/assetserver/webview/responsewriter_linux_gtk3.go
  • v3/internal/assetserver/webview/responsewriter_windows.go

Comment thread v3/internal/assetserver/content_type_sniffer.go

Copilot AI left a comment

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.

Pull request overview

This PR corrects flushing behavior in the v3 asset server so that Flush() actually releases buffered bytes when Content-Type sniffing is in progress, and so http.ResponseController.Flush() accurately reflects platform capabilities (including explicitly not claiming streaming support on Windows). It also documents why dispatchWorkers must remain 0 to avoid starvation/hangs with long-lived requests.

Changes:

  • Fix contentTypeSniffer.Flush() to complete sniffing and release any buffered prefix before delegating to the wrapped writer’s flush.
  • Declare no-op Flush() implementations for non-Windows platform WebView response writers so http.ResponseController can flush successfully where streaming is already effectively “write-through”.
  • Document Windows’ intentional lack of http.Flusher and document dispatchWorkers staying 0 to prevent worker-pool starvation.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
v3/internal/assetserver/webview/responsewriter_windows.go Documents intentional non-support for http.Flusher to avoid dishonest flush semantics on Windows.
v3/internal/assetserver/webview/responsewriter_linux.go Adds no-op Flush() so http.ResponseController.Flush() can succeed on Linux GTK4/WebKitGTK.
v3/internal/assetserver/webview/responsewriter_linux_gtk3.go Adds no-op Flush() so http.ResponseController.Flush() can succeed on Linux GTK3 legacy path.
v3/internal/assetserver/webview/responsewriter_ios.go Adds no-op Flush() so http.ResponseController.Flush() can succeed on iOS.
v3/internal/assetserver/webview/responsewriter_darwin.go Adds no-op Flush() so http.ResponseController.Flush() can succeed on macOS.
v3/internal/assetserver/content_type_sniffer.go Updates Flush() to resolve and release the sniffing prefix so short streaming writes aren’t silently withheld.
v3/internal/assetserver/content_type_sniffer_test.go Adds unit tests covering flush behavior, buffering semantics, and http.ResponseController integration.
v3/internal/assetserver/assetserver_webview.go Documents dispatchWorkers being intentionally kept at 0 to avoid starvation with long-lived requests.
Suppressed comments (2)

v3/internal/assetserver/content_type_sniffer_test.go:79

  • This test ignores the error return from io.WriteString. Checking it makes the test fail fast if the writer begins returning errors, rather than continuing with potentially invalid assertions.
	rw.Header().Set(HeaderContentType, "text/event-stream")
	rw.WriteHeader(http.StatusOK)
	_, _ = io.WriteString(rw, "data: x\n\n")
	rw.Flush()

v3/internal/assetserver/content_type_sniffer_test.go:116

  • This test ignores the error return from io.WriteString. If writes start failing, ResponseController.Flush might still return an error and the body assertions become misleading; checking the write error makes the test more robust.
	rw.WriteHeader(http.StatusOK)
	_, _ = io.WriteString(rw, "chunk")


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread v3/internal/assetserver/content_type_sniffer.go
Comment thread v3/internal/assetserver/content_type_sniffer_test.go
Review found a real hole in the previous commit. complete cleared the whole
prefix regardless of what the wrapped Write actually accepted, and Flush
discarded the error because http.Flusher cannot report one. A writer that
failed the flush-triggered write therefore lost the buffered bytes and the
failure with them: the request-end complete saw an empty prefix and
reported success.

complete now advances the prefix only by the number of bytes the writer
accepted, so a short or failed write leaves the remainder for a later
attempt, and records the error on the sniffer. Write and complete both
return that error afterwards, which is how the failure reaches the caller
now that Flush cannot.

Two tests added, both failing without the change: one fails only the
flush-triggered prefix write and asserts the error surfaces from the next
Write and from complete, the other uses a short writer and asserts only
the unsent remainder is kept. Existing tests also stopped ignoring errors
from io.WriteString, so a writer that starts failing can no longer let a
test pass while skipping its assertions.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

🤖 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/assetserver/content_type_sniffer_test.go`:
- Around line 193-204: Update shortWriter.Write to return io.ErrShortWrite
whenever it truncates the input because len(b) exceeds w.limit, while preserving
the underlying ResponseWriter error and byte count for non-truncated writes.
🪄 Autofix

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 Plus

Run ID: ed178421-b21e-4b76-927a-bb102f19730a

📥 Commits

Reviewing files that changed from the base of the PR and between 3632de5 and 73e3f18.

📒 Files selected for processing (2)
  • v3/internal/assetserver/content_type_sniffer.go
  • v3/internal/assetserver/content_type_sniffer_test.go

Comment thread v3/internal/assetserver/content_type_sniffer_test.go Outdated
shortWriter returned a short count with a nil error, which io.Writer
forbids: an implementation must return a non-nil error whenever it accepts
fewer bytes than it was offered. It now reports io.ErrShortWrite, which
also makes the test exercise the realistic path - the sniffer records that
error and surfaces it from the next Write - rather than a state a
conforming writer could never produce.

The test now asserts all three: the accepted bytes reach the client, the
unsent remainder is retained, and the error reaches the caller.
@leaanthony
leaanthony merged commit d79d990 into master Aug 9, 2026
48 checks passed
@leaanthony
leaanthony deleted the fix/assetserver-flush-semantics branch August 9, 2026 14:35
leaanthony pushed a commit that referenced this pull request Aug 9, 2026
@taliesin-ai taliesin-ai added this to the v3.0.0-beta.3 milestone Aug 10, 2026
@github-project-automation github-project-automation Bot moved this from Todo to Done in Wails Release Train Aug 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

3 participants