fix: add involves edge from task to agent:nomos at creation
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
ci / web (push) Has been cancelled
Desktop App / Build Linux (amd64) (push) Has been cancelled
Desktop App / Attach to Release (push) Has been cancelled

Plus sync vendor directory for Docker build compatibility.
This commit is contained in:
2026-08-11 22:03:12 +02:00
parent 7d6a3320d4
commit febc153b7f
3384 changed files with 945212 additions and 2 deletions

View File

@@ -0,0 +1,161 @@
package assetserver
import (
"bytes"
"context"
"embed"
"errors"
"fmt"
"io"
iofs "io/fs"
"net/http"
"os"
"path"
"strings"
)
const (
indexHTML = "index.html"
)
type assetFileServer struct {
fs iofs.FS
err error
}
func newAssetFileServerFS(vfs iofs.FS) http.Handler {
subDir, err := findPathToFile(vfs, indexHTML)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
msg := "no `index.html` could be found in your Assets fs.FS"
if embedFs, isEmbedFs := vfs.(embed.FS); isEmbedFs {
rootFolder, _ := findEmbedRootPath(embedFs)
msg += fmt.Sprintf(", please make sure the embedded directory '%s' is correct and contains your assets", rootFolder)
}
err = errors.New(msg)
}
} else {
vfs, err = iofs.Sub(vfs, path.Clean(subDir))
}
return &assetFileServer{fs: vfs, err: err}
}
func (d *assetFileServer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
ctx := req.Context()
url := req.URL.Path
err := d.err
if err == nil {
filename := path.Clean(strings.TrimPrefix(url, "/"))
d.logInfo(ctx, "Handling request", "url", url, "file", filename)
err = d.serveFSFile(rw, req, filename)
if os.IsNotExist(err) {
rw.WriteHeader(http.StatusNotFound)
return
}
}
if err != nil {
d.logError(ctx, "Unable to handle request", "url", url, "err", err)
http.Error(rw, err.Error(), http.StatusInternalServerError)
}
}
// serveFile will try to load the file from the fs.FS and write it to the response
func (d *assetFileServer) serveFSFile(rw http.ResponseWriter, req *http.Request, filename string) error {
if d.fs == nil {
return os.ErrNotExist
}
file, err := d.fs.Open(filename)
if err != nil {
if s := path.Ext(filename); s == "" {
filename = filename + ".html"
file, err = d.fs.Open(filename)
if err != nil {
return err
}
} else {
return err
}
}
defer file.Close()
statInfo, err := file.Stat()
if err != nil {
return err
}
url := req.URL.Path
isDirectoryPath := url == "" || url[len(url)-1] == '/'
if statInfo.IsDir() {
if !isDirectoryPath {
// If the URL doesn't end in a slash normally a http.redirect should be done, but that currently doesn't work on
// WebKit WebViews (macOS/Linux).
// So we handle this as a specific error
return fmt.Errorf("a directory has been requested without a trailing slash, please add a trailing slash to your request")
}
filename = path.Join(filename, indexHTML)
file, err = d.fs.Open(filename)
if err != nil {
return err
}
defer file.Close()
statInfo, err = file.Stat()
if err != nil {
return err
}
} else if isDirectoryPath {
return fmt.Errorf("a file has been requested with a trailing slash, please remove the trailing slash from your request")
}
var buf [512]byte
var n int
if _, haveType := rw.Header()[HeaderContentType]; !haveType {
// Detect MimeType by sniffing the first 512 bytes
n, err = file.Read(buf[:])
if err != nil && err != io.EOF {
return err
}
// Do the custom MimeType sniffing even though http.ServeContent would do it in case
// of an io.ReadSeeker. We would like to have a consistent behaviour in both cases.
if contentType := GetMimetype(filename, buf[:n]); contentType != "" {
rw.Header().Set(HeaderContentType, contentType)
}
}
if fileSeeker, _ := file.(io.ReadSeeker); fileSeeker != nil {
if _, err := fileSeeker.Seek(0, io.SeekStart); err != nil {
return fmt.Errorf("seeker can't seek")
}
http.ServeContent(rw, req, statInfo.Name(), statInfo.ModTime(), fileSeeker)
return nil
}
rw.Header().Set(HeaderContentLength, fmt.Sprintf("%d", statInfo.Size()))
// Write the first 512 bytes used for MimeType sniffing
_, err = io.Copy(rw, bytes.NewReader(buf[:n]))
if err != nil {
return err
}
// Copy the remaining content of the file
_, err = io.Copy(rw, file)
return err
}
func (d *assetFileServer) logInfo(ctx context.Context, message string, args ...interface{}) {
logInfo(ctx, "[AssetFileServerFS] "+message, args...)
}
func (d *assetFileServer) logError(ctx context.Context, message string, args ...interface{}) {
logError(ctx, "[AssetFileServerFS] "+message, args...)
}

View File

@@ -0,0 +1,175 @@
package assetserver
import (
"fmt"
"net"
"net/http"
"net/url"
"strings"
"time"
)
const (
webViewRequestHeaderWindowId = "x-wails-window-id"
webViewRequestHeaderWindowName = "x-wails-window-name"
HeaderAcceptLanguage = "accept-language"
)
type RuntimeHandler interface {
HandleRuntimeCall(w http.ResponseWriter, r *http.Request)
}
type service struct {
Route string
Handler http.Handler
}
type AssetServer struct {
options *Options
handler http.Handler
services []service
assetServerWebView
}
func NewAssetServer(options *Options) (*AssetServer, error) {
result := &AssetServer{
options: options,
}
userHandler := options.Handler
if userHandler == nil {
userHandler = http.NotFoundHandler()
}
handler := http.Handler(
http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
result.serveHTTP(w, r, userHandler)
}))
if middleware := options.Middleware; middleware != nil {
handler = middleware(handler)
}
result.handler = handler
return result, nil
}
func (a *AssetServer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
start := time.Now()
wrapped := newContentTypeSniffer(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)
a.options.Logger.Debug(
"Asset Request:",
"windowName", req.Header.Get(webViewRequestHeaderWindowName),
"windowID", req.Header.Get(webViewRequestHeaderWindowId),
"code", wrapped.status,
"method", req.Method,
"path", req.URL.EscapedPath(),
"duration", time.Since(start),
)
}
func (a *AssetServer) serveHTTP(rw http.ResponseWriter, req *http.Request, userHandler http.Handler) {
if isWebSocket(req) {
// WebSockets are not supported by the AssetServer
rw.WriteHeader(http.StatusNotImplemented)
return
}
header := rw.Header()
// TODO: I don't think this is needed now?
//if a.servingFromDisk {
// header.Add(HeaderCacheControl, "no-cache")
//}
reqPath := req.URL.Path
switch reqPath {
case "", "/", "/index.html":
// Cache the accept-language header
// before passing the request down the chain.
acceptLanguage := req.Header.Get(HeaderAcceptLanguage)
if acceptLanguage == "" {
acceptLanguage = "en"
}
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 a service route
for _, svc := range a.services {
if strings.HasPrefix(reqPath, svc.Route) {
req.URL.Path = strings.TrimPrefix(reqPath, svc.Route)
svc.Handler.ServeHTTP(rw, req)
return
}
}
// Forward to the user-provided handler
userHandler.ServeHTTP(rw, req)
}
}
func (a *AssetServer) AttachServiceHandler(route string, handler http.Handler) {
a.services = append(a.services, service{route, handler})
}
func (a *AssetServer) writeBlob(rw http.ResponseWriter, filename string, blob []byte) {
err := ServeFile(rw, filename, blob)
if err != nil {
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, "error", err)
a.options.Logger.Error(msg, args...)
rw.WriteHeader(http.StatusInternalServerError)
}
func GetStartURL(userURL string) (string, error) {
devServerURL := GetDevServerURL()
startURL := baseURL.String()
if devServerURL != "" {
// Parse the port
parsedURL, err := url.Parse(devServerURL)
if err != nil {
return "", fmt.Errorf("error parsing environment variable `FRONTEND_DEVSERVER_URL`: %w. Please check your `Taskfile.yml` file", err)
}
port := parsedURL.Port()
if port != "" {
baseURL.Host = net.JoinHostPort(baseURL.Hostname(), port)
startURL = baseURL.String()
}
}
if userURL != "" {
parsedURL, err := baseURL.Parse(userURL)
if err != nil {
return "", fmt.Errorf("error parsing URL: %w", err)
}
startURL = parsedURL.String()
}
return startURL, nil
}

View File

@@ -0,0 +1,12 @@
//go:build android
package assetserver
import "net/url"
// Android uses https://wails.localhost as the base URL
// This matches the WebViewAssetLoader domain configuration
var baseURL = url.URL{
Scheme: "https",
Host: "wails.localhost",
}

View File

@@ -0,0 +1,10 @@
//go:build darwin && !ios
package assetserver
import "net/url"
var baseURL = url.URL{
Scheme: "wails",
Host: "localhost",
}

View File

@@ -0,0 +1,50 @@
//go:build !production
package assetserver
import (
"embed"
"io"
iofs "io/fs"
)
//go:embed defaults
var defaultHTML embed.FS
func defaultIndexHTML(language string) []byte {
result := []byte("index.html not found")
// Create an fs.Sub in the defaults directory
defaults, err := iofs.Sub(defaultHTML, "defaults")
if err != nil {
return result
}
// Get the 2 character language code
lang := "en"
if len(language) >= 2 {
lang = language[:2]
}
// Now we can read the index.html file in the format
// index.<lang>.html.
indexFile, err := defaults.Open("index." + lang + ".html")
if err != nil {
return result
}
indexBytes, err := io.ReadAll(indexFile)
if err != nil {
return result
}
return indexBytes
}
func (a *AssetServer) LogDetails() {
var info = []any{
"middleware", a.options.Middleware != nil,
"handler", a.options.Handler != nil,
}
if devServerURL := GetDevServerURL(); devServerURL != "" {
info = append(info, "devServerURL", devServerURL)
}
a.options.Logger.Info("AssetServer Info:", info...)
}

View File

@@ -0,0 +1,10 @@
//go:build ios
package assetserver
import "net/url"
var baseURL = url.URL{
Scheme: "wails",
Host: "localhost",
}

View File

@@ -0,0 +1,10 @@
//go:build linux && !android
package assetserver
import "net/url"
var baseURL = url.URL{
Scheme: "wails",
Host: "localhost",
}

View File

@@ -0,0 +1,9 @@
//go:build production
package assetserver
func defaultIndexHTML(_ string) []byte {
return []byte("index.html not found")
}
func (a *AssetServer) LogDetails() {}

View File

