Skip to content

fix(v3/windows): fixes the 502 Bad Gateway errors that occur when using Vite as the frontend dev server in development mode#5265

Merged
leaanthony merged 3 commits into
wailsapp:masterfrom
AkagiYui:v3-alpha-bugfix/issue-4556
Apr 29, 2026
Merged

fix(v3/windows): fixes the 502 Bad Gateway errors that occur when using Vite as the frontend dev server in development mode#5265
leaanthony merged 3 commits into
wailsapp:masterfrom
AkagiYui:v3-alpha-bugfix/issue-4556

Conversation

@AkagiYui

@AkagiYui AkagiYui commented Apr 28, 2026

Copy link
Copy Markdown
Contributor

Description

This PR fixes the 502 Bad Gateway errors that occur when using Vite as the frontend dev server in development mode, particularly with projects that have many dynamic imports.

Problem: When a Vite project has many dynamic imports (e.g., lucide-solid icons), it generates hundreds of concurrent HTTP requests during page load. The Vite dev server may temporarily exhaust its connection queue, causing TCP connections to be refused. The Wails proxy's ErrorHandler immediately returned 502 without any retry mechanism.

Solution: Added a retryTransport struct that implements http.RoundTripper with retry logic:

  • Retries failed connections up to 50 times with 50ms delay between attempts
  • Only retries on transient connection errors (connection refused, connection reset, broken pipe, connectex)
  • Forces IPv4 for localhost connections to avoid IPv6 issues on Windows
  • Configures custom dialer with timeout settings

Fixes #4556

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • 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?

Tested with a Vite + SolidJS project using lucide-solid icon library (hundreds of dynamic imports).
Execute location.reload() in WebView console.

Before fix: ~50+ 502 errors during initial page load
After fix: All requests succeed after retries, no 502 errors in browser console

  • Windows
  • macOS
  • Linux

Test Configuration

 Wails (v3.0.0-alpha.78)  Wails Doctor 

# System 

┌────────────────────────────────────────────────────────────────────────────────────────────┐
| Name              | Windows 10 Enterprise                                                  |
| Version           | 2009 (Build: 22621)                                                    |
| ID                | 22H2                                                                   |
| Branding          | Windows 11 企业版                                                      |
| Platform          | windows                                                                |
| Architecture      | amd64                                                                  |
| Go WebView2Loader | true                                                                   |
| WebView2 Version  | 147.0.3912.86                                                          |
| CPU               | 13th Gen Intel(R) Core(TM) i5-13600K                                   |
| GPU 1             | GameViewer Virtual Display Adapter (GameViewer) - Driver: 15.6.5.199   |
| GPU 2             | Intel(R) UHD Graphics 770 (Intel Corporation) - Driver: 31.0.101.5382  |
| GPU 3             | NVIDIA GeForce RTX 4070 (NVIDIA) - Driver: 32.0.15.7680                |
| Memory            | 128GB                                                                  |
└────────────────────────────────────────────────────────────────────────────────────────────┘

# Build Environment 

┌──────────────────────────────────────────────────────────────────────┐
| Wails CLI      | v3.0.0-alpha.78                                     |
| Go Version     | go1.26.2                                            |
| -buildmode     | exe                                                 |
| -compiler      | gc                                                  |
| CGO_CFLAGS     |                                                     |
| CGO_CPPFLAGS   |                                                     |
| CGO_CXXFLAGS   |                                                     |
| CGO_ENABLED    | 1                                                   |
| CGO_LDFLAGS    |                                                     |
| DefaultGODEBUG | cryptocustomrand=1,tlssecpmlkem=0,urlstrictcolons=0 |
| GOAMD64        | v1                                                  |
| GOARCH         | amd64                                               |
| GOOS           | windows                                             |
└──────────────────────────────────────────────────────────────────────┘

# Dependencies 

┌────────────────────────────────────────────────────────────────────────────────┐
| npm                        | 11.12.1                                           |
| NSIS                       | Not Installed                                     |
| MakeAppx.exe (Windows SDK) | Not Installed                                     |
| MSIX Packaging Tool        | 1.2024.405.0                                      |
| SignTool.exe (Windows SDK) | Not Installed                                     |
| docker                     | *Not installed (optional - for cross-compilation) |
|                                                                                |
└─────────────────────────── * - Optional Dependency ────────────────────────────┘

