feat(goja): implement fetch API and fix websocket concurrency

fix(goja): handle ignored error returns in fetch API

docs: add fetch API type definitions to global-types
This commit is contained in:
dragon
2026-01-29 14:01:16 +08:00
parent a60f123bee
commit c630b7e34f
5 changed files with 164 additions and 0 deletions

View File

@@ -84,6 +84,9 @@ func FilterInstGoJa(gl core.Params) (core.FilterInstance, error) {
if err = EventInject(ctx, vm, jsLoop); err != nil {
return err
}
if err = FetchInject(vm, jsLoop); err != nil {
return err
}
if global.EnableWebsocket {
var closer io.Closer
closer, err = WebsocketInject(ctx, vm, debug, request, jsLoop)

View File

@@ -0,0 +1,99 @@
package goja
import (
"encoding/json"
"io"
"net/http"
"strings"
"github.com/dop251/goja"
"github.com/dop251/goja_nodejs/eventloop"
)
func FetchInject(jsCtx *goja.Runtime, loop *eventloop.EventLoop) error {
return jsCtx.GlobalObject().Set("fetch", func(url string, options ...map[string]interface{}) *goja.Promise {
promise, resolve, reject := jsCtx.NewPromise()
go func() {
method := "GET"
var body io.Reader
headers := make(http.Header)
if len(options) > 0 {
opts := options[0]
if m, ok := opts["method"].(string); ok {
method = strings.ToUpper(m)
}
if h, ok := opts["headers"].(map[string]interface{}); ok {
for k, v := range h {
if strVal, ok := v.(string); ok {
headers.Set(k, strVal)
}
}
}
if b, ok := opts["body"].(string); ok {
body = strings.NewReader(b)
}
}
req, err := http.NewRequest(method, url, body)
if err != nil {
loop.RunOnLoop(func(*goja.Runtime) {
_ = reject(err)
})
return
}
req.Header = headers
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
loop.RunOnLoop(func(*goja.Runtime) {
_ = reject(err)
})
return
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
loop.RunOnLoop(func(*goja.Runtime) {
_ = reject(err)
})
return
}
headersMap := make(map[string]interface{})
for k, v := range resp.Header {
headersMap[k] = v
}
loop.RunOnLoop(func(vm *goja.Runtime) {
responseObj := map[string]interface{}{
"ok": resp.StatusCode >= 200 && resp.StatusCode < 300,
"status": resp.StatusCode,
"statusText": resp.Status,
"headers": headersMap,
"text": func() *goja.Promise {
p, res, _ := vm.NewPromise()
_ = res(string(respBody))
return p
},
"json": func() *goja.Promise {
p, res, rej := vm.NewPromise()
var data interface{}
if err := json.Unmarshal(respBody, &data); err != nil {
_ = rej(err)
} else {
_ = res(data)
}
return p
},
}
_ = resolve(responseObj)
})
}()
return promise
})
}

View File

@@ -3,6 +3,7 @@ package goja
import (
"io"
"net/http"
"sync"
"time"
"github.com/dop251/goja"
@@ -21,6 +22,9 @@ func WebsocketInject(ctx core.FilterContext, jsCtx *goja.Runtime, w http.Respons
if err != nil {
return nil, err
}
var readMu sync.Mutex
var writeMu sync.Mutex
zap.L().Debug("websocket upgrader created")
closers.AddCloser(conn.Close)
go func() {
@@ -61,7 +65,9 @@ func WebsocketInject(ctx core.FilterContext, jsCtx *goja.Runtime, w http.Respons
})
}
}()
readMu.Lock()
_, p, err := conn.ReadMessage()
readMu.Unlock()
loop.RunOnLoop(func(runtime *goja.Runtime) {
if err != nil {
_ = reject(runtime.ToValue(err))
@@ -91,7 +97,9 @@ func WebsocketInject(ctx core.FilterContext, jsCtx *goja.Runtime, w http.Respons
})
}
}()
readMu.Lock()
messageType, p, err := conn.ReadMessage()
readMu.Unlock()
loop.RunOnLoop(func(runtime *goja.Runtime) {
if err != nil {
_ = reject(runtime.ToValue(err))
@@ -124,7 +132,9 @@ func WebsocketInject(ctx core.FilterContext, jsCtx *goja.Runtime, w http.Respons
})
}
}()
writeMu.Lock()
err := conn.WriteMessage(websocket.TextMessage, []byte(data))
writeMu.Unlock()
loop.RunOnLoop(func(runtime *goja.Runtime) {
if err != nil {
_ = reject(runtime.ToValue(err))
@@ -171,7 +181,9 @@ func WebsocketInject(ctx core.FilterContext, jsCtx *goja.Runtime, w http.Respons
return
}
writeMu.Lock()
err := conn.WriteMessage(mType, dataRaw)
writeMu.Unlock()
loop.RunOnLoop(func(runtime *goja.Runtime) {
if err != nil {
_ = reject(runtime.ToValue(err))