@@ -0,0 +1,198 @@
package assetserver
import (
"context"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"sync"
"github.com/wailsapp/wails/v3/internal/assetserver/webview"
)
type assetServerWebView struct {
// ExpectedWebViewHost is checked against the Request Host of every WebViewRequest, other hosts won't be processed.
ExpectedWebViewHost string
dispatchInit sync.Once
dispatchReqC chan<- webview.Request
dispatchWorkers int
}
// ServeWebViewRequest processes the HTTP Request asynchronously by faking a golang HTTP Server.
// The request will be finished with a StatusNotImplemented code if no handler has written to the response.
// The AssetServer takes ownership of the request and the caller mustn't close it or access it in any other way.
func (a *AssetServer) ServeWebViewRequest(req webview.Request) {
a.dispatchInit.Do(func() {
workers := a.dispatchWorkers
if workers <= 0 {
return
}
workerC := make(chan webview.Request, workers*2)
for i := 0; i < workers; i++ {
go func() {
for req := range workerC {
a.processWebViewRequest(req)
}
}()
}
dispatchC := make(chan webview.Request)
go queueingDispatcher(50, dispatchC, workerC)
a.dispatchReqC = dispatchC
})
if a.dispatchReqC == nil {
go a.processWebViewRequest(req)
} else {
a.dispatchReqC <- req
}
}
func (a *AssetServer) processWebViewRequest(r webview.Request) {
uri, _ := r.URL()
a.processWebViewRequestInternal(r)
if err := r.Close(); err != nil {
a.options.Logger.Error("Unable to call close for request for uri.", "uri", uri)
}
}
// processHTTPRequest processes the HTTP Request by faking a golang HTTP Server.
// The request will be finished with a StatusNotImplemented code if no handler has written to the response.
func (a *AssetServer) processWebViewRequestInternal(r webview.Request) {
uri := "unknown"
var err error
wrw := r.Response()
defer func() {
if err := wrw.Finish(); err != nil {
a.options.Logger.Error("Error finishing request.", "uri", uri, "error", err)
}
}()
rw := newContentTypeSniffer(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.webviewRequestErrorHandler(uri, rw, fmt.Errorf("URL: %w", err))
return
}
method, err := r.Method()
if err != nil {
a.webviewRequestErrorHandler(uri, rw, fmt.Errorf("HTTP-Method: %w", err))
return
}
header, err := r.Header()
if err != nil {
a.webviewRequestErrorHandler(uri, rw, fmt.Errorf("HTTP-Header: %w", err))
return
}
body, err := r.Body()
if err != nil {
a.webviewRequestErrorHandler(uri, rw, fmt.Errorf("HTTP-Body: %w", err))
return
}
if body == nil {
body = http.NoBody
}
defer body.Close()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
req, err := http.NewRequestWithContext(ctx, method, uri, body)
if err != nil {
a.webviewRequestErrorHandler(uri, rw, fmt.Errorf("HTTP-Request: %w", err))
return
}
// For server requests, the URL is parsed from the URI supplied on the Request-Line as stored in RequestURI. For
// most requests, fields other than Path and RawQuery will be empty. (See RFC 7230, Section 5.3)
req.URL.Scheme = ""
req.URL.Host = ""
req.URL.Fragment = ""
req.URL.RawFragment = ""
if requestURL := req.URL; req.RequestURI == "" && requestURL != nil {
req.RequestURI = requestURL.String()
}
req.Header = header
if req.RemoteAddr == "" {
// 192.0.2.0/24 is "TEST-NET" in RFC 5737
req.RemoteAddr = "192.0.2.1:1234"
}
if req.RequestURI == "" && req.URL != nil {
req.RequestURI = req.URL.String()
}
if req.ContentLength == 0 {
req.ContentLength, _ = strconv.ParseInt(req.Header.Get(HeaderContentLength), 10, 64)
} else {
req.Header.Set(HeaderContentLength, fmt.Sprintf("%d", req.ContentLength))
}
if host := req.Header.Get(HeaderHost); host != "" {
req.Host = host
}
// iOS uses "localhost" while other platforms might use different hosts
// Skip host check for iOS requests from wails:// scheme
if expectedHost := a.ExpectedWebViewHost; expectedHost != "" && expectedHost != req.Host && !strings.HasPrefix(uri, "wails://") {
a.webviewRequestErrorHandler(uri, rw, fmt.Errorf("expected host '%s' in request, but was '%s'", expectedHost, req.Host))
return
}
a.ServeHTTP(rw, req)
}
func (a *AssetServer) webviewRequestErrorHandler(uri string, rw http.ResponseWriter, err error) {
logInfo := uri
if uri, err := url.ParseRequestURI(uri); err == nil {
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)
http.Error(rw, err.Error(), http.StatusInternalServerError)
}
func queueingDispatcher[T any](minQueueSize uint, inC <-chan T, outC chan<- T) {
q := newRingqueue[T](minQueueSize)
for {
in, ok := <-inC
if !ok {
return
}
q.Add(in)
for q.Len() != 0 {
out, _ := q.Peek()
select {
case outC <- out:
q.Remove()
case in, ok := <-inC:
if !ok {
return
}
q.Add(in)
}
}
}
}

View File

@@ -0,0 +1,8 @@
package assetserver
import "net/url"
var baseURL = url.URL{
Scheme: "http",
Host: "wails.localhost",
}

View File

@@ -0,0 +1,102 @@
//go:build !production
package assetserver
import (
"context"
"io/fs"
"net"
"net/http"
"net/http/httputil"
"net/url"
"os"
"strings"
"time"
)
// retryTransport implements http.RoundTripper with retry logic for transient connection failures.
// This is particularly useful when the Vite dev server temporarily rejects connections due to
// high concurrency with many dynamic imports.
type retryTransport struct {
base http.RoundTripper
maxRetries int
delay time.Duration
}
// RoundTrip executes a single HTTP transaction with retry logic.
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
}
// isConnectionError checks if the error is a connection-related error that may be transient.
func isConnectionError(err error) bool {
if err == nil {
return false
}
errStr := strings.ToLower(err.Error())
return strings.Contains(errStr, "connection refused") ||
strings.Contains(errStr, "connection reset") ||
strings.Contains(errStr, "broken pipe") ||
strings.Contains(errStr, "connectex")
}
func NewAssetFileServer(vfs fs.FS) http.Handler {
devServerURL := GetDevServerURL()
if devServerURL == "" {
return newAssetFileServerFS(vfs)
}
parsedURL, err := url.Parse(devServerURL)
if err != nil {
return http.HandlerFunc(
func(rw http.ResponseWriter, req *http.Request) {
logError(req.Context(), "[ExternalAssetHandler] Invalid FRONTEND_DEVSERVER_URL. Should be valid URL", "error", err.Error())
http.Error(rw, err.Error(), http.StatusInternalServerError)
})
}
dialer := &net.Dialer{
Timeout: 5 * time.Second,
KeepAlive: 30 * time.Second,
}
proxy := httputil.NewSingleHostReverseProxy(parsedURL)
proxy.Transport = &retryTransport{
base: &http.Transport{
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,
}
proxy.ErrorHandler = func(rw http.ResponseWriter, r *http.Request, err error) {
logError(r.Context(), "[ExternalAssetHandler] Proxy error", "error", err.Error())
rw.WriteHeader(http.StatusBadGateway)
}
return proxy
}
func GetDevServerURL() string {
return os.Getenv("FRONTEND_DEVSERVER_URL")
}

View File

@@ -0,0 +1,16 @@
//go:build production
package assetserver
import (
"io/fs"
"net/http"
)
func NewAssetFileServer(vfs fs.FS) http.Handler {
return newAssetFileServerFS(vfs)
}
func GetDevServerURL() string {
return ""
}

View File

@@ -0,0 +1,33 @@
package assetserver
import (
"github.com/wailsapp/wails/v3/internal/assetserver/bundledassets"
"io/fs"
"net/http"
"strings"
)
type BundledAssetServer struct {
handler http.Handler
}
func NewBundledAssetFileServer(fs fs.FS) *BundledAssetServer {
return &BundledAssetServer{
handler: NewAssetFileServer(fs),
}
}
func (b *BundledAssetServer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if strings.HasPrefix(req.URL.Path, "/wails/") {
// Strip the /wails prefix
req.URL.Path = req.URL.Path[6:]
switch req.URL.Path {
case "/runtime.js":
rw.Header().Set("Content-Type", "application/javascript")
rw.Write([]byte(bundledassets.RuntimeJS))
return
}
return
}
b.handler.ServeHTTP(rw, req)
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,8 @@
//go:build !production
package bundledassets
import _ "embed"
//go:embed runtime.debug.js
var RuntimeJS []byte

View File

@@ -0,0 +1,8 @@
//go:build production
package bundledassets
import _ "embed"
//go:embed runtime.js
var RuntimeJS []byte

View File

@@ -0,0 +1,66 @@
package assetserver
import (
"context"
"fmt"
"log/slog"
"net/http"
"strings"
)
const (
HeaderHost = "Host"
HeaderContentType = "Content-Type"
HeaderContentLength = "Content-Length"
HeaderUserAgent = "User-Agent"
// TODO: Is this needed?
HeaderCacheControl = "Cache-Control"
HeaderUpgrade = "Upgrade"
WailsUserAgentValue = "wails.io"
)
type assetServerLogger struct{}
var assetServerLoggerKey assetServerLogger
// ServeFile writes the provided blob to rw as an HTTP 200 response, ensuring appropriate
// Content-Length and Content-Type headers are set.
//
// If the Content-Type header is not already present, ServeFile determines an appropriate
// MIME type from the filename and blob and sets the Content-Type header. It then writes
// the 200 status and the blob body to the response, returning any error encountered while
// writing the body.
func ServeFile(rw http.ResponseWriter, filename string, blob []byte) error {
header := rw.Header()
header.Set(HeaderContentLength, fmt.Sprintf("%d", len(blob)))
if mimeType := header.Get(HeaderContentType); mimeType == "" {
mimeType = GetMimetype(filename, blob)
header.Set(HeaderContentType, mimeType)
}
rw.WriteHeader(http.StatusOK)
_, err := rw.Write(blob)
return err
}
func isWebSocket(req *http.Request) bool {
upgrade := req.Header.Get(HeaderUpgrade)
return strings.EqualFold(upgrade, "websocket")
}
func contextWithLogger(ctx context.Context, logger *slog.Logger) context.Context {
return context.WithValue(ctx, assetServerLoggerKey, logger)
}
func logInfo(ctx context.Context, message string, args ...interface{}) {
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(assetServerLoggerKey).(*slog.Logger); logger != nil {
logger.Error(message, args...)
}
}

View File

@@ -0,0 +1,142 @@
package assetserver
import (
"net/http"
)
// newContentTypeSniffer creates a contentTypeSniffer that wraps the provided http.ResponseWriter.
// The returned sniffer does not allocate a close notification channel; it will be initialized lazily by CloseNotify.
func newContentTypeSniffer(rw http.ResponseWriter) *contentTypeSniffer {
return &contentTypeSniffer{
rw: rw,
}
}
type contentTypeSniffer struct {
rw http.ResponseWriter
prefix []byte
closeChannel chan bool // lazily allocated only if CloseNotify is called
status int
headerCommitted bool
headerWritten bool
}
// 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(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.headerCommitted {
return
}
rw.status = code
rw.headerCommitted = true
if _, hasType := rw.Header()[HeaderContentType]; hasType {
rw.rw.WriteHeader(rw.status)
rw.headerWritten = true
}
}
// 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.Header()
if _, hasType := m[HeaderContentType]; !hasType {
m.Set(HeaderContentType, http.DetectContentType(rw.prefix))
}
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
}
// CloseNotify implements the http.CloseNotifier interface.
// The channel is lazily allocated to avoid allocation overhead for requests
// that don't use this deprecated interface.
func (rw *contentTypeSniffer) CloseNotify() <-chan bool {
if rw.closeChannel == nil {
rw.closeChannel = make(chan bool, 1)
}
return rw.closeChannel
}
func (rw *contentTypeSniffer) closeClient() {
if rw.closeChannel != nil {
rw.closeChannel <- true
}
}
// Flush implements the http.Flusher interface.
func (rw *contentTypeSniffer) Flush() {
if f, ok := rw.rw.(http.Flusher); ok {
f.Flush()
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,80 @@
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)
}
// Flush implements the http.Flusher interface.
func (rw *fallbackResponseWriter) Flush() {
if f, ok := rw.rw.(http.Flusher); ok {
f.Flush()
}
}

View File

@@ -0,0 +1,76 @@
package assetserver
import (
"embed"
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"strings"
)
// 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 := errors.New("files or multiple dirs found")
fPath := ""
err := fs.WalkDir(fileSystem, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
fPath = path
if entries, dErr := fs.ReadDir(fileSystem, path); dErr != nil {
return dErr
} else if len(entries) <= 1 {
return nil
}
}
return stopErr
})
if err != nil && !errors.Is(err, stopErr) {
return "", err
}
return fPath, nil
}
func findPathToFile(fileSystem fs.FS, file string) (string, error) {
stat, _ := fs.Stat(fileSystem, file)
if stat != nil {
return ".", nil
}
var indexFiles []string
err := fs.WalkDir(fileSystem, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if strings.HasSuffix(path, file) {
indexFiles = append(indexFiles, path)
}
return nil
})
if err != nil {
return "", err
}
if len(indexFiles) > 1 {
selected := indexFiles[0]
for _, f := range indexFiles {
if len(f) < len(selected) {
selected = f
}
}
path, _ := filepath.Split(selected)
return path, nil
}
if len(indexFiles) > 0 {
path, _ := filepath.Split(indexFiles[0])
return path, nil
}
return "", fmt.Errorf("%s: %w", file, os.ErrNotExist)
}

View File

@@ -0,0 +1,20 @@
package assetserver
import (
"net/http"
)
// Middleware defines a HTTP middleware that can be applied to the AssetServer.
// The handler passed as next is the next handler in the chain. One can decide to call the next handler
// or implement a specialized handling.
type Middleware func(next http.Handler) http.Handler
// ChainMiddleware allows chaining multiple middlewares to one middleware.
func ChainMiddleware(middleware ...Middleware) Middleware {
return func(h http.Handler) http.Handler {
for i := len(middleware) - 1; i >= 0; i-- {
h = middleware[i](h)
}
return h
}
}

View File