# Checking for issues 

 SUCCESS  No issues found

# Diagnosis 

 SUCCESS  Your system is ready for Wails development!

Need documentation? Run: wails3 docs
 ♥   If Wails is useful to you or your company, please consider sponsoring the project: wails3 sponsor

Project tested: post-pigeon (Vite 8.0.10 + SolidJS 1.9.12 + lucide-solid 1.8.0)

Checklist:

  • I have updated v3/UNRELEASED_CHANGELOG.md with details of this PR
  • 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
    • Asset server now automatically retries upstream requests in the event of transient connection failures, improving overall reliability and reducing request failure rates.
    • Optimized connection behavior for local network addresses with IPv4 preference.

…nnection failures

Fixes wailsapp#4556

When using Vite with many dynamic imports, high concurrency can cause
the Vite dev server to temporarily reject connections. This change adds
a retryTransport that retries failed connections up to 3 times with
a 50ms delay between retries.

Key changes:
- Added retryTransport struct implementing http.RoundTripper
- Added isConnectionError helper to detect transient connection errors
- Configured custom dialer with timeout settings
- Force IPv4 for localhost connections to avoid IPv6 issues on Windows

This fixes the 502 Bad Gateway errors that users were experiencing
during development with Vite projects that have many dynamic imports.
@coderabbitai

coderabbitai Bot commented Apr 28, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

A reverse proxy for development asset serving is enhanced with a custom HTTP transport that retries transient connection failures. The implementation adds retry logic with configurable delay and forces IPv4 protocol selection through a custom DialContext to resolve connection issues with upstream Vite development servers.

Changes

Cohort / File(s) Summary
Retry Transport & Custom Dialer
v3/internal/assetserver/build_dev.go
Added retryTransport struct implementing retry logic for failed upstream requests. Enhanced proxy configuration with custom net.Dialer settings (5s timeout, 30s keep-alive) and DialContext override forcing IPv4 (tcp4) protocol to mitigate transient connection failures in development environments.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Poem

🐰 Hops through the network maze with care,
Retrying when connections aren't quite there,
IPv4 chosen, three attempts to try,
No more 502s filling our sky!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.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 identifies the specific issue (502 Bad Gateway errors with Vite dev server on Windows) and the main change being implemented.
Description check ✅ Passed The PR description is comprehensive, including problem statement, solution details, testing results, and configuration. However, the changelog update checklist item is unchecked.
Linked Issues check ✅ Passed The PR successfully addresses issue #4556 by implementing retry logic for transient connection failures, IPv4 forcing for localhost, and custom dialer settings as requested in the issue.
Out of Scope Changes check ✅ Passed All changes are directly related to fixing the 502 errors by enhancing the reverse proxy with retry logic and connection handling, with no unrelated modifications detected.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

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.

@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

🧹 Nitpick comments (2)
v3/internal/assetserver/build_dev.go (2)

46-56: String-based error detection is fragile but pragmatic for dev tooling.

This approach works but could break if Go changes error message wording. A more robust alternative would use type assertions:

♻️ Optional: Type-based error detection
import (
	"errors"
	"net"
	"syscall"
)

