diff --git a/docs/src/content/docs/changelog.mdx b/docs/src/content/docs/changelog.mdx
index c94be6695..c1b8b5051 100644
--- a/docs/src/content/docs/changelog.mdx
+++ b/docs/src/content/docs/changelog.mdx
@@ -60,6 +60,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Mac + Mac JS events now fixed by [@leaanthony](https://github.com/leaanthony)
- Fixed event deadlock for macOS by [@leaanthony](https://github.com/leaanthony)
- Fixed a `Parameter incorrect` error in Window initialisation on Windows when HTML provided but no JS by [@leaanthony](https://github.com/leaanthony)
+- Fixed size of response prefix used for content type sniffing in asset server by [@fbbdev](https://github.com/fbbdev) in [#4049](https://github.com/wailsapp/wails/pull/4049)
+- Fixed handling of non-404 responses on root index path in asset server by [@fbbdev](https://github.com/fbbdev) in [#4049](https://github.com/wailsapp/wails/pull/4049)
### Changed
diff --git a/v3/internal/assetserver/asset_fileserver.go b/v3/internal/assetserver/asset_fileserver.go
index d32bfd8f0..354cc3742 100644
--- a/v3/internal/assetserver/asset_fileserver.go
+++ b/v3/internal/assetserver/asset_fileserver.go
@@ -7,7 +7,6 @@ import (
"errors"
"fmt"
"io"
- "io/fs"
iofs "io/fs"
"net/http"
"os"
@@ -24,7 +23,7 @@ type assetFileServer struct {
err error
}
-func newAssetFileServerFS(vfs fs.FS) http.Handler {
+func newAssetFileServerFS(vfs iofs.FS) http.Handler {
subDir, err := findPathToFile(vfs, indexHTML)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
@@ -34,7 +33,7 @@ func newAssetFileServerFS(vfs fs.FS) http.Handler {
msg += fmt.Sprintf(", please make sure the embedded directory '%s' is correct and contains your assets", rootFolder)
}
- err = fmt.Errorf(msg)
+ err = errors.New(msg)
}
} else {
vfs, err = iofs.Sub(vfs, path.Clean(subDir))
diff --git a/v3/internal/assetserver/assetserver.go b/v3/internal/assetserver/assetserver.go
index 080c80fd7..5d6f8cc9d 100644
--- a/v3/internal/assetserver/assetserver.go
+++ b/v3/internal/assetserver/assetserver.go
@@ -4,7 +4,6 @@ import (
"fmt"
"net"
"net/http"
- "net/http/httptest"
"net/url"
"strings"
"time"
@@ -13,7 +12,6 @@ import (
const (
webViewRequestHeaderWindowId = "x-wails-window-id"
webViewRequestHeaderWindowName = "x-wails-window-name"
- servicePrefix = "wails/services"
HeaderAcceptLanguage = "accept-language"
)
@@ -59,6 +57,11 @@ func NewAssetServer(options *Options) (*AssetServer, error) {
func (a *AssetServer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
start := time.Now()
wrapped := &contentTypeSniffer{rw: rw}
+ defer func() {
+ if _, err := wrapped.complete(); err != nil {
+ a.options.Logger.Error("Error writing response data.", "uri", req.RequestURI, "error", err)
+ }
+ }()
req = req.WithContext(contextWithLogger(req.Context(), a.options.Logger))
a.handler.ServeHTTP(wrapped, req)
@@ -90,32 +93,25 @@ func (a *AssetServer) serveHTTP(rw http.ResponseWriter, req *http.Request, userH
reqPath := req.URL.Path
switch reqPath {
case "", "/", "/index.html":
- recorder := httptest.NewRecorder()
- userHandler.ServeHTTP(recorder, req)
- for k, v := range recorder.Result().Header {
- header[k] = v
+ // Cache the accept-language header
+ // before passing the request down the chain.
+ acceptLanguage := req.Header.Get(HeaderAcceptLanguage)
+ if acceptLanguage == "" {
+ acceptLanguage = "en"
}
- switch recorder.Code {
- case http.StatusOK:
- a.writeBlob(rw, indexHTML, recorder.Body.Bytes())
-
- case http.StatusNotFound:
- // Read the accept-language header
- acceptLanguage := req.Header.Get(HeaderAcceptLanguage)
- if acceptLanguage == "" {
- acceptLanguage = "en"
- }
- // Set content type for default index.html
- header.Set(HeaderContentType, "text/html; charset=utf-8")
- a.writeBlob(rw, indexHTML, defaultIndexHTML(acceptLanguage))
-
- default:
- rw.WriteHeader(recorder.Code)
+ wrapped := &fallbackResponseWriter{
+ rw: rw,
+ req: req,
+ fallback: http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
+ // Set content type for default index.html
+ header.Set(HeaderContentType, "text/html; charset=utf-8")
+ a.writeBlob(rw, indexHTML, defaultIndexHTML(acceptLanguage))
+ }),
}
+ userHandler.ServeHTTP(wrapped, req)
default:
-
// Check if the path matches the keys in the services map
for route, handler := range a.services {
if strings.HasPrefix(reqPath, route) {
@@ -125,14 +121,8 @@ func (a *AssetServer) serveHTTP(rw http.ResponseWriter, req *http.Request, userH
}
}
- // Check if it can be served by the user-provided handler
- if !strings.HasPrefix(reqPath, servicePrefix) {
- userHandler.ServeHTTP(rw, req)
- return
- }
-
- rw.WriteHeader(http.StatusNotFound)
- return
+ // Forward to the user-provided handler
+ userHandler.ServeHTTP(rw, req)
}
}
@@ -146,13 +136,13 @@ func (a *AssetServer) AttachServiceHandler(prefix string, handler http.Handler)
func (a *AssetServer) writeBlob(rw http.ResponseWriter, filename string, blob []byte) {
err := ServeFile(rw, filename, blob)
if err != nil {
- a.serveError(rw, err, "Unable to write content %s", filename)
+ a.serveError(rw, err, "Error writing file content.", "filename", filename)
}
}
func (a *AssetServer) serveError(rw http.ResponseWriter, err error, msg string, args ...interface{}) {
- args = append(args, err)
- a.options.Logger.Error(msg+":", args...)
+ args = append(args, "error", err)
+ a.options.Logger.Error(msg, args...)
rw.WriteHeader(http.StatusInternalServerError)
}
@@ -163,7 +153,7 @@ func GetStartURL(userURL string) (string, error) {
// Parse the port
parsedURL, err := url.Parse(devServerURL)
if err != nil {
- return "", fmt.Errorf("Error parsing environment variable 'FRONTEND_DEVSERVER_URL`: " + err.Error() + ". Please check your `Taskfile.yml` file")
+ return "", fmt.Errorf("error parsing environment variable `FRONTEND_DEVSERVER_URL`: %w. Please check your `Taskfile.yml` file", err)
}
port := parsedURL.Port()
if port != "" {
@@ -175,7 +165,7 @@ func GetStartURL(userURL string) (string, error) {
if userURL != "" {
parsedURL, err := baseURL.Parse(userURL)
if err != nil {
- return "", fmt.Errorf("Error parsing URL: " + err.Error())
+ return "", fmt.Errorf("error parsing URL: %w", err)
}
startURL = parsedURL.String()
diff --git a/v3/internal/assetserver/assetserver_dev.go b/v3/internal/assetserver/assetserver_dev.go
index 0082ce79f..e847ac480 100644
--- a/v3/internal/assetserver/assetserver_dev.go
+++ b/v3/internal/assetserver/assetserver_dev.go
@@ -4,7 +4,6 @@ package assetserver
import (
"embed"
- _ "embed"
"io"
iofs "io/fs"
)
diff --git a/v3/internal/assetserver/assetserver_test.go b/v3/internal/assetserver/assetserver_test.go
new file mode 100644
index 000000000..755ddf09c
--- /dev/null
+++ b/v3/internal/assetserver/assetserver_test.go
@@ -0,0 +1,244 @@
+package assetserver
+
+import (
+ "fmt"
+ "log/slog"
+ "net/http"
+ "net/http/httptest"
+ "strconv"
+ "strings"
+ "testing"
+ _ "unsafe"
+
+ "github.com/google/go-cmp/cmp"
+)
+
+func TestContentSniffing(t *testing.T) {
+ longLead := strings.Repeat(" ", 512-6)
+
+ tests := map[string]struct {
+ Expect string
+ Status int
+ Header map[string][]string
+ Body []string
+ }{
+ "/simple": {
+ Expect: "text/html; charset=utf-8",
+ Body: []string{"
Hello!"},
+ },
+ "/split": {
+ Expect: "text/html; charset=utf-8",
+ Body: []string{
+ "Hello!",
+ "",
+ },
+ },
+ "/lead/short/simple": {
+ Expect: "text/html; charset=utf-8",
+ Body: []string{
+ " " + "Hello!",
+ },
+ },
+ "/lead/short/split": {
+ Expect: "text/html; charset=utf-8",
+ Body: []string{
+ " ",
+ "Hello!",
+ },
+ },
+ "/lead/long/simple": {
+ Expect: "text/html; charset=utf-8",
+ Body: []string{
+ longLead + "Hello!",
+ },
+ },
+ "/lead/long/split": {
+ Expect: "text/html; charset=utf-8",
+ Body: []string{
+ longLead,
+ "Hello!",
+ },
+ },
+ "/lead/toolong/simple": {
+ Expect: "text/plain; charset=utf-8",
+ Body: []string{
+ "Hello" + longLead + "Hello!",
+ },
+ },
+ "/lead/toolong/split": {
+ Expect: "text/plain; charset=utf-8",
+ Body: []string{
+ "Hello" + longLead,
+ "Hello!",
+ },
+ },
+ "/header": {
+ Expect: "text/html; charset=utf-8",
+ Status: http.StatusForbidden,
+ Header: map[string][]string{
+ "X-Custom": {"CustomValue"},
+ },
+ Body: []string{"Hello!"},
+ },
+ "/custom": {
+ Expect: "text/plain;charset=utf-8",
+ Header: map[string][]string{
+ "Content-Type": {"text/plain;charset=utf-8"},
+ },
+ Body: []string{"Hello!"},
+ },
+ }
+
+ srv, err := NewAssetServer(&Options{
+ Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ test, ok := tests[r.URL.Path]
+ if !ok {
+ w.WriteHeader(http.StatusNotFound)
+ return
+ }
+
+ for key, values := range test.Header {
+ for _, value := range values {
+ w.Header().Add(key, value)
+ }
+ }
+
+ if test.Status != 0 {
+ w.WriteHeader(test.Status)
+ }
+
+ for _, chunk := range test.Body {
+ w.Write([]byte(chunk))
+ }
+ }),
+ Logger: slog.Default(),
+ })
+ if err != nil {
+ t.Fatal("AssetServer initialisation failed: ", err)
+ }
+
+ for path, test := range tests {
+ t.Run(path[1:], func(t *testing.T) {
+ res := httptest.NewRecorder()
+
+ req, err := http.NewRequest(http.MethodGet, path, nil)
+ if err != nil {
+ t.Fatal("http.NewRequest failed: ", err)
+ }
+
+ srv.ServeHTTP(res, req)
+
+ expectedStatus := http.StatusOK
+ if test.Status != 0 {
+ expectedStatus = test.Status
+ }
+ if res.Code != expectedStatus {
+ t.Errorf("Status code mismatch: want %d, got %d", expectedStatus, res.Code)
+ }
+
+ if ct := res.Header().Get("Content-Type"); ct != test.Expect {
+ t.Errorf("Content type mismatch: want '%s', got '%s'", test.Expect, ct)
+ }
+
+ for key, values := range test.Header {
+ if diff := cmp.Diff(values, res.Header().Values(key)); diff != "" {
+ t.Errorf("Header '%s' mismatch (-want +got):\n%s", key, diff)
+ }
+ }
+
+ if diff := cmp.Diff(strings.Join(test.Body, ""), res.Body.String()); diff != "" {
+ t.Errorf("Response body mismatch (-want +got):\n%s", diff)
+ }
+ })
+ }
+}
+
+func TestIndexFallback(t *testing.T) {
+ // Paths to try and whether a 404 should trigger a fallback.
+ paths := map[string]bool{
+ "": true,
+ "/": true,
+ "/index": false,
+ "/index.html": true,
+ "/other": false,
+ }
+
+ statuses := []int{
+ http.StatusOK,
+ http.StatusNotFound,
+ http.StatusForbidden,
+ }
+
+ header := map[string][]string{
+ "X-Custom": {"CustomValue"},
+ }
+ body := "Hello!"
+
+ srv, err := NewAssetServer(&Options{
+ Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ for key, values := range header {
+ for _, value := range values {
+ w.Header().Add(key, value)
+ }
+ }
+
+ status, err := strconv.Atoi(r.URL.Query().Get("status"))
+ if err == nil && status != 0 && status != http.StatusOK {
+ w.WriteHeader(status)
+ }
+
+ w.Write([]byte(body))
+ }),
+ Logger: slog.Default(),
+ })
+ if err != nil {
+ t.Fatal("AssetServer initialisation failed: ", err)
+ }
+
+ for path, fallback := range paths {
+ for _, status := range statuses {
+ key := ""
+ if len(path) > 0 {
+ key = path[1:]
+ }
+
+ t.Run(fmt.Sprintf("%s/status=%d", key, status), func(t *testing.T) {
+ res := httptest.NewRecorder()
+
+ req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s?status=%d", path, status), nil)
+ if err != nil {
+ t.Fatal("http.NewRequest failed: ", err)
+ }
+
+ srv.ServeHTTP(res, req)
+
+ fallbackTriggered := false
+ if status == http.StatusNotFound && fallback {
+ status = http.StatusOK
+ fallbackTriggered = true
+ }
+
+ if res.Code != status {
+ t.Errorf("Status code mismatch: want %d, got %d", status, res.Code)
+ }
+
+ if fallbackTriggered {
+ if cmp.Equal(body, res.Body.String()) {
+ t.Errorf("Fallback response has the same body as not found response")
+ }
+ return
+ } else {
+ for key, values := range header {
+ if diff := cmp.Diff(values, res.Header().Values(key)); diff != "" {
+ t.Errorf("Header '%s' mismatch (-want +got):\n%s", key, diff)
+ }
+ }
+
+ if diff := cmp.Diff(body, res.Body.String()); diff != "" {
+ t.Errorf("Response body mismatch (-want +got):\n%s", diff)
+ }
+ }
+ })
+ }
+ }
+}
diff --git a/v3/internal/assetserver/assetserver_webview.go b/v3/internal/assetserver/assetserver_webview.go
index 377458533..b569a4743 100644
--- a/v3/internal/assetserver/assetserver_webview.go
+++ b/v3/internal/assetserver/assetserver_webview.go
@@ -70,17 +70,21 @@ func (a *AssetServer) processWebViewRequestInternal(r webview.Request) {
wrw := r.Response()
defer func() {
if err := wrw.Finish(); err != nil {
- a.options.Logger.Error("Error finishing request '%s': %s", uri, err)
+ a.options.Logger.Error("Error finishing request.", "uri", uri, "error", err)
}
}()
- var rw http.ResponseWriter = &contentTypeSniffer{rw: wrw} // Make sure we have a Content-Type sniffer
- defer rw.WriteHeader(http.StatusNotImplemented) // This is a NOP when a handler has already written and set the status
+ rw := &contentTypeSniffer{rw: wrw} // Make sure we have a Content-Type sniffer
+ defer func() {
+ if _, err := rw.complete(); err != nil {
+ a.options.Logger.Error("Error writing response data.", "uri", uri, "error", err)
+ }
+ }()
+ defer rw.WriteHeader(http.StatusNotImplemented) // This is a NOP when a handler has already written and set the status
uri, err = r.URL()
if err != nil {
- a.options.Logger.Error(fmt.Sprintf("Error processing request, unable to get URL: %s (HttpResponse=500)", err))
- http.Error(rw, err.Error(), http.StatusInternalServerError)
+ a.webviewRequestErrorHandler(uri, rw, fmt.Errorf("URL: %w", err))
return
}
@@ -162,7 +166,7 @@ func (a *AssetServer) webviewRequestErrorHandler(uri string, rw http.ResponseWri
logInfo = strings.Replace(logInfo, fmt.Sprintf("%s://%s", uri.Scheme, uri.Host), "", 1)
}
- a.options.Logger.Error("Error processing request (HttpResponse=500)", "details", logInfo, "error", err.Error())
+ a.options.Logger.Error("Error processing request (HttpResponse=500)", "details", logInfo, "error", err)
http.Error(rw, err.Error(), http.StatusInternalServerError)
}
diff --git a/v3/internal/assetserver/common.go b/v3/internal/assetserver/common.go
index 2fe219f24..ce6201b6a 100644
--- a/v3/internal/assetserver/common.go
+++ b/v3/internal/assetserver/common.go
@@ -22,9 +22,9 @@ const (
WailsUserAgentValue = "wails.io"
)
-var (
- assetServerLogger = struct{}{}
-)
+type assetServerLogger struct{}
+
+var assetServerLoggerKey assetServerLogger
func ServeFile(rw http.ResponseWriter, filename string, blob []byte) error {
header := rw.Header()
@@ -45,17 +45,17 @@ func isWebSocket(req *http.Request) bool {
}
func contextWithLogger(ctx context.Context, logger *slog.Logger) context.Context {
- return context.WithValue(ctx, assetServerLogger, logger)
+ return context.WithValue(ctx, assetServerLoggerKey, logger)
}
func logInfo(ctx context.Context, message string, args ...interface{}) {
- if logger, _ := ctx.Value(assetServerLogger).(*slog.Logger); logger != nil {
+ if logger, _ := ctx.Value(assetServerLoggerKey).(*slog.Logger); logger != nil {
logger.Info(message, args...)
}
}
func logError(ctx context.Context, message string, args ...interface{}) {
- if logger, _ := ctx.Value(assetServerLogger).(*slog.Logger); logger != nil {
+ if logger, _ := ctx.Value(assetServerLoggerKey).(*slog.Logger); logger != nil {
logger.Error(message, args...)
}
}
diff --git a/v3/internal/assetserver/content_type_sniffer.go b/v3/internal/assetserver/content_type_sniffer.go
index 85e2191dc..44e4ceb39 100644
--- a/v3/internal/assetserver/content_type_sniffer.go
+++ b/v3/internal/assetserver/content_type_sniffer.go
@@ -5,38 +5,106 @@ import (
)
type contentTypeSniffer struct {
- rw http.ResponseWriter
- status int
- wroteHeader bool
+ rw http.ResponseWriter
+ prefix []byte
+ status int
+ headerCommitted bool
+ headerWritten bool
}
-func (rw contentTypeSniffer) Header() http.Header {
+// Unwrap returns the wrapped [http.ResponseWriter] for use with [http.ResponseController].
+func (rw *contentTypeSniffer) Unwrap() http.ResponseWriter {
+ return rw.rw
+}
+
+func (rw *contentTypeSniffer) Header() http.Header {
return rw.rw.Header()
}
-func (rw *contentTypeSniffer) Write(buf []byte) (int, error) {
- rw.writeHeader(buf)
- return rw.rw.Write(buf)
+func (rw *contentTypeSniffer) Write(chunk []byte) (int, error) {
+ if !rw.headerCommitted {
+ rw.WriteHeader(http.StatusOK)
+ }
+
+ if rw.headerWritten {
+ return rw.rw.Write(chunk)
+ }
+
+ if len(chunk) == 0 {
+ return 0, nil
+ }
+
+ // Cut away at most 512 bytes from chunk, and not less than 0.
+ cut := max(min(len(chunk), 512-len(rw.prefix)), 0)
+ if cut >= 512 {
+ // Avoid copying data if a full prefix is available on first non-zero write.
+ cut = len(chunk)
+ rw.prefix = chunk
+ chunk = nil
+ } else if cut > 0 {
+ // First write had less than 512 bytes -- copy data to the prefix buffer.
+ if rw.prefix == nil {
+ // Preallocate space for the prefix to be used for sniffing.
+ rw.prefix = make([]byte, 0, 512)
+ }
+ rw.prefix = append(rw.prefix, chunk[:cut]...)
+ chunk = chunk[cut:]
+ }
+
+ if len(rw.prefix) < 512 {
+ return cut, nil
+ }
+
+ if _, err := rw.complete(); err != nil {
+ return cut, err
+ }
+
+ n, err := rw.rw.Write(chunk)
+ return cut + n, err
}
func (rw *contentTypeSniffer) WriteHeader(code int) {
- if rw.wroteHeader {
+ if rw.headerCommitted {
return
}
+
rw.status = code
- rw.rw.WriteHeader(code)
- rw.wroteHeader = true
+ rw.headerCommitted = true
+
+ if _, hasType := rw.Header()[HeaderContentType]; hasType {
+ rw.rw.WriteHeader(rw.status)
+ rw.headerWritten = true
+ }
}
-func (rw *contentTypeSniffer) writeHeader(b []byte) {
- if rw.wroteHeader {
+// sniff sniffs the content type from the stored prefix if necessary,
+// then writes the header.
+func (rw *contentTypeSniffer) sniff() {
+ if rw.headerWritten || !rw.headerCommitted {
return
}
- m := rw.rw.Header()
+ m := rw.Header()
if _, hasType := m[HeaderContentType]; !hasType {
- m.Set(HeaderContentType, http.DetectContentType(b))
+ m.Set(HeaderContentType, http.DetectContentType(rw.prefix))
}
- rw.WriteHeader(http.StatusOK)
+ rw.rw.WriteHeader(rw.status)
+ rw.headerWritten = true
+}
+
+// complete sniffs the content type if necessary, writes the header
+// and sends the data prefix that has been stored for sniffing.
+//
+// Whoever creates a contentTypeSniffer instance
+// is responsible for calling complete after the nested handler has returned.
+func (rw *contentTypeSniffer) complete() (n int, err error) {
+ rw.sniff()
+
+ if rw.headerWritten && len(rw.prefix) > 0 {
+ n, err = rw.rw.Write(rw.prefix)
+ rw.prefix = nil
+ }
+
+ return
}
diff --git a/v3/internal/assetserver/fallback_response_writer.go b/v3/internal/assetserver/fallback_response_writer.go
new file mode 100644
index 000000000..cd5a5d9e3
--- /dev/null
+++ b/v3/internal/assetserver/fallback_response_writer.go
@@ -0,0 +1,73 @@
+package assetserver
+
+import (
+ "maps"
+ "net/http"
+)
+
+// fallbackResponseWriter wraps a [http.ResponseWriter].
+// If the main handler returns status code 404,
+// its response is discarded
+// and the request is forwarded to the fallback handler.
+type fallbackResponseWriter struct {
+ rw http.ResponseWriter
+ req *http.Request
+ fallback http.Handler
+
+ header http.Header
+ headerWritten bool
+ complete bool
+}
+
+// Unwrap returns the wrapped [http.ResponseWriter] for use with [http.ResponseController].
+func (fw *fallbackResponseWriter) Unwrap() http.ResponseWriter {
+ return fw.rw
+}
+
+func (fw *fallbackResponseWriter) Header() http.Header {
+ if fw.header == nil {
+ // Preserve original header in case we get a 404 response.
+ fw.header = fw.rw.Header().Clone()
+ }
+ return fw.header
+}
+
+func (fw *fallbackResponseWriter) Write(chunk []byte) (int, error) {
+ if fw.complete {
+ // Fallback triggered, discard further writes.
+ return len(chunk), nil
+ }
+
+ if !fw.headerWritten {
+ fw.WriteHeader(http.StatusOK)
+ }
+
+ return fw.rw.Write(chunk)
+}
+
+func (fw *fallbackResponseWriter) WriteHeader(statusCode int) {
+ if fw.headerWritten {
+ return
+ }
+ fw.headerWritten = true
+
+ if statusCode == http.StatusNotFound {
+ // Protect fallback header from external modifications.
+ if fw.header == nil {
+ fw.header = fw.rw.Header().Clone()
+ }
+
+ // Invoke fallback handler.
+ fw.complete = true
+ fw.fallback.ServeHTTP(fw.rw, fw.req)
+ return
+ }
+
+ if fw.header != nil {
+ // Apply headers and forward original map to the main handler.
+ maps.Copy(fw.rw.Header(), fw.header)
+ fw.header = fw.rw.Header()
+ }
+
+ fw.rw.WriteHeader(statusCode)
+}
diff --git a/v3/internal/assetserver/fs.go b/v3/internal/assetserver/fs.go
index 4d8aad579..20ccf3fab 100644
--- a/v3/internal/assetserver/fs.go
+++ b/v3/internal/assetserver/fs.go
@@ -12,7 +12,7 @@ import (
// findEmbedRootPath finds the root path in the embed FS. It's the directory which contains all the files.
func findEmbedRootPath(fileSystem embed.FS) (string, error) {
- stopErr := fmt.Errorf("files or multiple dirs found")
+ stopErr := errors.New("files or multiple dirs found")
fPath := ""
err := fs.WalkDir(fileSystem, ".", func(path string, d fs.DirEntry, err error) error {
diff --git a/v3/internal/assetserver/options.go b/v3/internal/assetserver/options.go
index 987eae7b5..ae0570880 100644
--- a/v3/internal/assetserver/options.go
+++ b/v3/internal/assetserver/options.go
@@ -1,7 +1,7 @@
package assetserver
import (
- "fmt"
+ "errors"
"log/slog"
"net/http"
)
@@ -31,7 +31,7 @@ type Options struct {
// Validate the options
func (o Options) Validate() error {
if o.Handler == nil && o.Middleware == nil {
- return fmt.Errorf("AssetServer options invalid: either Handler or Middleware must be set")
+ return errors.New("AssetServer options invalid: either Handler or Middleware must be set")
}
return nil
diff --git a/v3/internal/assetserver/webview/request_darwin.go b/v3/internal/assetserver/webview/request_darwin.go
index f0e85780b..2d8b00168 100644
--- a/v3/internal/assetserver/webview/request_darwin.go
+++ b/v3/internal/assetserver/webview/request_darwin.go
@@ -110,6 +110,7 @@ import "C"
import (
"bytes"
"encoding/json"
+ "errors"
"fmt"
"io"
"net/http"
@@ -227,9 +228,9 @@ func (r *requestBodyStreamReader) Read(p []byte) (n int, err error) {
case 0:
return 0, io.EOF
case -1:
- return 0, fmt.Errorf("body: stream error")
+ return 0, errors.New("body: stream error")
case -2:
- return 0, fmt.Errorf("body: no stream defined")
+ return 0, errors.New("body: no stream defined")
case -3:
return 0, io.ErrClosedPipe
default:
diff --git a/v3/internal/assetserver/webview/request_windows.go b/v3/internal/assetserver/webview/request_windows.go
index 77c51727a..9f68af2e1 100644
--- a/v3/internal/assetserver/webview/request_windows.go
+++ b/v3/internal/assetserver/webview/request_windows.go
@@ -3,10 +3,10 @@
package webview
import (
+ "errors"
"fmt"
"io"
"net/http"
- "strings"
"github.com/wailsapp/go-webview2/pkg/edge"
)
@@ -202,15 +202,17 @@ func getHeaders(req *edge.ICoreWebView2WebResourceRequest) (http.Header, error)
}
func combineErrs(errs []error) error {
- // TODO use Go1.20 errors.Join
- if len(errs) == 0 {
- return nil
+ err := errors.Join(errs...)
+
+ if err != nil {
+ // errors.Join wraps even a single error.
+ // Check the filtered error list,
+ // and if it has just one element return it directly.
+ errs = err.(interface{ Unwrap() []error }).Unwrap()
+ if len(errs) == 1 {
+ return errs[0]
+ }
}
- errStrings := make([]string, len(errs))
- for i, err := range errs {
- errStrings[i] = err.Error()
- }
-
- return fmt.Errorf(strings.Join(errStrings, "\n"))
+ return err
}
diff --git a/v3/internal/assetserver/webview/responsewriter_windows.go b/v3/internal/assetserver/webview/responsewriter_windows.go
index 09bec236f..c003f00bd 100644
--- a/v3/internal/assetserver/webview/responsewriter_windows.go
+++ b/v3/internal/assetserver/webview/responsewriter_windows.go
@@ -4,6 +4,7 @@ package webview
import (
"bytes"
+ "errors"
"fmt"
"net/http"
"strings"
@@ -68,7 +69,7 @@ func (rw *responseWriter) Finish() error {
if code == http.StatusNotModified {
// WebView2 has problems when a request returns a 304 status code and the WebView2 is going to hang for other
// requests including IPC calls.
- errs = append(errs, fmt.Errorf("AssetServer returned 304 - StatusNotModified which are going to hang WebView2, changed code to 505 - StatusInternalServerError"))
+ errs = append(errs, errors.New("AssetServer returned 304 - StatusNotModified which are going to hang WebView2, changed code to 505 - StatusInternalServerError"))
code = http.StatusInternalServerError
}