@@ -0,0 +1,116 @@
package assetserver
import (
"net/http"
"path/filepath"
"sync"
)
var (
// mimeCache uses sync.Map for better concurrent read performance
// since reads are far more common than writes
mimeCache sync.Map
// mimeTypesByExt maps file extensions to MIME types for common web formats.
// This approach is preferred over content-based detection because:
// 1. Extension-based lookup is O(1) vs O(n) content scanning
// 2. Web assets typically have correct extensions
// 3. stdlib's http.DetectContentType handles remaining cases adequately
// 4. Saves ~208KB binary size by not using github.com/wailsapp/mimetype
mimeTypesByExt = map[string]string{
// HTML
".htm": "text/html; charset=utf-8",
".html": "text/html; charset=utf-8",
// CSS/JS
".css": "text/css; charset=utf-8",
".js": "text/javascript; charset=utf-8",
".mjs": "text/javascript; charset=utf-8",
".ts": "application/x-typescript; charset=utf-8",
".tsx": "application/x-typescript; charset=utf-8",
".jsx": "text/javascript; charset=utf-8",
// Data formats
".json": "application/json",
".xml": "text/xml; charset=utf-8",
".yaml": "text/yaml; charset=utf-8",
".yml": "text/yaml; charset=utf-8",
".toml": "text/toml; charset=utf-8",
// Images
".png": "image/png",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".gif": "image/gif",
".webp": "image/webp",
".avif": "image/avif",
".svg": "image/svg+xml",
".ico": "image/x-icon",
".bmp": "image/bmp",
".tiff": "image/tiff",
".tif": "image/tiff",
// Fonts
".woff": "font/woff",
".woff2": "font/woff2",
".ttf": "font/ttf",
".otf": "font/otf",
".eot": "application/vnd.ms-fontobject",
// Audio
".mp3": "audio/mpeg",
".wav": "audio/wav",
".ogg": "audio/ogg",
".m4a": "audio/mp4",
".aac": "audio/aac",
".flac": "audio/flac",
".opus": "audio/opus",
// Video
".mp4": "video/mp4",
".webm": "video/webm",
".ogv": "video/ogg",
".mov": "video/quicktime",
".avi": "video/x-msvideo",
".mkv": "video/x-matroska",
".m4v": "video/mp4",
// Documents
".pdf": "application/pdf",
".txt": "text/plain; charset=utf-8",
".md": "text/markdown; charset=utf-8",
// Archives
".zip": "application/zip",
".gz": "application/gzip",
".tar": "application/x-tar",
// WebAssembly
".wasm": "application/wasm",
// Source maps
".map": "application/json",
}
)
// "application/octet-stream".
func GetMimetype(filename string, data []byte) string {
// Fast path: check extension map first (no lock needed)
if result := mimeTypesByExt[filepath.Ext(filename)]; result != "" {
return result
}
// Check cache (lock-free read)
if cached, ok := mimeCache.Load(filename); ok {
return cached.(string)
}
// Slow path: use stdlib content-based detection and cache
result := http.DetectContentType(data)
if result == "" {
result = "application/octet-stream"
}
mimeCache.Store(filename, result)
return result
}

View File

@@ -0,0 +1,38 @@
package assetserver
import (
"errors"
"log/slog"
"net/http"
)
// Options defines the configuration of the AssetServer.
type Options struct {
// Handler which serves all the content to the WebView.
Handler http.Handler
// Middleware is a HTTP Middleware which allows to hook into the AssetServer request chain. It allows to skip the default
// request handler dynamically, e.g. implement specialized Routing etc.
// The Middleware is called to build a new `http.Handler` used by the AssetSever and it also receives the default
// handler used by the AssetServer as an argument.
//
// This middleware injects itself before any of Wails internal middlewares.
//
// If not defined, the default AssetServer request chain is executed.
//
// Multiple Middlewares can be chained together with:
// ChainMiddleware(middleware ...Middleware) Middleware
Middleware Middleware
// Logger is the logger used by the AssetServer. If not defined, no logging will be done.
Logger *slog.Logger
}
// Validate the options
func (o Options) Validate() error {
if o.Handler == nil && o.Middleware == nil {
return errors.New("AssetServer options invalid: either Handler or Middleware must be set")
}
return nil
}

View File

@@ -0,0 +1,101 @@
// Code from https://github.com/erikdubbelboer/ringqueue
/*
The MIT License (MIT)
Copyright (c) 2015 Erik Dubbelboer
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
package assetserver
type ringqueue[T any] struct {
nodes []T
head int
tail int
cnt int
minSize int
}
func newRingqueue[T any](minSize uint) *ringqueue[T] {
if minSize < 2 {
minSize = 2
}
return &ringqueue[T]{
nodes: make([]T, minSize),
minSize: int(minSize),
}
}
func (q *ringqueue[T]) resize(n int) {
nodes := make([]T, n)
if q.head < q.tail {
copy(nodes, q.nodes[q.head:q.tail])
} else {
copy(nodes, q.nodes[q.head:])
copy(nodes[len(q.nodes)-q.head:], q.nodes[:q.tail])
}
q.tail = q.cnt % n
q.head = 0
q.nodes = nodes
}
func (q *ringqueue[T]) Add(i T) {
if q.cnt == len(q.nodes) {
// Also tested a grow rate of 1.5, see: http://stackoverflow.com/questions/2269063/buffer-growth-strategy
// In Go this resulted in a higher memory usage.
q.resize(q.cnt * 2)
}
q.nodes[q.tail] = i
q.tail = (q.tail + 1) % len(q.nodes)
q.cnt++
}
func (q *ringqueue[T]) Peek() (T, bool) {
if q.cnt == 0 {
var none T
return none, false
}
return q.nodes[q.head], true
}
func (q *ringqueue[T]) Remove() (T, bool) {
if q.cnt == 0 {
var none T
return none, false
}
i := q.nodes[q.head]
q.head = (q.head + 1) % len(q.nodes)
q.cnt--
if n := len(q.nodes) / 2; n > q.minSize && q.cnt <= n {
q.resize(n)
}
return i, true
}
func (q *ringqueue[T]) Cap() int {
return cap(q.nodes)
}
func (q *ringqueue[T]) Len() int {
return q.cnt
}

View File

@@ -0,0 +1,152 @@
//go:build linux && cgo && !android
package webview
/*
#cgo linux pkg-config: glib-2.0
#include <glib.h>
#include <stdint.h>
extern void webviewMainThreadCallback(uintptr_t id);
// webview_dispatch_mu serializes the enabled-check plus scheduling in
// webview_invoke_on_main_sync against the flag clear in
// webview_disable_main_dispatch. Without it a worker could read
// enabled == TRUE, then have the flag flipped before it scheduled its source,
// and still queue work onto the now-dead main loop — blocking forever. A
// statically allocated GMutex needs no g_mutex_init.
static GMutex webview_dispatch_mu;
// webview_main_dispatch_enabled gates whether webview_invoke_on_main_sync may
// schedule work onto the GTK main loop. It starts enabled and is cleared once
// the loop has stopped (see webview_disable_main_dispatch). It is only ever read
// or written while holding webview_dispatch_mu.
static gboolean webview_main_dispatch_enabled = TRUE;
static void webview_disable_main_dispatch(void) {
g_mutex_lock(&webview_dispatch_mu);
webview_main_dispatch_enabled = FALSE;
g_mutex_unlock(&webview_dispatch_mu);
}
typedef struct {
uintptr_t id;
GMutex mutex;
GCond cond;
gboolean done;
} webviewMainSyncCall;
// webview_main_sync_trampoline runs on the GTK main thread (scheduled via
// g_main_context_invoke). It invokes the Go callback, then signals the waiting
// worker. g_cond_signal happens while the mutex is held and before the unlock,
// so the waiter cannot re-acquire the mutex (and destroy the primitives) until
// the signal has completed — making the stack-allocated GMutex/GCond safe.
static gboolean webview_main_sync_trampoline(gpointer data) {
webviewMainSyncCall *call = (webviewMainSyncCall *)data;
webviewMainThreadCallback(call->id);
g_mutex_lock(&call->mutex);
call->done = TRUE;
g_cond_signal(&call->cond);
g_mutex_unlock(&call->mutex);
return G_SOURCE_REMOVE;
}
// webview_invoke_on_main_sync schedules the Go callback identified by id on the
// default GTK main context and blocks the calling thread until it has finished.
//
// WebKit2GTK objects may only be touched on the thread running the GTK main loop
// (g_application_run). Asset-server responses are produced on worker goroutines,
// so the WebKit calls that complete a request must hop here first. The wait is
// safe because webkit_uri_scheme_request_finish_with_response returns before the
// response stream is drained (WebKit reads it asynchronously), so the main loop
// never blocks waiting on the worker.
//
// If the caller is already the main thread, g_main_context_invoke runs the
// trampoline inline, so the wait completes immediately without deadlocking.
static void webview_invoke_on_main_sync(uintptr_t id) {
webviewMainSyncCall call;
call.id = id;
call.done = FALSE;
g_mutex_init(&call.mutex);
g_cond_init(&call.cond);
// The enabled-check and the g_main_context_invoke that acts on it must be
// atomic with respect to webview_disable_main_dispatch. Holding
// webview_dispatch_mu across both means a worker either schedules onto a live
// loop or sees the loop already stopped — it can never schedule onto a loop
// that stops in between (which would block it here forever). The trampoline
// only touches the per-call mutex/cond, never webview_dispatch_mu, so when
// g_main_context_invoke runs it inline (main-thread caller) there is no
// self-deadlock.
g_mutex_lock(&webview_dispatch_mu);
if (!webview_main_dispatch_enabled) {
// The GTK main loop has stopped: a scheduled source would never run. The
// loop is no longer iterating, so the cross-thread race that makes
// main-thread confinement necessary is gone — running the callback inline
// on the worker lets in-flight asset requests drain during shutdown
// instead of wedging. See #5631 (review question 5).
g_mutex_unlock(&webview_dispatch_mu);
webviewMainThreadCallback(id);
g_mutex_clear(&call.mutex);
g_cond_clear(&call.cond);
return;
}
g_main_context_invoke(NULL, webview_main_sync_trampoline, &call);
g_mutex_unlock(&webview_dispatch_mu);
g_mutex_lock(&call.mutex);
while (!call.done) {
g_cond_wait(&call.cond, &call.mutex);
}
g_mutex_unlock(&call.mutex);
g_mutex_clear(&call.mutex);
g_cond_clear(&call.cond);
}
*/
import "C"
import (
"sync"
)
var (
mainSyncMu sync.Mutex
mainSyncNextID uintptr
mainSyncCallbacks = map[uintptr]func(){}
)
// invokeOnMainSync runs fn on the GTK main thread and blocks until it returns.
// It is safe to call from any goroutine, including the main thread itself.
func invokeOnMainSync(fn func()) {
mainSyncMu.Lock()
mainSyncNextID++
id := mainSyncNextID
mainSyncCallbacks[id] = fn
mainSyncMu.Unlock()
C.webview_invoke_on_main_sync(C.uintptr_t(id))
}
// DisableMainThreadDispatch marks the GTK main loop as stopped. After it is
// called, invokeOnMainSync runs callbacks inline on the calling goroutine
// instead of scheduling them onto the now-dead main loop, so asset-server
// workers that complete a request during shutdown cannot block forever waiting
// for a source that will never be serviced. The application layer calls this
// once g_application_run has returned. See issue #5631.
func DisableMainThreadDispatch() {
C.webview_disable_main_dispatch()
}
//export webviewMainThreadCallback
func webviewMainThreadCallback(id C.uintptr_t) {
mainSyncMu.Lock()
fn := mainSyncCallbacks[uintptr(id)]
delete(mainSyncCallbacks, uintptr(id))
mainSyncMu.Unlock()
if fn != nil {
fn()
}
}

View File