func isConnectionError(err error) bool {
	if err == nil {
		return false
	}
	var netErr *net.OpError
	if errors.As(err, &netErr) {
		var syscallErr syscall.Errno
		if errors.As(netErr.Err, &syscallErr) {
			switch syscallErr {
			case syscall.ECONNREFUSED, syscall.ECONNRESET, syscall.EPIPE:
				return true
			}
		}
	}
	// Fallback to string matching for platform-specific errors like Windows connectex
	errStr := strings.ToLower(err.Error())
	return strings.Contains(errStr, "connectex")
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@v3/internal/assetserver/build_dev.go` around lines 46 - 56, Replace fragile
string-only checks in isConnectionError with type-based detection: use errors.As
to check for *net.OpError and then extract a syscall.Errno to match
syscall.ECONNREFUSED, syscall.ECONNRESET, and syscall.EPIPE, and keep a fallback
string check only for platform-specific messages like "connectex"; update
isConnectionError to import and use errors, net, and syscall accordingly while
preserving the existing nil guard and fallback behavior.

74-92: Consider documenting the retry parameters and enhancing Transport configuration.

The retry parameters (50 retries, 50ms delay = 2.5s max) are effective but hardcoded. A brief comment explaining the rationale would help future maintainers.

The custom http.Transport only configures DialContext, leaving other settings at zero-values (vs. http.DefaultTransport which has tuned pooling/timeout settings). For a dev-only asset server this is acceptable, but consider copying relevant settings from http.DefaultTransport if connection pooling issues arise.

📝 Suggested documentation
 	proxy.Transport = &retryTransport{
 		base: &http.Transport{
+			// Inherit sensible defaults for connection pooling
+			MaxIdleConns:        100,
+			IdleConnTimeout:     90 * time.Second,
 			DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
 				// Force IPv4 for localhost connections to avoid IPv6 issues on Windows
 				if parsedURL.Hostname() == "localhost" || parsedURL.Hostname() == "127.0.0.1" {
 					return dialer.DialContext(ctx, "tcp4", addr)
 				}
 				return dialer.DialContext(ctx, network, addr)
 			},
 		},
-		maxRetries: 50,
-		delay:      50 * time.Millisecond,
+		maxRetries: 50,               // High retry count to handle Vite's concurrent connection bursts
+		delay:      50 * time.Millisecond, // Total max retry window: ~2.5s
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@v3/internal/assetserver/build_dev.go` around lines 74 - 92, Add a short
inline comment above the retryTransport instantiation explaining the choice of
maxRetries=50 and delay=50ms (total ~2.5s) so maintainers know the rationale and
can tune for dev use, and make those values easy to change (e.g., named
constants or configuration) instead of magic numbers; also improve the custom
http.Transport used as retryTransport.base by copying relevant production-tuned
fields from http.DefaultTransport (or cloning http.DefaultTransport and only
overriding DialContext) so connection pooling, IdleConnTimeout,
TLSHandshakeTimeout, MaxIdleConnsPerHost, etc., are preserved while still
forcing "tcp4" for localhost via DialContext; reference retryTransport,
proxy.Transport, DialContext, parsedURL and dialer when locating where to apply
these changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@v3/internal/assetserver/build_dev.go`:
- Around line 28-44: The RoundTrip retry loop in retryTransport.RoundTrip
consumes req.Body on the first t.base.RoundTrip(req) call which makes subsequent
retries send an empty body; buffer the request body (if non-nil) before the
first attempt (read into memory or a temporary buffer) and, before each retry,
replace req.Body with a new io.ReadCloser that replays the buffered bytes and
reset ContentLength as needed; also check req.Context().Err() before
sleeping/continuing to avoid retrying when the client has disconnected, and
ensure any response bodies from failed attempts are closed to avoid leaks (use
the existing isConnectionError check to decide retries).

---

Nitpick comments:
In `@v3/internal/assetserver/build_dev.go`:
- Around line 46-56: Replace fragile string-only checks in isConnectionError
with type-based detection: use errors.As to check for *net.OpError and then
extract a syscall.Errno to match syscall.ECONNREFUSED, syscall.ECONNRESET, and
syscall.EPIPE, and keep a fallback string check only for platform-specific
messages like "connectex"; update isConnectionError to import and use errors,
net, and syscall accordingly while preserving the existing nil guard and
fallback behavior.
- Around line 74-92: Add a short inline comment above the retryTransport
instantiation explaining the choice of maxRetries=50 and delay=50ms (total
~2.5s) so maintainers know the rationale and can tune for dev use, and make
those values easy to change (e.g., named constants or configuration) instead of
magic numbers; also improve the custom http.Transport used as
retryTransport.base by copying relevant production-tuned fields from
http.DefaultTransport (or cloning http.DefaultTransport and only overriding
DialContext) so connection pooling, IdleConnTimeout, TLSHandshakeTimeout,
MaxIdleConnsPerHost, etc., are preserved while still forcing "tcp4" for
localhost via DialContext; reference retryTransport, proxy.Transport,
DialContext, parsedURL and dialer when locating where to apply these changes.
🪄 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: b000ee5d-bd81-49cd-8531-cc7818478a6b

📥 Commits

Reviewing files that changed from the base of the PR and between b1fd8e9 and 59e2ab8.

📒 Files selected for processing (1)
  • v3/internal/assetserver/build_dev.go

Comment on lines +28 to +44
func (t *retryTransport) RoundTrip(req *http.Request) (*http.Response, error) {
var resp *http.Response
var err error
for i := 0; i < t.maxRetries; i++ {
resp, err = t.base.RoundTrip(req)
if err == nil {
return resp, nil
}
// Only retry on connection errors (e.g., connection refused)
if isConnectionError(err) && i < t.maxRetries-1 {
time.Sleep(t.delay)
continue
}
break
}
return resp, err
}

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.

⚠️ Potential issue | 🟠 Major

Request body is consumed on first attempt, breaking retries for POST/PUT requests.

When t.base.RoundTrip(req) is called, it consumes req.Body. Subsequent retry attempts will send an empty body, causing silent failures for non-GET requests (e.g., HMR websocket upgrades, API proxying).

For a dev asset server, GET requests dominate so this may work in practice, but POST/PUT requests through the proxy will fail on retry.

Additionally, consider checking req.Context().Err() before each retry to avoid unnecessary retries if the client has disconnected.

🛠️ Suggested fix with body preservation and context check
+import (
+	"bytes"
+	"io"
+	// ... existing imports
+)

 func (t *retryTransport) RoundTrip(req *http.Request) (*http.Response, error) {
+	// Buffer the body for potential retries
+	var bodyBytes []byte
+	if req.Body != nil {
+		var err error
+		bodyBytes, err = io.ReadAll(req.Body)
+		req.Body.Close()
+		if err != nil {
+			return nil, err
+		}
+	}
+
 	var resp *http.Response
 	var err error
 	for i := 0; i < t.maxRetries; i++ {
+		// Check if context is cancelled
+		if req.Context().Err() != nil {
+			return nil, req.Context().Err()
+		}
+		// Reset body for each attempt
+		if bodyBytes != nil {
+			req.Body = io.NopCloser(bytes.NewReader(bodyBytes))
+		}
 		resp, err = t.base.RoundTrip(req)
 		if err == nil {
 			return resp, nil
 		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (t *retryTransport) RoundTrip(req *http.Request) (*http.Response, error) {
var resp *http.Response
var err error
for i := 0; i < t.maxRetries; i++ {
resp, err = t.base.RoundTrip(req)
if err == nil {
return resp, nil
}
// Only retry on connection errors (e.g., connection refused)
if isConnectionError(err) && i < t.maxRetries-1 {
time.Sleep(t.delay)
continue
}
break
}
return resp, err
}
func (t *retryTransport) RoundTrip(req *http.Request) (*http.Response, error) {
// Buffer the body for potential retries
var bodyBytes []byte
if req.Body != nil {
var err error
bodyBytes, err = io.ReadAll(req.Body)
req.Body.Close()
if err != nil {
return nil, err
}
}
var resp *http.Response
var err error
for i := 0; i < t.maxRetries; i++ {
// Check if context is cancelled
if req.Context().Err() != nil {
return nil, req.Context().Err()
}
// Reset body for each attempt
if bodyBytes != nil {
req.Body = io.NopCloser(bytes.NewReader(bodyBytes))
}
resp, err = t.base.RoundTrip(req)
if err == nil {
return resp, nil
}
// Only retry on connection errors (e.g., connection refused)
if isConnectionError(err) && i < t.maxRetries-1 {
time.Sleep(t.delay)
continue
}
break
}
return resp, err
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@v3/internal/assetserver/build_dev.go` around lines 28 - 44, The RoundTrip
retry loop in retryTransport.RoundTrip consumes req.Body on the first
t.base.RoundTrip(req) call which makes subsequent retries send an empty body;
buffer the request body (if non-nil) before the first attempt (read into memory
or a temporary buffer) and, before each retry, replace req.Body with a new
io.ReadCloser that replays the buffered bytes and reset ContentLength as needed;
also check req.Context().Err() before sleeping/continuing to avoid retrying when
the client has disconnected, and ensure any response bodies from failed attempts
are closed to avoid leaks (use the existing isConnectionError check to decide
retries).

@leaanthony leaanthony changed the base branch from v3-alpha to master April 29, 2026 13:06
@leaanthony leaanthony merged commit 3b317b1 into wailsapp:master Apr 29, 2026
3 of 4 checks passed
pull Bot pushed a commit to nagyist/wails that referenced this pull request Apr 29, 2026
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.

got 502 with vite server: [ExternalAssetHandler] Proxy error error="dial tcp [::1]:9245

2 participants