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(`TestHi
`)
+ result := injectDevProxyScript(html)
+ s := string(result)
+ if !strings.Contains(s, `