@@ -0,0 +1,90 @@
//go:build linux && cgo && !android
package webview
// This file provides cgo helpers for mainthread_linux_test.go. Go forbids cgo
// (an `import "C"` preamble) inside _test.go files, so the small GLib main-loop
// scaffolding the test needs lives here instead. None of it is referenced by
// production code paths.
/*
#cgo linux pkg-config: glib-2.0
#include <glib.h>
// All shared loop state is published and read under webview_test_mu so the
// worker goroutines that call the helpers below never observe it across threads
// without synchronization. webview_test_ready is signalled from inside the loop
// (via an idle source) so waiters block on the cond instead of spinning on an
// unsynchronized flag.
static GMutex webview_test_mu;
static GCond webview_test_cond;
static GMainLoop *webview_test_loop; // guarded by webview_test_mu
static GThread *webview_test_loop_thread; // guarded by webview_test_mu
static gboolean webview_test_ready; // guarded by webview_test_mu
// webview_test_mark_ready runs on the loop thread once the loop starts
// iterating, publishing that it is live.
static gboolean webview_test_mark_ready(gpointer data) {
g_mutex_lock(&webview_test_mu);
webview_test_ready = TRUE;
g_cond_signal(&webview_test_cond);
g_mutex_unlock(&webview_test_mu);
return G_SOURCE_REMOVE;
}
// webview_test_run_loop records the running thread and drives the default GLib
// main context, mimicking the GTK main loop that invokeOnMainSync dispatches to.
static void webview_test_run_loop(void) {
GMainLoop *loop = g_main_loop_new(NULL, FALSE);
g_mutex_lock(&webview_test_mu);
webview_test_loop = loop;
webview_test_loop_thread = g_thread_self();
g_mutex_unlock(&webview_test_mu);
g_idle_add(webview_test_mark_ready, NULL);
g_main_loop_run(loop);
g_mutex_lock(&webview_test_mu);
webview_test_loop = NULL;
webview_test_ready = FALSE;
g_mutex_unlock(&webview_test_mu);
g_main_loop_unref(loop);
}
static void webview_test_wait_running(void) {
g_mutex_lock(&webview_test_mu);
while (!webview_test_ready) {
g_cond_wait(&webview_test_cond, &webview_test_mu);
}
g_mutex_unlock(&webview_test_mu);
}
static void webview_test_quit_loop(void) {
g_mutex_lock(&webview_test_mu);
GMainLoop *loop = webview_test_loop;
g_mutex_unlock(&webview_test_mu);
if (loop != NULL) {
g_main_loop_quit(loop);
}
}
// webview_test_on_loop_thread reports whether the caller is the thread that runs
// the main loop.
static int webview_test_on_loop_thread(void) {
g_mutex_lock(&webview_test_mu);
GThread *loopThread = webview_test_loop_thread;
g_mutex_unlock(&webview_test_mu);
return g_thread_self() == loopThread ? 1 : 0;
}
*/
import "C"
func testRunMainLoop() { C.webview_test_run_loop() }
func testWaitLoopRunning() { C.webview_test_wait_running() }
func testQuitMainLoop() { C.webview_test_quit_loop() }
func testOnLoopThread() bool {
return C.webview_test_on_loop_thread() != 0
}

View File

@@ -0,0 +1,17 @@
package webview
import (
"io"
"net/http"
)
type Request interface {
URL() (string, error)
Method() (string, error)
Header() (http.Header, error)
Body() (io.ReadCloser, error)
Response() ResponseWriter
Close() error
}

View File

@@ -0,0 +1,102 @@
//go:build android
package webview
import (
"bytes"
"io"
"net/http"
)
// Request interface for Android asset requests
// On Android, requests are handled via JNI from Java's WebViewAssetLoader
// androidRequest implements the Request interface for Android
type androidRequest struct {
url string
method string
headers http.Header
body io.ReadCloser
rw *androidResponseWriter
}
// NewRequestFromJNI creates a new request from JNI parameters
func NewRequestFromJNI(url string, method string, headersJSON string) Request {
return &androidRequest{
url: url,
method: method,
headers: http.Header{},
body: http.NoBody,
}
}
func (r *androidRequest) URL() (string, error) {
return r.url, nil
}
func (r *androidRequest) Method() (string, error) {
return r.method, nil
}
func (r *androidRequest) Header() (http.Header, error) {
return r.headers, nil
}
func (r *androidRequest) Body() (io.ReadCloser, error) {
return r.body, nil
}
func (r *androidRequest) Response() ResponseWriter {
if r.rw == nil {
r.rw = &androidResponseWriter{}
}
return r.rw
}
func (r *androidRequest) Close() error {
if r.body != nil {
return r.body.Close()
}
return nil
}
// androidResponseWriter implements ResponseWriter for Android
type androidResponseWriter struct {
statusCode int
headers http.Header
body bytes.Buffer
finished bool
}
func (w *androidResponseWriter) Header() http.Header {
if w.headers == nil {
w.headers = http.Header{}
}
return w.headers
}
func (w *androidResponseWriter) Write(data []byte) (int, error) {
return w.body.Write(data)
}
func (w *androidResponseWriter) WriteHeader(statusCode int) {
w.statusCode = statusCode
}
func (w *androidResponseWriter) Finish() error {
w.finished = true
return nil
}
// Code returns the HTTP status code of the response
func (w *androidResponseWriter) Code() int {
if w.statusCode == 0 {
return 200
}
return w.statusCode
}
// GetResponseData returns the response data for JNI
func (w *androidResponseWriter) GetResponseData() []byte {
return w.body.Bytes()
}

View File

@@ -0,0 +1,250 @@
//go:build darwin && !ios
package webview
/*
#cgo CFLAGS: -x objective-c
#cgo LDFLAGS: -framework Foundation -framework WebKit
#import <Foundation/Foundation.h>
#import <WebKit/WebKit.h>
#include <string.h>
static void URLSchemeTaskRetain(void *wkUrlSchemeTask) {
id<WKURLSchemeTask> urlSchemeTask = (id<WKURLSchemeTask>) wkUrlSchemeTask;
[urlSchemeTask retain];
}
static void URLSchemeTaskRelease(void *wkUrlSchemeTask) {
id<WKURLSchemeTask> urlSchemeTask = (id<WKURLSchemeTask>) wkUrlSchemeTask;
[urlSchemeTask release];
}
static const char * URLSchemeTaskRequestURL(void *wkUrlSchemeTask) {
id<WKURLSchemeTask> urlSchemeTask = (id<WKURLSchemeTask>) wkUrlSchemeTask;
@autoreleasepool {
return [urlSchemeTask.request.URL.absoluteString UTF8String];
}
}
static const char * URLSchemeTaskRequestMethod(void *wkUrlSchemeTask) {
id<WKURLSchemeTask> urlSchemeTask = (id<WKURLSchemeTask>) wkUrlSchemeTask;
@autoreleasepool {
return [urlSchemeTask.request.HTTPMethod UTF8String];
}
}
static const char * URLSchemeTaskRequestHeadersJSON(void *wkUrlSchemeTask) {
id<WKURLSchemeTask> urlSchemeTask = (id<WKURLSchemeTask>) wkUrlSchemeTask;
@autoreleasepool {
NSData *headerData = [NSJSONSerialization dataWithJSONObject: urlSchemeTask.request.allHTTPHeaderFields options:0 error: nil];
if (!headerData) {
return nil;
}
NSString* headerString = [[[NSString alloc] initWithData:headerData encoding:NSUTF8StringEncoding] autorelease];
const char * headerJSON = [headerString UTF8String];
return strdup(headerJSON);
}
}
static bool URLSchemeTaskRequestBodyBytes(void *wkUrlSchemeTask, const void **body, int *bodyLen) {
id<WKURLSchemeTask> urlSchemeTask = (id<WKURLSchemeTask>) wkUrlSchemeTask;
@autoreleasepool {
if (!urlSchemeTask.request.HTTPBody) {
return false;
}
*body = urlSchemeTask.request.HTTPBody.bytes;
*bodyLen = urlSchemeTask.request.HTTPBody.length;
return true;
}
}
static bool URLSchemeTaskRequestBodyStreamOpen(void *wkUrlSchemeTask) {
id<WKURLSchemeTask> urlSchemeTask = (id<WKURLSchemeTask>) wkUrlSchemeTask;
@autoreleasepool {
if (!urlSchemeTask.request.HTTPBodyStream) {
return false;
}
[urlSchemeTask.request.HTTPBodyStream open];
return true;
}
}
static void URLSchemeTaskRequestBodyStreamClose(void *wkUrlSchemeTask) {
id<WKURLSchemeTask> urlSchemeTask = (id<WKURLSchemeTask>) wkUrlSchemeTask;
@autoreleasepool {
if (!urlSchemeTask.request.HTTPBodyStream) {
return;
}
[urlSchemeTask.request.HTTPBodyStream close];
}
}
static int URLSchemeTaskRequestBodyStreamRead(void *wkUrlSchemeTask, void *buf, int bufLen) {
id<WKURLSchemeTask> urlSchemeTask = (id<WKURLSchemeTask>) wkUrlSchemeTask;
@autoreleasepool {
NSInputStream *stream = urlSchemeTask.request.HTTPBodyStream;
if (!stream) {
return -2;
}
NSStreamStatus status = stream.streamStatus;
if (status == NSStreamStatusAtEnd || !stream.hasBytesAvailable) {
return 0;
} else if (status != NSStreamStatusOpen) {
return -3;
}
return [stream read:buf maxLength:bufLen];
}
}
*/
import "C"
import (
"bytes"
"errors"
"fmt"
"io"
"net/http"
"unsafe"
"encoding/json"
)
// NewRequest creates as new WebViewRequest based on a pointer to an `id<WKURLSchemeTask>`
func NewRequest(wkURLSchemeTask unsafe.Pointer) Request {
C.URLSchemeTaskRetain(wkURLSchemeTask)
return newRequestFinalizer(&request{task: wkURLSchemeTask})
}
var _ Request = &request{}
type request struct {
task unsafe.Pointer
header http.Header
body io.ReadCloser
rw *responseWriter
}
func (r *request) URL() (string, error) {
return C.GoString(C.URLSchemeTaskRequestURL(r.task)), nil
}
func (r *request) Method() (string, error) {
return C.GoString(C.URLSchemeTaskRequestMethod(r.task)), nil
}
func (r *request) Header() (http.Header, error) {
if r.header != nil {
return r.header, nil
}
header := http.Header{}
if cHeaders := C.URLSchemeTaskRequestHeadersJSON(r.task); cHeaders != nil {
if headers := C.GoString(cHeaders); headers != "" {
var h map[string]string
if err := json.Unmarshal([]byte(headers), &h); err != nil {
return nil, fmt.Errorf("unable to unmarshal request headers: %s", err)
}
for k, v := range h {
header.Add(k, v)
}
}
C.free(unsafe.Pointer(cHeaders))
}
r.header = header
return header, nil
}
func (r *request) Body() (io.ReadCloser, error) {
if r.body != nil {
return r.body, nil
}
var body unsafe.Pointer
var bodyLen C.int
if C.URLSchemeTaskRequestBodyBytes(r.task, &body, &bodyLen) {
if body != nil && bodyLen > 0 {
r.body = io.NopCloser(bytes.NewReader(C.GoBytes(body, bodyLen)))
} else {
r.body = http.NoBody
}
} else if C.URLSchemeTaskRequestBodyStreamOpen(r.task) {
r.body = &requestBodyStreamReader{task: r.task}
}
return r.body, nil
}
func (r *request) Response() ResponseWriter {
if r.rw != nil {
return r.rw
}
r.rw = &responseWriter{r: r}
return r.rw
}
func (r *request) Close() error {
var err error
if r.body != nil {
err = r.body.Close()
}
r.Response().Finish()
C.URLSchemeTaskRelease(r.task)
return err
}
var _ io.ReadCloser = &requestBodyStreamReader{}
type requestBodyStreamReader struct {
task unsafe.Pointer
closed bool
}
// Read implements io.Reader
func (r *requestBodyStreamReader) Read(p []byte) (n int, err error) {
var content unsafe.Pointer
var contentLen int
if p != nil {
content = unsafe.Pointer(&p[0])
contentLen = len(p)
}
res := C.URLSchemeTaskRequestBodyStreamRead(r.task, content, C.int(contentLen))
if res > 0 {
return int(res), nil
}
switch res {
case 0:
return 0, io.EOF
case -1:
return 0, errors.New("body: stream error")
case -2:
return 0, errors.New("body: no stream defined")
case -3:
return 0, io.ErrClosedPipe
default:
return 0, fmt.Errorf("body: unknown error %d", res)
}
}
func (r *requestBodyStreamReader) Close() error {
if r.closed {
return nil
}
r.closed = true
C.URLSchemeTaskRequestBodyStreamClose(r.task)
return nil
}

View File

@@ -0,0 +1,40 @@
package webview
import (
"runtime"
"sync/atomic"
)
var _ Request = &requestFinalizer{}
type requestFinalizer struct {
Request
closed int32
}
// newRequestFinalizer returns a request with a runtime finalizer to make sure it will be closed from the finalizer
// if it has not been already closed.
// It also makes sure Close() of the wrapping request is only called once.
func newRequestFinalizer(r Request) Request {
rf := &requestFinalizer{Request: r}
// Make sure to async release since it might block the finalizer goroutine for a longer period
runtime.SetFinalizer(rf, func(obj *requestFinalizer) { rf.close(true) })
return rf
}
func (r *requestFinalizer) Close() error {
return r.close(false)
}
func (r *requestFinalizer) close(asyncRelease bool) error {
if atomic.CompareAndSwapInt32(&r.closed, 0, 1) {
runtime.SetFinalizer(r, nil)
if asyncRelease {
go r.Request.Close()
return nil
} else {
return r.Request.Close()
}
}
return nil
}

