diff --git a/pkg/miniapp/miniapp.go b/pkg/miniapp/miniapp.go index 3a3d41599..4f4c9538a 100644 --- a/pkg/miniapp/miniapp.go +++ b/pkg/miniapp/miniapp.go @@ -279,6 +279,23 @@ func (h *Handler) ActivateDevTarget(id string) error { } proxy := httputil.NewSingleHostReverseProxy(u) + proxy.ModifyResponse = func(resp *http.Response) error { + ct := resp.Header.Get("Content-Type") + if !strings.Contains(ct, "text/html") { + return nil + } + body, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + resp.Body.Close() + modified := injectDevProxyScript(body) + resp.Body = io.NopCloser(bytes.NewReader(modified)) + resp.ContentLength = int64(len(modified)) + resp.Header.Set("Content-Length", strconv.Itoa(len(modified))) + resp.Header.Del("Content-Encoding") + return nil + } proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) { w.Header().Set("Content-Type", "text/html; charset=utf-8") w.WriteHeader(http.StatusBadGateway) @@ -339,6 +356,66 @@ func (h *Handler) ListDevTargets() []DevTarget { return targets } +// devProxyScript is the JavaScript injected into HTML responses from the dev proxy. +// It rewrites fetch() and XMLHttpRequest.open() so that absolute paths like +// "/api/items" are prefixed with "/miniapp/dev", matching the reverse proxy mount. +const devProxyScript = `` + +// injectDevProxyScript inserts the dev proxy rewrite script into an HTML document. +// Insertion priority: before , after , or prepend to document. +func injectDevProxyScript(html []byte) []byte { + script := []byte(devProxyScript) + + // Priority 1: before + if idx := bytes.Index(bytes.ToLower(html), []byte("")); idx >= 0 { + out := make([]byte, 0, len(html)+len(script)) + out = append(out, html[:idx]...) + out = append(out, script...) + out = append(out, html[idx:]...) + return out + } + + // Priority 2: after + lower := bytes.ToLower(html) + if idx := bytes.Index(lower, []byte("= 0 { + // Find the closing '>' of the tag + closeIdx := bytes.IndexByte(lower[idx:], '>') + if closeIdx >= 0 { + insertAt := idx + closeIdx + 1 + out := make([]byte, 0, len(html)+len(script)) + out = append(out, html[:insertAt]...) + out = append(out, script...) + out = append(out, html[insertAt:]...) + return out + } + } + + // Priority 3: prepend + out := make([]byte, 0, len(html)+len(script)) + out = append(out, script...) + out = append(out, html...) + return out +} + // escapeHTMLString escapes HTML special characters in a string. func escapeHTMLString(s string) string { s = strings.ReplaceAll(s, "&", "&") diff --git a/pkg/miniapp/miniapp_test.go b/pkg/miniapp/miniapp_test.go index 4108a647b..3f962c4fb 100644 --- a/pkg/miniapp/miniapp_test.go +++ b/pkg/miniapp/miniapp_test.go @@ -13,6 +13,7 @@ import ( "net/url" "runtime" "sort" + "strconv" "strings" "sync/atomic" "testing" @@ -1813,6 +1814,202 @@ func TestDevProxy_HostHeaderForwarded(t *testing.T) { } } +// ── injectDevProxyScript tests ── + +func TestInjectDevProxyScript_HeadTag(t *testing.T) { + html := []byte(`Test

Hi

`) + result := injectDevProxyScript(html) + s := string(result) + if !strings.Contains(s, `