View File

@@ -0,0 +1,248 @@
//go:build ios
package webview
/*
#cgo CFLAGS: -x objective-c -fobjc-arc
#cgo LDFLAGS: -framework Foundation -framework WebKit -framework CoreFoundation
#import <Foundation/Foundation.h>
#import <WebKit/WebKit.h>
#import <CoreFoundation/CoreFoundation.h>
#include <string.h>
static void URLSchemeTaskRetain(void *wkUrlSchemeTask) {
id<WKURLSchemeTask> urlSchemeTask = (__bridge id<WKURLSchemeTask>) wkUrlSchemeTask;
CFRetain((CFTypeRef)urlSchemeTask);
}
static void URLSchemeTaskRelease(void *wkUrlSchemeTask) {
id<WKURLSchemeTask> urlSchemeTask = (__bridge id<WKURLSchemeTask>) wkUrlSchemeTask;
CFRelease((CFTypeRef)urlSchemeTask);
}
static const char * URLSchemeTaskRequestURL(void *wkUrlSchemeTask) {
id<WKURLSchemeTask> urlSchemeTask = (__bridge id<WKURLSchemeTask>) wkUrlSchemeTask;
@autoreleasepool {
return [urlSchemeTask.request.URL.absoluteString UTF8String];
}
}
static const char * URLSchemeTaskRequestMethod(void *wkUrlSchemeTask) {
id<WKURLSchemeTask> urlSchemeTask = (__bridge id<WKURLSchemeTask>) wkUrlSchemeTask;
@autoreleasepool {
return [urlSchemeTask.request.HTTPMethod UTF8String];
}
}
static const char * URLSchemeTaskRequestHeadersJSON(void *wkUrlSchemeTask) {
id<WKURLSchemeTask> urlSchemeTask = (__bridge id<WKURLSchemeTask>) wkUrlSchemeTask;
@autoreleasepool {
NSData *headerData = [NSJSONSerialization dataWithJSONObject:urlSchemeTask.request.allHTTPHeaderFields options:0 error:nil];
if (!headerData) {
return nil;
}
NSString *headerString = [[NSString alloc] initWithData:headerData encoding:NSUTF8StringEncoding];
const char *headerJSON = [headerString UTF8String];
return strdup(headerJSON);
}
}
static bool URLSchemeTaskRequestBodyBytes(void *wkUrlSchemeTask, const void **body, int *bodyLen) {
id<WKURLSchemeTask> urlSchemeTask = (__bridge id<WKURLSchemeTask>) wkUrlSchemeTask;
@autoreleasepool {
if (!urlSchemeTask.request.HTTPBody) {
return false;
}
*body = urlSchemeTask.request.HTTPBody.bytes;
*bodyLen = urlSchemeTask.request.HTTPBody.length;
return true;
}
}
static bool URLSchemeTaskRequestBodyStreamOpen(void *wkUrlSchemeTask) {
id<WKURLSchemeTask> urlSchemeTask = (__bridge id<WKURLSchemeTask>) wkUrlSchemeTask;
@autoreleasepool {
if (!urlSchemeTask.request.HTTPBodyStream) {
return false;
}
[urlSchemeTask.request.HTTPBodyStream open];
return true;
}
}
static void URLSchemeTaskRequestBodyStreamClose(void *wkUrlSchemeTask) {
id<WKURLSchemeTask> urlSchemeTask = (__bridge id<WKURLSchemeTask>) wkUrlSchemeTask;
@autoreleasepool {
if (!urlSchemeTask.request.HTTPBodyStream) {
return;
}
[urlSchemeTask.request.HTTPBodyStream close];
}
}
static int URLSchemeTaskRequestBodyStreamRead(void *wkUrlSchemeTask, void *buf, int bufLen) {
id<WKURLSchemeTask> urlSchemeTask = (__bridge id<WKURLSchemeTask>) wkUrlSchemeTask;
@autoreleasepool {
NSInputStream *stream = urlSchemeTask.request.HTTPBodyStream;
if (!stream) {
return -2;
}
NSStreamStatus status = stream.streamStatus;
if (status == NSStreamStatusAtEnd || !stream.hasBytesAvailable) {
return 0;
} else if (status != NSStreamStatusOpen) {
return -3;
}
return [stream read:buf maxLength:bufLen];
}
}
*/
import "C"
import (
"bytes"
"errors"
"fmt"
"io"
"net/http"
"unsafe"
"encoding/json"
)
// NewRequest creates as new WebViewRequest based on a pointer to an `id<WKURLSchemeTask>`
func NewRequest(wkURLSchemeTask unsafe.Pointer) Request {
C.URLSchemeTaskRetain(wkURLSchemeTask)
return newRequestFinalizer(&request{task: wkURLSchemeTask})
}
var _ Request = &request{}
type request struct {
task unsafe.Pointer
header http.Header
body io.ReadCloser
rw *responseWriter
}
func (r *request) URL() (string, error) {
return C.GoString(C.URLSchemeTaskRequestURL(r.task)), nil
}
func (r *request) Method() (string, error) {
return C.GoString(C.URLSchemeTaskRequestMethod(r.task)), nil
}
func (r *request) Header() (http.Header, error) {
if r.header != nil {
return r.header, nil
}
header := http.Header{}
if cHeaders := C.URLSchemeTaskRequestHeadersJSON(r.task); cHeaders != nil {
if headers := C.GoString(cHeaders); headers != "" {
var h map[string]string
if err := json.Unmarshal([]byte(headers), &h); err != nil {
return nil, fmt.Errorf("unable to unmarshal request headers: %s", err)
}
for k, v := range h {
header.Add(k, v)
}
}
C.free(unsafe.Pointer(cHeaders))
}
r.header = header
return header, nil
}
func (r *request) Body() (io.ReadCloser, error) {
if r.body != nil {
return r.body, nil
}
var body unsafe.Pointer
var bodyLen C.int
if C.URLSchemeTaskRequestBodyBytes(r.task, &body, &bodyLen) {
if body != nil && bodyLen > 0 {
r.body = io.NopCloser(bytes.NewReader(C.GoBytes(body, bodyLen)))
} else {
r.body = http.NoBody
}
} else if C.URLSchemeTaskRequestBodyStreamOpen(r.task) {
r.body = &requestBodyStreamReader{task: r.task}
}
return r.body, nil
}
func (r *request) Response() ResponseWriter {
if r.rw != nil {
return r.rw
}
r.rw = &responseWriter{r: r}
return r.rw
}
func (r *request) Close() error {
var err error
if r.body != nil {
err = r.body.Close()
}
r.Response().Finish()
C.URLSchemeTaskRelease(r.task)
return err
}
var _ io.ReadCloser = &requestBodyStreamReader{}
type requestBodyStreamReader struct {
task unsafe.Pointer
closed bool
}
// Read implements io.Reader
func (r *requestBodyStreamReader) Read(p []byte) (n int, err error) {
var content unsafe.Pointer
var contentLen int
if p != nil {
content = unsafe.Pointer(&p[0])
contentLen = len(p)
}
res := C.URLSchemeTaskRequestBodyStreamRead(r.task, content, C.int(contentLen))
if res > 0 {
return int(res), nil
}
switch res {
case 0:
return 0, io.EOF
case -1:
return 0, errors.New("body: stream error")
case -2:
return 0, errors.New("body: no stream defined")
case -3:
return 0, io.ErrClosedPipe
default:
return 0, fmt.Errorf("body: unknown error %d", res)
}
}
func (r *requestBodyStreamReader) Close() error {
if r.closed {
return nil
}
r.closed = true
C.URLSchemeTaskRequestBodyStreamClose(r.task)
return nil
}

View File

@@ -0,0 +1,106 @@
//go:build linux && cgo && !gtk3 && !android
package webview
/*
#cgo linux pkg-config: gtk4 webkitgtk-6.0 gio-unix-2.0
#include <gtk/gtk.h>
#include <webkit/webkit.h>
static gboolean unref_request_on_main(gpointer data) {
if (data != NULL) {
g_object_unref(data);
}
return G_SOURCE_REMOVE;
}
// releaseRequestOnMainThread schedules the WebKitURISchemeRequest unref on the
// GTK main context. Close() runs on the assetserver goroutine, and dropping
// what may be the last reference finalizes a WebKit GObject — only safe on the
// UI thread (see #5557).
static void releaseRequestOnMainThread(WebKitURISchemeRequest *request) {
if (request == NULL) {
return;
}
g_main_context_invoke(NULL, unref_request_on_main, request);
}
*/
import "C"
import (
"io"
"net/http"
"unsafe"
)
func NewRequest(webKitURISchemeRequest unsafe.Pointer) Request {
webkitReq := (*C.WebKitURISchemeRequest)(webKitURISchemeRequest)
C.g_object_ref(C.gpointer(webkitReq))
req := &request{req: webkitReq}
return newRequestFinalizer(req)
}
var _ Request = &request{}
type request struct {
req *C.WebKitURISchemeRequest
header http.Header
body io.ReadCloser
rw *responseWriter
}
func (r *request) URL() (string, error) {
// Reading the URI touches the WebKit-owned request on the GTK main loop;
// this runs on a worker goroutine, so it must hop to the main thread.
// See mainthread_linux.go and issue #5631.
var uri string
invokeOnMainSync(func() {
uri = C.GoString(C.webkit_uri_scheme_request_get_uri(r.req))
})
return uri, nil
}
func (r *request) Method() (string, error) {
return webkit_uri_scheme_request_get_http_method(r.req), nil
}
func (r *request) Header() (http.Header, error) {
if r.header != nil {
return r.header, nil
}
r.header = webkit_uri_scheme_request_get_http_headers(r.req)
return r.header, nil
}
func (r *request) Body() (io.ReadCloser, error) {
if r.body != nil {
return r.body, nil
}
r.body = webkit_uri_scheme_request_get_http_body(r.req)
return r.body, nil
}
func (r *request) Response() ResponseWriter {
if r.rw != nil {
return r.rw
}
r.rw = &responseWriter{req: r.req}
return r.rw
}
func (r *request) Close() error {
var err error
if r.body != nil {
err = r.body.Close()
}
r.Response().Finish()
C.releaseRequestOnMainThread(r.req)
return err
}

View File

@@ -0,0 +1,107 @@
//go:build linux && cgo && gtk3 && !android
package webview
/*
#cgo linux pkg-config: gtk+-3.0 webkit2gtk-4.1 gio-unix-2.0
#include "gtk/gtk.h"
#include "webkit2/webkit2.h"
static gboolean unref_request_on_main(gpointer data) {
if (data != NULL) {
g_object_unref(data);
}
return G_SOURCE_REMOVE;
}
// releaseRequestOnMainThread schedules the WebKitURISchemeRequest unref on the
// GTK main context. Close() runs on the assetserver goroutine, and dropping
// what may be the last reference finalizes a WebKit GObject — only safe on the
// UI thread (see #5557).
static void releaseRequestOnMainThread(WebKitURISchemeRequest *request) {
if (request == NULL) {
return;
}
g_main_context_invoke(NULL, unref_request_on_main, request);
}
*/
import "C"
import (
"io"
"net/http"
"unsafe"
)
// NewRequest creates as new WebViewRequest based on a pointer to an `WebKitURISchemeRequest`
func NewRequest(webKitURISchemeRequest unsafe.Pointer) Request {
webkitReq := (*C.WebKitURISchemeRequest)(webKitURISchemeRequest)
C.g_object_ref(C.gpointer(webkitReq))
req := &request{req: webkitReq}
return newRequestFinalizer(req)
}
var _ Request = &request{}
type request struct {
req *C.WebKitURISchemeRequest
header http.Header
body io.ReadCloser
rw *responseWriter
}
func (r *request) URL() (string, error) {
// Reading the URI touches the WebKit-owned request on the GTK main loop;
// this runs on a worker goroutine, so it must hop to the main thread.
// See mainthread_linux.go and issue #5631.
var uri string
invokeOnMainSync(func() {
uri = C.GoString(C.webkit_uri_scheme_request_get_uri(r.req))
})
return uri, nil
}
func (r *request) Method() (string, error) {
return webkit_uri_scheme_request_get_http_method(r.req), nil
}
func (r *request) Header() (http.Header, error) {
if r.header != nil {
return r.header, nil
}
r.header = webkit_uri_scheme_request_get_http_headers(r.req)
return r.header, nil
}
func (r *request) Body() (io.ReadCloser, error) {
if r.body != nil {
return r.body, nil
}
r.body = webkit_uri_scheme_request_get_http_body(r.req)
return r.body, nil
}
func (r *request) Response() ResponseWriter {
if r.rw != nil {
return r.rw
}
r.rw = &responseWriter{req: r.req}
return r.rw
}
func (r *request) Close() error {
var err error
if r.body != nil {
err = r.body.Close()
}
r.Response().Finish()
C.releaseRequestOnMainThread(r.req)
return err
}

View File

@@ -0,0 +1,218 @@
//go:build windows
package webview
import (
"errors"
"fmt"
"io"
"net/http"
"github.com/wailsapp/wails/v3/internal/webview2/pkg/edge"
)
// NewRequest creates as new WebViewRequest for chromium. This Method must be called from the Main-Thread!
func NewRequest(env *edge.ICoreWebView2Environment, args *edge.ICoreWebView2WebResourceRequestedEventArgs, invokeSync func(fn func())) (Request, error) {
req, err := args.GetRequest()
if err != nil {
return nil, fmt.Errorf("GetRequest failed: %s", err)
}
defer req.Release()
r := &request{
invokeSync: invokeSync,
}
code := http.StatusInternalServerError
r.response, err = env.CreateWebResourceResponse(nil, code, http.StatusText(code), "")
if err != nil {
return nil, fmt.Errorf("CreateWebResourceResponse failed: %s", err)
}
if err := args.PutResponse(r.response); err != nil {
r.finishResponse()
return nil, fmt.Errorf("PutResponse failed: %s", err)
}
r.deferral, err = args.GetDeferral()
if err != nil {
r.finishResponse()
return nil, fmt.Errorf("GetDeferral failed: %s", err)
}
r.url, r.urlErr = req.GetUri()
r.method, r.methodErr = req.GetMethod()
r.header, r.headerErr = getHeaders(req)
if content, err := req.GetContent(); err != nil {
r.bodyErr = err
} else if content != nil {
// It is safe to access Content from another Thread: https://learn.microsoft.com/en-us/microsoft-edge/webview2/concepts/threading-model#thread-safety
r.body = &iStreamReleaseCloser{stream: content}
}
return r, nil
}
var _ Request = &request{}
type request struct {
response *edge.ICoreWebView2WebResourceResponse
deferral *edge.ICoreWebView2Deferral
url string
urlErr error
method string
methodErr error
header http.Header
headerErr error
body io.ReadCloser
bodyErr error
rw *responseWriter
invokeSync func(fn func())
}
func (r *request) URL() (string, error) {
return r.url, r.urlErr
}
func (r *request) Method() (string, error) {
return r.method, r.methodErr
}
func (r *request) Header() (http.Header, error) {
return r.header, r.headerErr
}
func (r *request) Body() (io.ReadCloser, error) {
return r.body, r.bodyErr
}
func (r *request) Response() ResponseWriter {
if r.rw != nil {
return r.rw
}
r.rw = &responseWriter{req: r}
return r.rw
}
func (r *request) Close() error {
var errs []error
if r.body != nil {
if err := r.body.Close(); err != nil {
errs = append(errs, err)
}
r.body = nil
}
if err := r.Response().Finish(); err != nil {
errs = append(errs, err)
}
return combineErrs(errs)
}
// finishResponse must be called on the main-thread
func (r *request) finishResponse() error {
var errs []error
if r.response != nil {
if err := r.response.Release(); err != nil {
errs = append(errs, err)
}
r.response = nil
}
if r.deferral != nil {
if err := r.deferral.Complete(); err != nil {
errs = append(errs, err)
}
if err := r.deferral.Release(); err != nil {
errs = append(errs, err)
}
r.deferral = nil
}
return combineErrs(errs)
}
type iStreamReleaseCloser struct {
stream *edge.IStream
closed bool
}
func (i *iStreamReleaseCloser) Read(p []byte) (int, error) {
if i.closed {
return 0, io.ErrClosedPipe
}
return i.stream.Read(p)
}
func (i *iStreamReleaseCloser) Close() error {
if i.closed {
return nil
}
i.closed = true
return i.stream.Release()
}
func getHeaders(req *edge.ICoreWebView2WebResourceRequest) (http.Header, error) {
header := http.Header{}
headers, err := req.GetHeaders()
if err != nil {
return nil, fmt.Errorf("GetHeaders Error: %s", err)
}
defer headers.Release()
headersIt, err := headers.GetIterator()
if err != nil {
return nil, fmt.Errorf("GetIterator Error: %s", err)
}
defer headersIt.Release()
for {
has, err := headersIt.HasCurrentHeader()
if err != nil {
return nil, fmt.Errorf("HasCurrentHeader Error: %s", err)
}
if !has {
break
}
name, value, err := headersIt.GetCurrentHeader()
if err != nil {
return nil, fmt.Errorf("GetCurrentHeader Error: %s", err)
}
header.Set(name, value)
if _, err := headersIt.MoveNext(); err != nil {
return nil, fmt.Errorf("MoveNext Error: %s", err)
}
}
// WebView2 has problems when a request returns a 304 status code and the WebView2 is going to hang for other
// requests including IPC calls.
// So prevent 304 status codes by removing the headers that are used in combinationwith caching.
header.Del("If-Modified-Since")
header.Del("If-None-Match")
return header, nil
}
func combineErrs(errs []error) error {
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]
}
}
return err
}

View File

@@ -0,0 +1,28 @@
package webview
import (
"errors"
"net/http"
)
const (
HeaderContentLength = "Content-Length"
HeaderContentType = "Content-Type"
)
var (
errRequestStopped = errors.New("request has been stopped")
errResponseFinished = errors.New("response has been finished")
)
// A ResponseWriter interface is used by an HTTP handler to
// construct an HTTP response for the WebView.
type ResponseWriter interface {
http.ResponseWriter
// Finish the response and flush all data. A Finish after the request has already been finished has no effect.
Finish() error
// Code returns the HTTP status code of the response
Code() int
}

View File

@@ -0,0 +1,156 @@
//go:build darwin && !ios
package webview
/*
#cgo CFLAGS: -x objective-c
#cgo LDFLAGS: -framework Foundation -framework WebKit
#import <Foundation/Foundation.h>
#import <WebKit/WebKit.h>
typedef void (^schemeTaskCaller)(id<WKURLSchemeTask>);
static bool urlSchemeTaskCall(void *wkUrlSchemeTask, schemeTaskCaller fn) {
id<WKURLSchemeTask> urlSchemeTask = (id<WKURLSchemeTask>) wkUrlSchemeTask;
if (urlSchemeTask == nil) {
return false;
}
@autoreleasepool {
@try {
fn(urlSchemeTask);
} @catch (NSException *exception) {
// This is very bad to detect a stopped schemeTask this should be implemented in a better way
// But it seems to be very tricky to not deadlock when keeping a lock curing executing fn()
// It seems like those call switch the thread back to the main thread and then deadlocks when they reentrant want
// to get the lock again to start another request or stop it.
if ([exception.reason isEqualToString: @"This task has already been stopped"]) {
return false;
}
@throw exception;
}
return true;
}
}
static bool URLSchemeTaskDidReceiveData(void *wkUrlSchemeTask, void* data, int datalength) {
return urlSchemeTaskCall(
wkUrlSchemeTask,
^(id<WKURLSchemeTask> urlSchemeTask) {
NSData *nsdata = [NSData dataWithBytes:data length:datalength];
[urlSchemeTask didReceiveData:nsdata];
});
}
static bool URLSchemeTaskDidFinish(void *wkUrlSchemeTask) {
return urlSchemeTaskCall(
wkUrlSchemeTask,
^(id<WKURLSchemeTask> urlSchemeTask) {
[urlSchemeTask didFinish];
});
}
static bool URLSchemeTaskDidReceiveResponse(void *wkUrlSchemeTask, int statusCode, void *headersString, int headersStringLength) {
return urlSchemeTaskCall(
wkUrlSchemeTask,
^(id<WKURLSchemeTask> urlSchemeTask) {
NSData *nsHeadersJSON = [NSData dataWithBytes:headersString length:headersStringLength];
NSDictionary *headerFields = [NSJSONSerialization JSONObjectWithData:nsHeadersJSON options: NSJSONReadingMutableContainers error: nil];
NSHTTPURLResponse *response = [[[NSHTTPURLResponse alloc] initWithURL:urlSchemeTask.request.URL statusCode:statusCode HTTPVersion:@"HTTP/1.1" headerFields:headerFields] autorelease];
[urlSchemeTask didReceiveResponse:response];
});
}
*/
import "C"
import (
"net/http"
"unsafe"
"encoding/json"
)
var _ ResponseWriter = &responseWriter{}
type responseWriter struct {
r *request
header http.Header
wroteHeader bool
code int
finished bool
}
func (rw *responseWriter) Header() http.Header {
if rw.header == nil {
rw.header = http.Header{}
}
return rw.header
}
func (rw *responseWriter) Write(buf []byte) (int, error) {
if rw.finished {
return 0, errResponseFinished
}
rw.WriteHeader(http.StatusOK)
var content unsafe.Pointer
var contentLen int
if buf != nil && len(buf) > 0 {
content = unsafe.Pointer(&buf[0])
contentLen = len(buf)
}
if !C.URLSchemeTaskDidReceiveData(rw.r.task, content, C.int(contentLen)) {
return 0, errRequestStopped
}
return contentLen, nil
}
func (rw *responseWriter) WriteHeader(code int) {
rw.code = code
if rw.wroteHeader || rw.finished {
return
}
rw.wroteHeader = true
header := map[string]string{}
for k := range rw.Header() {
header[k] = rw.Header().Get(k)
}
headerData, _ := json.Marshal(header)
var headers unsafe.Pointer
var headersLen int
if len(headerData) != 0 {
headers = unsafe.Pointer(&headerData[0])
headersLen = len(headerData)
}
C.URLSchemeTaskDidReceiveResponse(rw.r.task, C.int(code), headers, C.int(headersLen))
}
func (rw *responseWriter) Finish() error {
if !rw.wroteHeader {
rw.WriteHeader(http.StatusNotImplemented)
}
if rw.finished {
return nil
}
rw.finished = true
C.URLSchemeTaskDidFinish(rw.r.task)
return nil
}
func (rw *responseWriter) Code() int {
return rw.code
}

View File

@@ -0,0 +1,161 @@
//go:build ios
package webview
/*
#cgo CFLAGS: -x objective-c -fobjc-arc
#cgo LDFLAGS: -framework Foundation -framework WebKit
#include <stdlib.h>
#import <Foundation/Foundation.h>
#import <WebKit/WebKit.h>
typedef void (^schemeTaskCaller)(id<WKURLSchemeTask>);
static bool urlSchemeTaskCall(void *wkUrlSchemeTask, schemeTaskCaller fn) {
id<WKURLSchemeTask> urlSchemeTask = (__bridge id<WKURLSchemeTask>) wkUrlSchemeTask;
if (urlSchemeTask == nil) {
return false;
}
@autoreleasepool {
@try {
fn(urlSchemeTask);
} @catch (NSException *exception) {
// This is very bad to detect a stopped schemeTask this should be implemented in a better way
// But it seems to be very tricky to not deadlock when keeping a lock curing executing fn()
// It seems like those call switch the thread back to the main thread and then deadlocks when they reentrant want
// to get the lock again to start another request or stop it.
if ([exception.reason isEqualToString: @"This task has already been stopped"]) {
return false;
}
@throw exception;
}
return true;
}
}
static bool URLSchemeTaskDidReceiveData(void *wkUrlSchemeTask, void* data, int datalength) {
return urlSchemeTaskCall(
wkUrlSchemeTask,
^(id<WKURLSchemeTask> urlSchemeTask) {
NSData *nsdata = [NSData dataWithBytes:data length:datalength];
[urlSchemeTask didReceiveData:nsdata];
});
}
static bool URLSchemeTaskDidFinish(void *wkUrlSchemeTask) {
return urlSchemeTaskCall(
wkUrlSchemeTask,
^(id<WKURLSchemeTask> urlSchemeTask) {
[urlSchemeTask didFinish];
});
}
static bool URLSchemeTaskDidReceiveResponse(void *wkUrlSchemeTask, int statusCode, void *headersString, int headersStringLength) {
return urlSchemeTaskCall(
wkUrlSchemeTask,
^(id<WKURLSchemeTask> urlSchemeTask) {
NSData *nsHeadersJSON = [NSData dataWithBytes:headersString length:headersStringLength];
NSDictionary *headerFields = [NSJSONSerialization JSONObjectWithData:nsHeadersJSON options:NSJSONReadingMutableContainers error:nil];
NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:urlSchemeTask.request.URL statusCode:statusCode HTTPVersion:nil headerFields:headerFields];
[urlSchemeTask didReceiveResponse:response];
});
}
*/
import "C"
import (
"net/http"
"unsafe"
"encoding/json"
)
var _ ResponseWriter = &responseWriter{}
type responseWriter struct {
r *request
header http.Header
wroteHeader bool
code int
finished bool
}
func (rw *responseWriter) Header() http.Header {
if rw.header == nil {
rw.header = http.Header{}
}
return rw.header
}
func (rw *responseWriter) Write(buf []byte) (int, error) {
if rw.finished {
return 0, errResponseFinished
}
rw.WriteHeader(http.StatusOK)
var content unsafe.Pointer
var contentLen int
// Guard on length, not just nil: a non-nil but empty slice (e.g. the body of
// a bound method that returned "") would make &buf[0] panic with an
// index-out-of-range, aborting the Go runtime. An empty body is valid — pass
// a nil pointer with length 0, which yields an empty NSData on the C side.
if len(buf) != 0 {
content = unsafe.Pointer(&buf[0])
contentLen = len(buf)
}
if !C.URLSchemeTaskDidReceiveData(rw.r.task, content, C.int(contentLen)) {
return 0, errRequestStopped
}
return contentLen, nil
}
func (rw *responseWriter) WriteHeader(code int) {
rw.code = code
if rw.wroteHeader || rw.finished {
return
}
rw.wroteHeader = true
header := map[string]string{}
for k := range rw.Header() {
header[k] = rw.Header().Get(k)
}
headerData, _ := json.Marshal(header)
var headers unsafe.Pointer
var headersLen int
if len(headerData) != 0 {
headers = unsafe.Pointer(&headerData[0])
headersLen = len(headerData)
}
C.URLSchemeTaskDidReceiveResponse(rw.r.task, C.int(code), headers, C.int(headersLen))
}
func (rw *responseWriter) Finish() error {
if !rw.wroteHeader {
rw.WriteHeader(http.StatusNotImplemented)
}
if rw.finished {
return nil
}
rw.finished = true
C.URLSchemeTaskDidFinish(rw.r.task)
return nil
}
func (rw *responseWriter) Code() int {
return rw.code
}

View File

@@ -0,0 +1,146 @@
//go:build linux && cgo && !gtk3 && !android
package webview
/*
#cgo linux pkg-config: gtk4 webkitgtk-6.0
#include <gtk/gtk.h>
#include <webkit/webkit.h>
// webview_asset_error_quark returns a stable GError domain for asset-server
// failures. The string literal is static storage, so g_quark_from_static_string
// interns it once and never leaks. Interning the per-request error message
// instead (the previous behaviour) grew the global quark table unboundedly on
// long-running apps, since GQuarks are never freed.
static GQuark webview_asset_error_quark(void) {
return g_quark_from_static_string("wails-webview-assetserver");
}
*/
import "C"
import (
"fmt"
"io"
"net/http"
"os"
"strconv"
"syscall"
"unsafe"
)
type responseWriter struct {
req *C.WebKitURISchemeRequest
header http.Header
wroteHeader bool
finished bool
code int
w io.WriteCloser
wErr error
}
func (rw *responseWriter) Code() int {
return rw.code
}
func (rw *responseWriter) Header() http.Header {
if rw.header == nil {
rw.header = http.Header{}
}
return rw.header
}
func (rw *responseWriter) Write(buf []byte) (int, error) {
if rw.finished {
return 0, errResponseFinished
}
rw.WriteHeader(http.StatusOK)
if rw.wErr != nil {
return 0, rw.wErr
}
return rw.w.Write(buf)
}
func (rw *responseWriter) WriteHeader(code int) {
rw.code = code
if rw.wroteHeader || rw.finished {
return
}
rw.wroteHeader = true
contentLength := int64(-1)
if sLen := rw.Header().Get(HeaderContentLength); sLen != "" {
if pLen, _ := strconv.ParseInt(sLen, 10, 64); pLen > 0 {
contentLength = pLen
}
}
rFD, w, err := pipe()
if err != nil {
rw.finishWithError(http.StatusInternalServerError, fmt.Errorf("unable to open pipe: %s", err))
return
}
rw.w = w
// webkit_uri_scheme_request_finish wraps the read end of the pipe in a
// GUnixInputStream and completes the request on the GTK main thread; the
// stream is created and released there too. See webkit_linux.go and #5631.
if err := webkit_uri_scheme_request_finish(rw.req, code, rw.Header(), rFD, contentLength); err != nil {
rw.finishWithError(http.StatusInternalServerError, fmt.Errorf("unable to finish request: %s", err))
return
}
}
func (rw *responseWriter) Finish() error {
if !rw.wroteHeader {
rw.WriteHeader(http.StatusNotImplemented)
}
if rw.finished {
return nil
}
rw.finished = true
if rw.w != nil {
rw.w.Close()
}
return nil
}
func (rw *responseWriter) finishWithError(code int, err error) {
if rw.w != nil {
rw.w.Close()
rw.w = &nopCloser{io.Discard}
}
rw.wErr = err
msg := C.CString(err.Error())
defer C.free(unsafe.Pointer(msg))
// webkit_uri_scheme_request_finish_error touches the WebKit-owned request
// on the GTK main loop; this runs on an asset-server worker goroutine, so
// it must hop to the main thread. See mainthread_linux.go and issue #5631.
invokeOnMainSync(func() {
gerr := C.g_error_new_literal(C.webview_asset_error_quark(), C.int(code), msg)
C.webkit_uri_scheme_request_finish_error(rw.req, gerr)
C.g_error_free(gerr)
})
}
type nopCloser struct {
io.Writer
}
func (nopCloser) Close() error { return nil }
func pipe() (r int, w *os.File, err error) {
var p [2]int
e := syscall.Pipe2(p[0:], 0)
if e != nil {
return 0, nil, fmt.Errorf("pipe2: %s", e)
}
return p[0], os.NewFile(uintptr(p[1]), "|1"), nil
}

View File

@@ -0,0 +1,149 @@
//go:build linux && cgo && gtk3 && !android
package webview
/*
#cgo linux pkg-config: gtk+-3.0 webkit2gtk-4.1
#include "gtk/gtk.h"
#include "webkit2/webkit2.h"
// webview_asset_error_quark returns a stable GError domain for asset-server
// failures. The string literal is static storage, so g_quark_from_static_string
// interns it once and never leaks. Interning the per-request error message
// instead (the previous behaviour) grew the global quark table unboundedly on
// long-running apps, since GQuarks are never freed.
static GQuark webview_asset_error_quark(void) {
return g_quark_from_static_string("wails-webview-assetserver");
}
*/
import "C"
import (
"fmt"
"io"
"net/http"
"os"
"strconv"
"syscall"
"unsafe"
)
type responseWriter struct {
req *C.WebKitURISchemeRequest
header http.Header
wroteHeader bool
finished bool
code int
w io.WriteCloser
wErr error
}
func (rw *responseWriter) Code() int {
return rw.code
}
func (rw *responseWriter) Header() http.Header {
if rw.header == nil {
rw.header = http.Header{}
}
return rw.header
}
func (rw *responseWriter) Write(buf []byte) (int, error) {
if rw.finished {
return 0, errResponseFinished
}
rw.WriteHeader(http.StatusOK)
if rw.wErr != nil {
return 0, rw.wErr
}
return rw.w.Write(buf)
}
func (rw *responseWriter) WriteHeader(code int) {
rw.code = code
if rw.wroteHeader || rw.finished {
return
}
rw.wroteHeader = true
contentLength := int64(-1)
if sLen := rw.Header().Get(HeaderContentLength); sLen != "" {
if pLen, _ := strconv.ParseInt(sLen, 10, 64); pLen > 0 {
contentLength = pLen
}
}
// We can't use os.Pipe here, because that returns files with a finalizer for closing the FD. But the control over the
// read FD is given to the InputStream and will be closed there.
// Furthermore we especially don't want to have the FD_CLOEXEC
rFD, w, err := pipe()
if err != nil {
rw.finishWithError(http.StatusInternalServerError, fmt.Errorf("unable to open pipe: %s", err))
return
}
rw.w = w
// webkit_uri_scheme_request_finish wraps the read end of the pipe in a
// GUnixInputStream and completes the request on the GTK main thread; the
// stream is created and released there too. See webkit_linux_gtk3.go and #5631.
if err := webkit_uri_scheme_request_finish(rw.req, code, rw.Header(), rFD, contentLength); err != nil {
rw.finishWithError(http.StatusInternalServerError, fmt.Errorf("unable to finish request: %s", err))
return
}
}
func (rw *responseWriter) Finish() error {
if !rw.wroteHeader {
rw.WriteHeader(http.StatusNotImplemented)
}
if rw.finished {
return nil
}
rw.finished = true
if rw.w != nil {
rw.w.Close()
}
return nil
}
func (rw *responseWriter) finishWithError(code int, err error) {
if rw.w != nil {
rw.w.Close()
rw.w = &nopCloser{io.Discard}
}
rw.wErr = err
msg := C.CString(err.Error())
defer C.free(unsafe.Pointer(msg))
// webkit_uri_scheme_request_finish_error touches the WebKit-owned request
// on the GTK main loop; this runs on an asset-server worker goroutine, so
// it must hop to the main thread. See mainthread_linux.go and issue #5631.
invokeOnMainSync(func() {
gerr := C.g_error_new_literal(C.webview_asset_error_quark(), C.int(code), msg)
C.webkit_uri_scheme_request_finish_error(rw.req, gerr)
C.g_error_free(gerr)
})
}
type nopCloser struct {
io.Writer
}
func (nopCloser) Close() error { return nil }
func pipe() (r int, w *os.File, err error) {
var p [2]int
e := syscall.Pipe2(p[0:], 0)
if e != nil {
return 0, nil, fmt.Errorf("pipe2: %s", e)
}
return p[0], os.NewFile(uintptr(p[1]), "|1"), nil
}

View File

@@ -0,0 +1,109 @@
//go:build windows
package webview
import (
"bytes"
"errors"
"fmt"
"net/http"
"strings"
)
var _ http.ResponseWriter = &responseWriter{}
type responseWriter struct {
req *request
header http.Header
wroteHeader bool
code int
body *bytes.Buffer
finished bool
}
func (rw *responseWriter) Header() http.Header {
if rw.header == nil {
rw.header = http.Header{}
}
return rw.header
}
func (rw *responseWriter) Write(buf []byte) (int, error) {
if rw.finished {
return 0, errResponseFinished
}
rw.WriteHeader(http.StatusOK)
return rw.body.Write(buf)
}
func (rw *responseWriter) WriteHeader(code int) {
if rw.wroteHeader || rw.finished {
return
}
rw.wroteHeader = true
if rw.body == nil {
rw.body = &bytes.Buffer{}
}
rw.code = code
}
func (rw *responseWriter) Finish() error {
if !rw.wroteHeader {
rw.WriteHeader(http.StatusNotImplemented)
}
if rw.finished {
return nil
}
rw.finished = true
var errs []error
code := rw.code
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, errors.New("AssetServer returned 304 - StatusNotModified which are going to hang WebView2, changed code to 505 - StatusInternalServerError"))
code = http.StatusInternalServerError
}
rw.req.invokeSync(func() {
resp := rw.req.response
hdrs, err := resp.GetHeaders()
if err != nil {
errs = append(errs, fmt.Errorf("Resp.GetHeaders failed: %s", err))
} else {
for k, v := range rw.header {
if err := hdrs.AppendHeader(k, strings.Join(v, ",")); err != nil {
errs = append(errs, fmt.Errorf("Resp.AppendHeader failed: %s", err))
}
}
hdrs.Release()
}
if err := resp.PutStatusCode(code); err != nil {
errs = append(errs, fmt.Errorf("Resp.PutStatusCode failed: %s", err))
}
if err := resp.PutByteContent(rw.body.Bytes()); err != nil {
errs = append(errs, fmt.Errorf("Resp.PutByteContent failed: %s", err))
}
if err := rw.req.finishResponse(); err != nil {
errs = append(errs, fmt.Errorf("Resp.finishResponse failed: %s", err))
}
})
return combineErrs(errs)
}
func (rw *responseWriter) Code() int {
return rw.code
}

View File

@@ -0,0 +1,181 @@
//go:build linux && cgo && !gtk3 && !android
package webview
/*
#cgo linux pkg-config: gtk4 webkitgtk-6.0 libsoup-3.0 gio-unix-2.0
#include <gtk/gtk.h>
#include <webkit/webkit.h>
#include <libsoup/soup.h>
#include <gio/gunixinputstream.h>
*/
import "C"
import (
"fmt"
"io"
"net/http"
"strings"
"unsafe"
)
const Webkit2MinMinorVersion = 0
func webkit_uri_scheme_request_get_http_method(req *C.WebKitURISchemeRequest) string {
// Reading request metadata touches the WebKit-owned request object, which
// belongs to the GTK main loop; this runs on a worker goroutine, so it must
// hop to the main thread. See mainthread_linux.go and issue #5631.
var method string
invokeOnMainSync(func() {
method = C.GoString(C.webkit_uri_scheme_request_get_http_method(req))
})
return strings.ToUpper(method)
}
func webkit_uri_scheme_request_get_http_headers(req *C.WebKitURISchemeRequest) http.Header {
h := http.Header{}
// Reading and iterating the request's libsoup headers touches WebKit-owned
// state on the GTK main loop; this runs on a worker goroutine, so it must hop
// to the main thread. See mainthread_linux.go and issue #5631.
invokeOnMainSync(func() {
hdrs := C.webkit_uri_scheme_request_get_http_headers(req)
var iter C.SoupMessageHeadersIter
C.soup_message_headers_iter_init(&iter, hdrs)
var name *C.char
var value *C.char
for C.soup_message_headers_iter_next(&iter, &name, &value) != 0 {
h.Add(C.GoString(name), C.GoString(value))
}
})
return h
}
func webkit_uri_scheme_request_finish(req *C.WebKitURISchemeRequest, code int, header http.Header, rFD int, streamLength int64) error {
// Completing the request touches WebKit/libsoup objects owned by the GTK
// main loop, but this runs on an asset-server worker goroutine. WebKit2GTK
// is not thread-safe, so the whole sequence must hop to the main thread.
//
// The response input stream is created and unref'd inside the same hop: it is
// ref-taken by webkit_uri_scheme_response_new on the main thread, so creating
// and releasing our reference here too keeps every refcount operation on a
// single thread. Previously the stream was built and unref'd on the worker
// while WebKit took its ref on the main thread, splitting the stream's
// refcount across threads. See mainthread_linux.go and issue #5631.
invokeOnMainSync(func() {
stream := C.g_unix_input_stream_new(C.int(rFD), C.gboolean(1))
defer C.g_object_unref(C.gpointer(stream))
resp := C.webkit_uri_scheme_response_new(stream, C.gint64(streamLength))
defer C.g_object_unref(C.gpointer(resp))
cReason := C.CString(http.StatusText(code))
C.webkit_uri_scheme_response_set_status(resp, C.guint(code), cReason)
C.free(unsafe.Pointer(cReason))
cMimeType := C.CString(header.Get(HeaderContentType))
C.webkit_uri_scheme_response_set_content_type(resp, cMimeType)
C.free(unsafe.Pointer(cMimeType))
// Ownership of hdrs is transferred to the response by
// webkit_uri_scheme_response_set_http_headers (transfer full), so we must
// not unref it here — doing so frees the headers while WebKit/libsoup still
// reference them, crashing in soup_message_headers_iter_next on render.
hdrs := C.soup_message_headers_new(C.SOUP_MESSAGE_HEADERS_RESPONSE)
for name, values := range header {
cName := C.CString(name)
for _, value := range values {
cValue := C.CString(value)
C.soup_message_headers_append(hdrs, cName, cValue)
C.free(unsafe.Pointer(cValue))
}
C.free(unsafe.Pointer(cName))
}
C.webkit_uri_scheme_response_set_http_headers(resp, hdrs)
C.webkit_uri_scheme_request_finish_with_response(req, resp)
})
return nil
}
func webkit_uri_scheme_request_get_http_body(req *C.WebKitURISchemeRequest) io.ReadCloser {
// Fetching the request body stream touches the WebKit-owned request on the
// GTK main loop; this runs on a worker goroutine, so it must hop to the main
// thread. See mainthread_linux.go and issue #5631.
var stream *C.GInputStream
invokeOnMainSync(func() {
stream = C.webkit_uri_scheme_request_get_http_body(req)
})
if stream == nil {
return http.NoBody
}
return &webkitRequestBody{stream: stream}
}
type webkitRequestBody struct {
stream *C.GInputStream
closed bool
}
func (r *webkitRequestBody) Read(p []byte) (int, error) {
if r.closed {
return 0, io.ErrClosedPipe
}
// io.Reader allows a zero-length read; taking &p[0] on an empty slice would
// panic, so return early before touching the backing array.
if len(p) == 0 {
return 0, nil
}
content := unsafe.Pointer(&p[0])
contentLen := len(p)
var n C.gsize
var gErr *C.GError
var res C.gboolean
// Reading the WebKit-owned request body stream must happen on the GTK main
// loop thread; this runs on a worker goroutine. See issue #5631.
invokeOnMainSync(func() {
res = C.g_input_stream_read_all(r.stream, content, C.gsize(contentLen), &n, nil, &gErr)
})
if res == 0 {
return 0, formatGError("stream read failed", gErr)
} else if n == 0 {
return 0, io.EOF
}
return int(n), nil
}
func (r *webkitRequestBody) Close() error {
if r.closed {
return nil
}
r.closed = true
var err error
var gErr *C.GError
// Closing and unref-ing the WebKit-owned request body stream finalizes a
// GObject tied to the GTK main loop; this runs on a worker goroutine, so it
// must hop to the main thread. See issue #5631.
invokeOnMainSync(func() {
if C.g_input_stream_close(r.stream, nil, &gErr) == 0 {
err = formatGError("stream close failed", gErr)
}
C.g_object_unref(C.gpointer(r.stream))
})
r.stream = nil
return err
}
func formatGError(msg string, gErr *C.GError, args ...any) error {
if gErr != nil && gErr.message != nil {
msg += ": " + C.GoString(gErr.message)
C.g_error_free(gErr)
}
return fmt.Errorf(msg, args...)
}

View File

@@ -0,0 +1,185 @@
//go:build linux && cgo && gtk3 && !android
package webview
/*
#cgo linux pkg-config: gtk+-3.0 webkit2gtk-4.1 libsoup-3.0 gio-unix-2.0
#include "gtk/gtk.h"
#include "webkit2/webkit2.h"
#include "libsoup/soup.h"
#include "gio/gunixinputstream.h"
*/
import "C"
import (
"fmt"
"io"
"net/http"
"strings"
"unsafe"
)
const Webkit2MinMinorVersion = 40
func webkit_uri_scheme_request_get_http_method(req *C.WebKitURISchemeRequest) string {
// Reading request metadata touches the WebKit-owned request object, which
// belongs to the GTK main loop; this runs on a worker goroutine, so it must
// hop to the main thread. See mainthread_linux.go and issue #5631.
var method string
invokeOnMainSync(func() {
method = C.GoString(C.webkit_uri_scheme_request_get_http_method(req))
})
return strings.ToUpper(method)
}
func webkit_uri_scheme_request_get_http_headers(req *C.WebKitURISchemeRequest) http.Header {
h := http.Header{}
// Reading and iterating the request's libsoup headers touches WebKit-owned
// state on the GTK main loop; this runs on a worker goroutine, so it must hop
// to the main thread. See mainthread_linux.go and issue #5631.
invokeOnMainSync(func() {
hdrs := C.webkit_uri_scheme_request_get_http_headers(req)
var iter C.SoupMessageHeadersIter
C.soup_message_headers_iter_init(&iter, hdrs)
var name *C.char
var value *C.char
for C.soup_message_headers_iter_next(&iter, &name, &value) != 0 {
h.Add(C.GoString(name), C.GoString(value))
}
})
return h
}
func webkit_uri_scheme_request_finish(req *C.WebKitURISchemeRequest, code int, header http.Header, rFD int, streamLength int64) error {
// Completing the request touches WebKit/libsoup objects owned by the GTK
// main loop, but this runs on an asset-server worker goroutine. WebKit2GTK
// is not thread-safe, so the whole sequence must hop to the main thread.
//
// The response input stream is created and unref'd inside the same hop: it is
// ref-taken by webkit_uri_scheme_response_new on the main thread, so creating
// and releasing our reference here too keeps every refcount operation on a
// single thread. Previously the stream was built and unref'd on the worker
// while WebKit took its ref on the main thread, splitting the stream's
// refcount across threads. See mainthread_linux.go and issue #5631.
invokeOnMainSync(func() {
stream := C.g_unix_input_stream_new(C.int(rFD), C.gboolean(1))
defer C.g_object_unref(C.gpointer(stream))
resp := C.webkit_uri_scheme_response_new(stream, C.gint64(streamLength))
defer C.g_object_unref(C.gpointer(resp))
cReason := C.CString(http.StatusText(code))
C.webkit_uri_scheme_response_set_status(resp, C.guint(code), cReason)
C.free(unsafe.Pointer(cReason))
cMimeType := C.CString(header.Get(HeaderContentType))
C.webkit_uri_scheme_response_set_content_type(resp, cMimeType)
C.free(unsafe.Pointer(cMimeType))
// Ownership of hdrs is transferred to the response by
// webkit_uri_scheme_response_set_http_headers (transfer full), so we must
// not unref it here — doing so frees the headers while WebKit/libsoup still
// reference them, crashing in soup_message_headers_iter_next on render.
hdrs := C.soup_message_headers_new(C.SOUP_MESSAGE_HEADERS_RESPONSE)
for name, values := range header {
cName := C.CString(name)
for _, value := range values {
cValue := C.CString(value)
C.soup_message_headers_append(hdrs, cName, cValue)
C.free(unsafe.Pointer(cValue))
}
C.free(unsafe.Pointer(cName))
}
C.webkit_uri_scheme_response_set_http_headers(resp, hdrs)
C.webkit_uri_scheme_request_finish_with_response(req, resp)
})
return nil
}
func webkit_uri_scheme_request_get_http_body(req *C.WebKitURISchemeRequest) io.ReadCloser {
// Fetching the request body stream touches the WebKit-owned request on the
// GTK main loop; this runs on a worker goroutine, so it must hop to the main
// thread. See mainthread_linux.go and issue #5631.
var stream *C.GInputStream
invokeOnMainSync(func() {
stream = C.webkit_uri_scheme_request_get_http_body(req)
})
if stream == nil {
return http.NoBody
}
return &webkitRequestBody{stream: stream}
}
type webkitRequestBody struct {
stream *C.GInputStream
closed bool
}
// Read implements io.Reader
func (r *webkitRequestBody) Read(p []byte) (int, error) {
if r.closed {
return 0, io.ErrClosedPipe
}
// io.Reader allows a zero-length read; taking &p[0] on an empty slice would
// panic, so return early before touching the backing array.
if len(p) == 0 {
return 0, nil
}
content := unsafe.Pointer(&p[0])
contentLen := len(p)
var n C.gsize
var gErr *C.GError
var res C.gboolean
// Reading the WebKit-owned request body stream must happen on the GTK main
// loop thread; this runs on a worker goroutine. See issue #5631.
invokeOnMainSync(func() {
res = C.g_input_stream_read_all(r.stream, content, C.gsize(contentLen), &n, nil, &gErr)
})
if res == 0 {
return 0, formatGError("stream read failed", gErr)
} else if n == 0 {
return 0, io.EOF
}
return int(n), nil
}
func (r *webkitRequestBody) Close() error {
if r.closed {
return nil
}
r.closed = true
// https://docs.gtk.org/gio/method.InputStream.close.html
// Streams will be automatically closed when the last reference is dropped, but you might want to call this function
// to make sure resources are released as early as possible.
var err error
var gErr *C.GError
// Closing and unref-ing the WebKit-owned request body stream finalizes a
// GObject tied to the GTK main loop; this runs on a worker goroutine, so it
// must hop to the main thread. See issue #5631.
invokeOnMainSync(func() {
if C.g_input_stream_close(r.stream, nil, &gErr) == 0 {
err = formatGError("stream close failed", gErr)
}
C.g_object_unref(C.gpointer(r.stream))
})
r.stream = nil
return err
}
func formatGError(msg string, gErr *C.GError, args ...any) error {
if gErr != nil && gErr.message != nil {
msg += ": " + C.GoString(gErr.message)
C.g_error_free(gErr)
}
return fmt.Errorf(msg, args...)
}