From d48013dd549a2f4a23c15466ccc04b8a61ef084a Mon Sep 17 00:00:00 2001 From: AgnesToulet <35176601+AgnesToulet@users.noreply.github.com> Date: Tue, 22 Jul 2025 14:47:53 +0200 Subject: [PATCH 01/15] Refactor: move route types and function to its own file --- internal/js/modules/k6/browser/common/http.go | 55 ----------------- .../js/modules/k6/browser/common/route.go | 59 +++++++++++++++++++ 2 files changed, 59 insertions(+), 55 deletions(-) create mode 100644 internal/js/modules/k6/browser/common/route.go diff --git a/internal/js/modules/k6/browser/common/http.go b/internal/js/modules/k6/browser/common/http.go index 4c3758b2e91..76d78ec68b5 100644 --- a/internal/js/modules/k6/browser/common/http.go +++ b/internal/js/modules/k6/browser/common/http.go @@ -708,58 +708,3 @@ func (r *Response) Text() (string, error) { func (r *Response) URL() string { return r.url } - -// Route allows to handle a request. -type Route struct { - logger *log.Logger - networkManager *NetworkManager - - request *Request - handled bool -} - -// NewRoute creates a new Route that allows to modify a request's behavior. -func NewRoute(logger *log.Logger, networkManager *NetworkManager, request *Request) *Route { - return &Route{ - logger: logger, - networkManager: networkManager, - request: request, - handled: false, - } -} - -func (r *Route) Request() *Request { return r.request } - -// Abort aborts the request with the given error code. -func (r *Route) Abort(errorCode string) { - err := r.startHandling() - if err != nil { - r.logger.Errorf("Route:Abort", "rurl:%s err:%s", r.request.URL(), err) - return - } - - if errorCode == "" { - errorCode = "failed" - } - - r.networkManager.AbortRequest(r.request.interceptionID, errorCode) -} - -// Continue continues the request. -func (r *Route) Continue() { - err := r.startHandling() - if err != nil { - r.logger.Errorf("Route:Continue", "rurl:%s err:%s", r.request.URL(), err) - return - } - - r.networkManager.ContinueRequest(r.request.interceptionID) -} - -func (r *Route) startHandling() error { - if r.handled { - return fmt.Errorf("route %s is already handled", r.request.URL()) - } - r.handled = true - return nil -} diff --git a/internal/js/modules/k6/browser/common/route.go b/internal/js/modules/k6/browser/common/route.go new file mode 100644 index 00000000000..f18ee5f1cde --- /dev/null +++ b/internal/js/modules/k6/browser/common/route.go @@ -0,0 +1,59 @@ +package common + +import ( + "fmt" + + "go.k6.io/k6/internal/js/modules/k6/browser/log" +) + +// Route allows to handle a request +type Route struct { + logger *log.Logger + networkManager *NetworkManager + + request *Request + handled bool +} + +func NewRoute(logger *log.Logger, networkManager *NetworkManager, request *Request) *Route { + return &Route{ + logger: logger, + networkManager: networkManager, + request: request, + handled: false, + } +} + +func (r *Route) Request() *Request { return r.request } + +func (r *Route) Abort(errorCode string) { + err := r.startHandling() + if err != nil { + r.logger.Errorf("Route:Abort", "rurl:%s err:%s", r.request.URL(), err) + return + } + + if errorCode == "" { + errorCode = "failed" + } + + r.networkManager.AbortRequest(r.request.interceptionID, errorCode) +} + +func (r *Route) Continue() { + err := r.startHandling() + if err != nil { + r.logger.Errorf("Route:Continue", "rurl:%s err:%s", r.request.URL(), err) + return + } + + r.networkManager.ContinueRequest(r.request.interceptionID) +} + +func (r *Route) startHandling() error { + if r.handled { + return fmt.Errorf("route is already handled") + } + r.handled = true + return nil +} From f033f0e5f1ae375d326125fe68b4fd407df5207f Mon Sep 17 00:00:00 2001 From: AgnesToulet <35176601+AgnesToulet@users.noreply.github.com> Date: Tue, 22 Jul 2025 15:00:21 +0200 Subject: [PATCH 02/15] Refactor: add route mapping and return errors --- .../k6/browser/browser/page_mapping.go | 3 +- .../k6/browser/browser/route_mapping.go | 20 +++++++++++++ .../k6/browser/common/frame_manager.go | 6 +++- .../k6/browser/common/network_manager.go | 28 +++++++++++-------- .../js/modules/k6/browser/common/route.go | 14 ++++------ .../k6/browser/tests/frame_manager_test.go | 15 ++++++---- 6 files changed, 60 insertions(+), 26 deletions(-) create mode 100644 internal/js/modules/k6/browser/browser/route_mapping.go diff --git a/internal/js/modules/k6/browser/browser/page_mapping.go b/internal/js/modules/k6/browser/browser/page_mapping.go index 785e9b5ada6..869d157e1f8 100644 --- a/internal/js/modules/k6/browser/browser/page_mapping.go +++ b/internal/js/modules/k6/browser/browser/page_mapping.go @@ -909,9 +909,10 @@ func mapPageRoute(vu moduleVU, p *common.Page) func(path sobek.Value, handler so tq.Queue(func() error { defer close(done) + mr := mapRoute(vu, route) _, err = handler( sobek.Undefined(), - vu.Runtime().ToValue(route), + vu.Runtime().ToValue(mr), ) if err != nil { rtnErr = fmt.Errorf("executing page.route('%s') handler: %w", path, err) diff --git a/internal/js/modules/k6/browser/browser/route_mapping.go b/internal/js/modules/k6/browser/browser/route_mapping.go new file mode 100644 index 00000000000..1e1a603a984 --- /dev/null +++ b/internal/js/modules/k6/browser/browser/route_mapping.go @@ -0,0 +1,20 @@ +package browser + +import ( + "github.com/grafana/sobek" + + "go.k6.io/k6/internal/js/modules/k6/browser/common" + "go.k6.io/k6/internal/js/modules/k6/browser/k6ext" +) + +// mapRoute to the JS module. +func mapRoute(vu moduleVU, route *common.Route) mapping { + return mapping{ + "abort": func(reason string) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + return nil, route.Abort(reason) + }) + }, + "request": route.Request, + } +} diff --git a/internal/js/modules/k6/browser/common/frame_manager.go b/internal/js/modules/k6/browser/common/frame_manager.go index 87d40953983..86c0a1f401f 100644 --- a/internal/js/modules/k6/browser/common/frame_manager.go +++ b/internal/js/modules/k6/browser/common/frame_manager.go @@ -574,7 +574,11 @@ func (m *FrameManager) requestStarted(req *Request) { return } - route.Continue() + + if err := route.Continue(); err != nil { + m.logger.Warnf("FrameManager:requestStarted", + "fmid:%d rurl:%s error continuing request: %v", m.ID(), req.URL(), err) + } } // Frames returns a list of frames on the page. diff --git a/internal/js/modules/k6/browser/common/network_manager.go b/internal/js/modules/k6/browser/common/network_manager.go index 66694f2a3b2..a65cfd482d6 100644 --- a/internal/js/modules/k6/browser/common/network_manager.go +++ b/internal/js/modules/k6/browser/common/network_manager.go @@ -642,7 +642,11 @@ func (m *NetworkManager) onRequestPaused(event *fetch.EventRequestPaused) { // If no route was added, continue all requests if m.frameManager.page == nil || !m.frameManager.page.hasRoutes() { - m.ContinueRequest(event.RequestID) + err := m.ContinueRequest(event.RequestID) + if err != nil { + m.logger.Errorf("NetworkManager:onRequestPaused", + "continuing request %s %s: %s", event.Request.Method, event.Request.URL, err) + } } }() @@ -852,13 +856,12 @@ func (m *NetworkManager) Authenticate(credentials Credentials) error { return nil } -func (m *NetworkManager) AbortRequest(requestID fetch.RequestID, errorReason string) { +func (m *NetworkManager) AbortRequest(requestID fetch.RequestID, errorReason string) error { m.logger.Debugf("NetworkManager:AbortRequest", "aborting request (id: %s, errorReason: %s)", requestID, errorReason) netErrorReason, ok := m.errorReasons[errorReason] if !ok { - m.logger.Errorf("NetworkManager:AbortRequest", "unknown error code: %s", errorReason) - return + return fmt.Errorf("unknown error code: %s", errorReason) } action := fetch.FailRequest(requestID, netErrorReason) @@ -869,13 +872,15 @@ func (m *NetworkManager) AbortRequest(requestID fetch.RequestID, errorReason str if errors.Is(err, context.Canceled) { m.logger.Debug("NetworkManager:AbortRequest", "context canceled interrupting request") } else { - m.logger.Errorf("NetworkManager:AbortRequest", "fail to abort request (id: %s): %s", requestID, err) + return fmt.Errorf("fail to abort request (id: %s): %w", requestID, err) } - return + return nil } + + return nil } -func (m *NetworkManager) ContinueRequest(requestID fetch.RequestID) { +func (m *NetworkManager) ContinueRequest(requestID fetch.RequestID) error { m.logger.Debugf("NetworkManager:ContinueRequest", "continuing request (id: %s)", requestID) action := fetch.ContinueRequest(requestID) @@ -885,7 +890,7 @@ func (m *NetworkManager) ContinueRequest(requestID fetch.RequestID) { // while the iteration is ending and therefore the browser context is being closed. if errors.Is(err, context.Canceled) { m.logger.Debug("NetworkManager:ContinueRequest", "context canceled continuing request") - return + return nil } // This error message is an internal issue, rather than something that the user can @@ -895,12 +900,13 @@ func (m *NetworkManager) ContinueRequest(requestID fetch.RequestID) { if strings.Contains(err.Error(), "Invalid InterceptionId") { m.logger.Debugf("NetworkManager:ContinueRequest", "invalid interception ID (%s) continuing request: %s", requestID, err) - return + return nil } - m.logger.Errorf("NetworkManager:ContinueRequest", "fail to continue request (id: %s): %s", - requestID, err) + return fmt.Errorf("fail to continue request (id: %s): %w", requestID, err) } + + return nil } func (m *NetworkManager) FulfillRequest(requestID fetch.RequestID, params fetch.FulfillRequestParams) { diff --git a/internal/js/modules/k6/browser/common/route.go b/internal/js/modules/k6/browser/common/route.go index f18ee5f1cde..4444cf30975 100644 --- a/internal/js/modules/k6/browser/common/route.go +++ b/internal/js/modules/k6/browser/common/route.go @@ -26,28 +26,26 @@ func NewRoute(logger *log.Logger, networkManager *NetworkManager, request *Reque func (r *Route) Request() *Request { return r.request } -func (r *Route) Abort(errorCode string) { +func (r *Route) Abort(errorCode string) error { err := r.startHandling() if err != nil { - r.logger.Errorf("Route:Abort", "rurl:%s err:%s", r.request.URL(), err) - return + return err } if errorCode == "" { errorCode = "failed" } - r.networkManager.AbortRequest(r.request.interceptionID, errorCode) + return r.networkManager.AbortRequest(r.request.interceptionID, errorCode) } -func (r *Route) Continue() { +func (r *Route) Continue() error { err := r.startHandling() if err != nil { - r.logger.Errorf("Route:Continue", "rurl:%s err:%s", r.request.URL(), err) - return + return err } - r.networkManager.ContinueRequest(r.request.interceptionID) + return r.networkManager.ContinueRequest(r.request.interceptionID) } func (r *Route) startHandling() error { diff --git a/internal/js/modules/k6/browser/tests/frame_manager_test.go b/internal/js/modules/k6/browser/tests/frame_manager_test.go index 3d63b7176c7..a05c91d8688 100644 --- a/internal/js/modules/k6/browser/tests/frame_manager_test.go +++ b/internal/js/modules/k6/browser/tests/frame_manager_test.go @@ -148,7 +148,8 @@ func TestFrameManagerRequestStartedWithRoutes(t *testing.T) { name: "continue_request_with_matching_string_route", routePath: "/data/first", routeHandler: func(route *common.Route) { - route.Continue() + err := route.Continue() + assert.NoError(t, err) }, routeHandlerCallsCount: 1, apiHandlerCallsCount: 2, @@ -157,7 +158,8 @@ func TestFrameManagerRequestStartedWithRoutes(t *testing.T) { name: "continue_request_with_non_matching_string_route", routePath: "/data/third", routeHandler: func(route *common.Route) { - route.Continue() + err := route.Continue() + assert.NoError(t, err) }, routeHandlerCallsCount: 0, apiHandlerCallsCount: 2, @@ -166,7 +168,8 @@ func TestFrameManagerRequestStartedWithRoutes(t *testing.T) { name: "continue_request_with_multiple_matching_regex_route", routePath: "/data/.*", routeHandler: func(route *common.Route) { - route.Continue() + err := route.Continue() + assert.NoError(t, err) }, routeHandlerCallsCount: 2, apiHandlerCallsCount: 2, @@ -175,7 +178,8 @@ func TestFrameManagerRequestStartedWithRoutes(t *testing.T) { name: "abort_first_request", routePath: "/data/first", routeHandler: func(route *common.Route) { - route.Abort("failed") + err := route.Abort("failed") + assert.NoError(t, err) }, routeHandlerCallsCount: 1, apiHandlerCallsCount: 0, // Second API call is not made because the first throws an error @@ -184,7 +188,8 @@ func TestFrameManagerRequestStartedWithRoutes(t *testing.T) { name: "abort_second_request", routePath: "/data/second", routeHandler: func(route *common.Route) { - route.Abort("failed") + err := route.Abort("failed") + assert.NoError(t, err) }, routeHandlerCallsCount: 1, apiHandlerCallsCount: 1, From 49e4e61c4a438e9f121c32fa419a646c29579c46 Mon Sep 17 00:00:00 2001 From: AgnesToulet <35176601+AgnesToulet@users.noreply.github.com> Date: Tue, 22 Jul 2025 15:08:30 +0200 Subject: [PATCH 03/15] Add fulfill function --- .../k6/browser/browser/route_mapping.go | 44 +++++++++++++++ .../k6/browser/common/network_manager.go | 56 ++++++++++++++++--- .../js/modules/k6/browser/common/route.go | 16 ++++++ 3 files changed, 107 insertions(+), 9 deletions(-) diff --git a/internal/js/modules/k6/browser/browser/route_mapping.go b/internal/js/modules/k6/browser/browser/route_mapping.go index 1e1a603a984..d0d1045aa0c 100644 --- a/internal/js/modules/k6/browser/browser/route_mapping.go +++ b/internal/js/modules/k6/browser/browser/route_mapping.go @@ -1,6 +1,9 @@ package browser import ( + "context" + "fmt" + "github.com/grafana/sobek" "go.k6.io/k6/internal/js/modules/k6/browser/common" @@ -15,6 +18,47 @@ func mapRoute(vu moduleVU, route *common.Route) mapping { return nil, route.Abort(reason) }) }, + "fulfill": func(opts sobek.Value) *sobek.Promise { + return k6ext.Promise(vu.Context(), func() (any, error) { + fopts := parseFulfillOptions(vu.Context(), opts) + fmt.Printf("Route:Fulfill: %+v\n", fopts) + return nil, route.Fulfill(fopts) + }) + }, "request": route.Request, } } + +func parseFulfillOptions(ctx context.Context, opts sobek.Value) *common.FulfillOptions { + if !sobekValueExists(opts) { + return nil + } + + rt := k6ext.Runtime(ctx) + copts := &common.FulfillOptions{} + + obj := opts.ToObject(rt) + for _, k := range obj.Keys() { + switch k { + case "body": + copts.Body = obj.Get(k).String() + case "contentType": + copts.ContentType = obj.Get(k).String() + case "headers": + headers := obj.Get(k).ToObject(rt) + headersKeys := headers.Keys() + copts.Headers = make([]common.HTTPHeader, len(headersKeys)) + for i, hk := range headersKeys { + copts.Headers[i] = common.HTTPHeader{ + Name: hk, + Value: headers.Get(hk).String(), + } + } + case "status": + copts.Status = obj.Get(k).ToInteger() + + } + } + + return copts +} diff --git a/internal/js/modules/k6/browser/common/network_manager.go b/internal/js/modules/k6/browser/common/network_manager.go index a65cfd482d6..13c5249a2da 100644 --- a/internal/js/modules/k6/browser/common/network_manager.go +++ b/internal/js/modules/k6/browser/common/network_manager.go @@ -2,9 +2,11 @@ package common import ( "context" + "encoding/base64" "errors" "fmt" "net" + "net/http" "net/url" "strconv" "strings" @@ -909,11 +911,30 @@ func (m *NetworkManager) ContinueRequest(requestID fetch.RequestID) error { return nil } -func (m *NetworkManager) FulfillRequest(requestID fetch.RequestID, params fetch.FulfillRequestParams) { - action := fetch.FulfillRequest(requestID, params.ResponseCode). - WithResponseHeaders(params.ResponseHeaders). - WithResponsePhrase(params.ResponsePhrase). - WithBody(params.Body) +func (m *NetworkManager) FulfillRequest(request *Request, opts *FulfillOptions) error { + responseCode := int64(http.StatusOK) + if opts != nil && opts.Status != 0 { + responseCode = opts.Status + } + + action := fetch.FulfillRequest(request.interceptionID, responseCode) + + if opts.ContentType != "" { + opts.Headers = append(opts.Headers, HTTPHeader{ + Name: "Content-Type", + Value: opts.ContentType, + }) + } + + headers := toFetchHeaders(opts.Headers) + if len(headers) > 0 { + action = action.WithResponseHeaders(headers) + } + + if opts.Body != "" { + b64Body := base64.StdEncoding.EncodeToString([]byte(opts.Body)) + action = action.WithBody(b64Body) + } if err := action.Do(cdp.WithExecutor(m.ctx, m.session)); err != nil { // Avoid logging as error when context is canceled. @@ -921,12 +942,29 @@ func (m *NetworkManager) FulfillRequest(requestID fetch.RequestID, params fetch. // while the iteration is ending and therefore the browser context is being closed. if errors.Is(err, context.Canceled) { m.logger.Debug("NetworkManager:FulfillRequest", "context canceled fulfilling request") - } else { - m.logger.Errorf("NetworkManager:FulfillRequest", "fail to fulfill request (id: %s): %s", - requestID, err) + return nil + } + + return fmt.Errorf("fail to fulfill request (id: %s): %s", + request.interceptionID, err) + } + + return nil +} + +func toFetchHeaders(headers []HTTPHeader) []*fetch.HeaderEntry { + if len(headers) == 0 { + return nil + } + + fetchHeaders := make([]*fetch.HeaderEntry, len(headers)) + for i, header := range headers { + fetchHeaders[i] = &fetch.HeaderEntry{ + Name: header.Name, + Value: header.Value, } - return } + return fetchHeaders } // SetExtraHTTPHeaders sets extra HTTP request headers to be sent with every request. diff --git a/internal/js/modules/k6/browser/common/route.go b/internal/js/modules/k6/browser/common/route.go index 4444cf30975..e9047d97e95 100644 --- a/internal/js/modules/k6/browser/common/route.go +++ b/internal/js/modules/k6/browser/common/route.go @@ -15,6 +15,13 @@ type Route struct { handled bool } +type FulfillOptions struct { + Body string + ContentType string + Headers []HTTPHeader + Status int64 +} + func NewRoute(logger *log.Logger, networkManager *NetworkManager, request *Request) *Route { return &Route{ logger: logger, @@ -48,6 +55,15 @@ func (r *Route) Continue() error { return r.networkManager.ContinueRequest(r.request.interceptionID) } +func (r *Route) Fulfill(opts *FulfillOptions) error { + err := r.startHandling() + if err != nil { + return err + } + + return r.networkManager.FulfillRequest(r.request, opts) +} + func (r *Route) startHandling() error { if r.handled { return fmt.Errorf("route is already handled") From fabd8315c09f13813957c991a90fc71b4bc7a31b Mon Sep 17 00:00:00 2001 From: AgnesToulet <35176601+AgnesToulet@users.noreply.github.com> Date: Tue, 22 Jul 2025 15:08:36 +0200 Subject: [PATCH 04/15] Add fulfill test --- .../k6/browser/tests/frame_manager_test.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/internal/js/modules/k6/browser/tests/frame_manager_test.go b/internal/js/modules/k6/browser/tests/frame_manager_test.go index a05c91d8688..a67cac91352 100644 --- a/internal/js/modules/k6/browser/tests/frame_manager_test.go +++ b/internal/js/modules/k6/browser/tests/frame_manager_test.go @@ -194,6 +194,23 @@ func TestFrameManagerRequestStartedWithRoutes(t *testing.T) { routeHandlerCallsCount: 1, apiHandlerCallsCount: 1, }, + { + name: "fulfill_request", + routePath: "/data/first", + routeHandler: func(route *common.Route) { + err := route.Fulfill(&common.FulfillOptions{ + Body: `{"data": "Fulfilled data"}`, + ContentType: "application/json", + Headers: []common.HTTPHeader{ + {Name: "Access-Control-Allow-Origin", Value: "*"}, + }, + Status: 200, + }) + assert.NoError(t, err) + }, + routeHandlerCallsCount: 1, + apiHandlerCallsCount: 1, + }, } for _, tt := range tests { From 3f5b581b9a053be164f6df217e0268ab3b847553 Mon Sep 17 00:00:00 2001 From: AgnesToulet <35176601+AgnesToulet@users.noreply.github.com> Date: Thu, 24 Jul 2025 10:05:41 +0200 Subject: [PATCH 05/15] add comments --- internal/js/modules/k6/browser/common/route.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/internal/js/modules/k6/browser/common/route.go b/internal/js/modules/k6/browser/common/route.go index e9047d97e95..1f3dc78d2e0 100644 --- a/internal/js/modules/k6/browser/common/route.go +++ b/internal/js/modules/k6/browser/common/route.go @@ -6,7 +6,7 @@ import ( "go.k6.io/k6/internal/js/modules/k6/browser/log" ) -// Route allows to handle a request +// Route allows to handle a request. type Route struct { logger *log.Logger networkManager *NetworkManager @@ -15,6 +15,7 @@ type Route struct { handled bool } +// FulfillOptions are response fields that can be set when fulfilling a request. type FulfillOptions struct { Body string ContentType string @@ -22,6 +23,7 @@ type FulfillOptions struct { Status int64 } +// NewRoute creates a new Route that allows to modify a request's behavior. func NewRoute(logger *log.Logger, networkManager *NetworkManager, request *Request) *Route { return &Route{ logger: logger, @@ -31,8 +33,10 @@ func NewRoute(logger *log.Logger, networkManager *NetworkManager, request *Reque } } +// Request returns the request associated with the route. func (r *Route) Request() *Request { return r.request } +// Abort aborts the request with the given error code. func (r *Route) Abort(errorCode string) error { err := r.startHandling() if err != nil { @@ -46,6 +50,7 @@ func (r *Route) Abort(errorCode string) error { return r.networkManager.AbortRequest(r.request.interceptionID, errorCode) } +// Continue continues the request. func (r *Route) Continue() error { err := r.startHandling() if err != nil { @@ -55,6 +60,7 @@ func (r *Route) Continue() error { return r.networkManager.ContinueRequest(r.request.interceptionID) } +// Fulfill fulfills the request with the given options for the response. func (r *Route) Fulfill(opts *FulfillOptions) error { err := r.startHandling() if err != nil { From 1926565f2935c56b4085027aaded399e02a1ac56 Mon Sep 17 00:00:00 2001 From: AgnesToulet <35176601+AgnesToulet@users.noreply.github.com> Date: Thu, 24 Jul 2025 10:15:45 +0200 Subject: [PATCH 06/15] move back route struct to http.go --- internal/js/modules/k6/browser/common/http.go | 72 +++++++++++++++++ .../js/modules/k6/browser/common/route.go | 79 ------------------- 2 files changed, 72 insertions(+), 79 deletions(-) delete mode 100644 internal/js/modules/k6/browser/common/route.go diff --git a/internal/js/modules/k6/browser/common/http.go b/internal/js/modules/k6/browser/common/http.go index 76d78ec68b5..745e95f70c0 100644 --- a/internal/js/modules/k6/browser/common/http.go +++ b/internal/js/modules/k6/browser/common/http.go @@ -708,3 +708,75 @@ func (r *Response) Text() (string, error) { func (r *Response) URL() string { return r.url } + +// Route allows to handle a request. +type Route struct { + logger *log.Logger + networkManager *NetworkManager + + request *Request + handled bool +} + +// FulfillOptions are response fields that can be set when fulfilling a request. +type FulfillOptions struct { + Body string + ContentType string + Headers []HTTPHeader + Status int64 +} + +// NewRoute creates a new Route that allows to modify a request's behavior. +func NewRoute(logger *log.Logger, networkManager *NetworkManager, request *Request) *Route { + return &Route{ + logger: logger, + networkManager: networkManager, + request: request, + handled: false, + } +} + +// Request returns the request associated with the route. +func (r *Route) Request() *Request { return r.request } + +// Abort aborts the request with the given error code. +func (r *Route) Abort(errorCode string) error { + err := r.startHandling() + if err != nil { + return err + } + + if errorCode == "" { + errorCode = "failed" + } + + return r.networkManager.AbortRequest(r.request.interceptionID, errorCode) +} + +// Continue continues the request. +func (r *Route) Continue() error { + err := r.startHandling() + if err != nil { + return err + } + + return r.networkManager.ContinueRequest(r.request.interceptionID) +} + +// Fulfill fulfills the request with the given options for the response. +func (r *Route) Fulfill(opts *FulfillOptions) error { + err := r.startHandling() + if err != nil { + return err + } + + return r.networkManager.FulfillRequest(r.request, opts) +} + +func (r *Route) startHandling() error { + if r.handled { + return fmt.Errorf("route is already handled") + } + r.handled = true + return nil +} diff --git a/internal/js/modules/k6/browser/common/route.go b/internal/js/modules/k6/browser/common/route.go deleted file mode 100644 index 1f3dc78d2e0..00000000000 --- a/internal/js/modules/k6/browser/common/route.go +++ /dev/null @@ -1,79 +0,0 @@ -package common - -import ( - "fmt" - - "go.k6.io/k6/internal/js/modules/k6/browser/log" -) - -// Route allows to handle a request. -type Route struct { - logger *log.Logger - networkManager *NetworkManager - - request *Request - handled bool -} - -// FulfillOptions are response fields that can be set when fulfilling a request. -type FulfillOptions struct { - Body string - ContentType string - Headers []HTTPHeader - Status int64 -} - -// NewRoute creates a new Route that allows to modify a request's behavior. -func NewRoute(logger *log.Logger, networkManager *NetworkManager, request *Request) *Route { - return &Route{ - logger: logger, - networkManager: networkManager, - request: request, - handled: false, - } -} - -// Request returns the request associated with the route. -func (r *Route) Request() *Request { return r.request } - -// Abort aborts the request with the given error code. -func (r *Route) Abort(errorCode string) error { - err := r.startHandling() - if err != nil { - return err - } - - if errorCode == "" { - errorCode = "failed" - } - - return r.networkManager.AbortRequest(r.request.interceptionID, errorCode) -} - -// Continue continues the request. -func (r *Route) Continue() error { - err := r.startHandling() - if err != nil { - return err - } - - return r.networkManager.ContinueRequest(r.request.interceptionID) -} - -// Fulfill fulfills the request with the given options for the response. -func (r *Route) Fulfill(opts *FulfillOptions) error { - err := r.startHandling() - if err != nil { - return err - } - - return r.networkManager.FulfillRequest(r.request, opts) -} - -func (r *Route) startHandling() error { - if r.handled { - return fmt.Errorf("route is already handled") - } - r.handled = true - return nil -} From 9d91850d3690f1fc2d694cff9e4be43a9ecaa221 Mon Sep 17 00:00:00 2001 From: AgnesToulet <35176601+AgnesToulet@users.noreply.github.com> Date: Thu, 24 Jul 2025 10:16:13 +0200 Subject: [PATCH 07/15] add request mapping --- internal/js/modules/k6/browser/browser/route_mapping.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/internal/js/modules/k6/browser/browser/route_mapping.go b/internal/js/modules/k6/browser/browser/route_mapping.go index d0d1045aa0c..d64985f7709 100644 --- a/internal/js/modules/k6/browser/browser/route_mapping.go +++ b/internal/js/modules/k6/browser/browser/route_mapping.go @@ -25,7 +25,9 @@ func mapRoute(vu moduleVU, route *common.Route) mapping { return nil, route.Fulfill(fopts) }) }, - "request": route.Request, + "request": func() mapping { + return mapRequest(vu, route.Request()) + }, } } From 50596d6135ecae25b96797b4503d37cb96bbe316 Mon Sep 17 00:00:00 2001 From: AgnesToulet <35176601+AgnesToulet@users.noreply.github.com> Date: Thu, 24 Jul 2025 10:16:25 +0200 Subject: [PATCH 08/15] remove forgotten log --- internal/js/modules/k6/browser/browser/route_mapping.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/internal/js/modules/k6/browser/browser/route_mapping.go b/internal/js/modules/k6/browser/browser/route_mapping.go index d64985f7709..56138d4ba39 100644 --- a/internal/js/modules/k6/browser/browser/route_mapping.go +++ b/internal/js/modules/k6/browser/browser/route_mapping.go @@ -2,7 +2,6 @@ package browser import ( "context" - "fmt" "github.com/grafana/sobek" @@ -21,7 +20,6 @@ func mapRoute(vu moduleVU, route *common.Route) mapping { "fulfill": func(opts sobek.Value) *sobek.Promise { return k6ext.Promise(vu.Context(), func() (any, error) { fopts := parseFulfillOptions(vu.Context(), opts) - fmt.Printf("Route:Fulfill: %+v\n", fopts) return nil, route.Fulfill(fopts) }) }, From 99559700350547cd1898c43b3e148b67ce9b5366 Mon Sep 17 00:00:00 2001 From: AgnesToulet <35176601+AgnesToulet@users.noreply.github.com> Date: Thu, 24 Jul 2025 10:19:30 +0200 Subject: [PATCH 09/15] fix linter issues --- internal/js/modules/k6/browser/browser/route_mapping.go | 1 - internal/js/modules/k6/browser/common/network_manager.go | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/internal/js/modules/k6/browser/browser/route_mapping.go b/internal/js/modules/k6/browser/browser/route_mapping.go index 56138d4ba39..332856cfcb5 100644 --- a/internal/js/modules/k6/browser/browser/route_mapping.go +++ b/internal/js/modules/k6/browser/browser/route_mapping.go @@ -56,7 +56,6 @@ func parseFulfillOptions(ctx context.Context, opts sobek.Value) *common.FulfillO } case "status": copts.Status = obj.Get(k).ToInteger() - } } diff --git a/internal/js/modules/k6/browser/common/network_manager.go b/internal/js/modules/k6/browser/common/network_manager.go index 13c5249a2da..0e79611b2d5 100644 --- a/internal/js/modules/k6/browser/common/network_manager.go +++ b/internal/js/modules/k6/browser/common/network_manager.go @@ -945,7 +945,7 @@ func (m *NetworkManager) FulfillRequest(request *Request, opts *FulfillOptions) return nil } - return fmt.Errorf("fail to fulfill request (id: %s): %s", + return fmt.Errorf("fail to fulfill request (id: %s): %w", request.interceptionID, err) } From 4a66aa04674f8ca4083e3cfb30c03406e2b4b32e Mon Sep 17 00:00:00 2001 From: AgnesToulet <35176601+AgnesToulet@users.noreply.github.com> Date: Thu, 24 Jul 2025 15:34:51 +0200 Subject: [PATCH 10/15] rename var --- .../js/modules/k6/browser/browser/route_mapping.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/internal/js/modules/k6/browser/browser/route_mapping.go b/internal/js/modules/k6/browser/browser/route_mapping.go index 332856cfcb5..620d1952c2b 100644 --- a/internal/js/modules/k6/browser/browser/route_mapping.go +++ b/internal/js/modules/k6/browser/browser/route_mapping.go @@ -35,29 +35,29 @@ func parseFulfillOptions(ctx context.Context, opts sobek.Value) *common.FulfillO } rt := k6ext.Runtime(ctx) - copts := &common.FulfillOptions{} + fopts := &common.FulfillOptions{} obj := opts.ToObject(rt) for _, k := range obj.Keys() { switch k { case "body": - copts.Body = obj.Get(k).String() + fopts.Body = obj.Get(k).String() case "contentType": - copts.ContentType = obj.Get(k).String() + fopts.ContentType = obj.Get(k).String() case "headers": headers := obj.Get(k).ToObject(rt) headersKeys := headers.Keys() - copts.Headers = make([]common.HTTPHeader, len(headersKeys)) + fopts.Headers = make([]common.HTTPHeader, len(headersKeys)) for i, hk := range headersKeys { - copts.Headers[i] = common.HTTPHeader{ + fopts.Headers[i] = common.HTTPHeader{ Name: hk, Value: headers.Get(hk).String(), } } case "status": - copts.Status = obj.Get(k).ToInteger() + fopts.Status = obj.Get(k).ToInteger() } } - return copts + return fopts } From 4452b9e7a6bd7196a429689bd91ca7365035d56e Mon Sep 17 00:00:00 2001 From: AgnesToulet <35176601+AgnesToulet@users.noreply.github.com> Date: Tue, 29 Jul 2025 15:40:47 +0200 Subject: [PATCH 11/15] remove pointer to fulfilloptions --- internal/js/modules/k6/browser/browser/route_mapping.go | 9 ++++----- internal/js/modules/k6/browser/common/http.go | 2 +- internal/js/modules/k6/browser/common/network_manager.go | 4 ++-- .../js/modules/k6/browser/tests/frame_manager_test.go | 2 +- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/internal/js/modules/k6/browser/browser/route_mapping.go b/internal/js/modules/k6/browser/browser/route_mapping.go index 620d1952c2b..eb880af128c 100644 --- a/internal/js/modules/k6/browser/browser/route_mapping.go +++ b/internal/js/modules/k6/browser/browser/route_mapping.go @@ -18,8 +18,8 @@ func mapRoute(vu moduleVU, route *common.Route) mapping { }) }, "fulfill": func(opts sobek.Value) *sobek.Promise { + fopts := parseFulfillOptions(vu.Context(), opts) return k6ext.Promise(vu.Context(), func() (any, error) { - fopts := parseFulfillOptions(vu.Context(), opts) return nil, route.Fulfill(fopts) }) }, @@ -29,14 +29,13 @@ func mapRoute(vu moduleVU, route *common.Route) mapping { } } -func parseFulfillOptions(ctx context.Context, opts sobek.Value) *common.FulfillOptions { +func parseFulfillOptions(ctx context.Context, opts sobek.Value) common.FulfillOptions { + fopts := common.FulfillOptions{} if !sobekValueExists(opts) { - return nil + return fopts } rt := k6ext.Runtime(ctx) - fopts := &common.FulfillOptions{} - obj := opts.ToObject(rt) for _, k := range obj.Keys() { switch k { diff --git a/internal/js/modules/k6/browser/common/http.go b/internal/js/modules/k6/browser/common/http.go index 745e95f70c0..ed48cf6b38b 100644 --- a/internal/js/modules/k6/browser/common/http.go +++ b/internal/js/modules/k6/browser/common/http.go @@ -764,7 +764,7 @@ func (r *Route) Continue() error { } // Fulfill fulfills the request with the given options for the response. -func (r *Route) Fulfill(opts *FulfillOptions) error { +func (r *Route) Fulfill(opts FulfillOptions) error { err := r.startHandling() if err != nil { return err diff --git a/internal/js/modules/k6/browser/common/network_manager.go b/internal/js/modules/k6/browser/common/network_manager.go index 0e79611b2d5..5bb184a598b 100644 --- a/internal/js/modules/k6/browser/common/network_manager.go +++ b/internal/js/modules/k6/browser/common/network_manager.go @@ -911,9 +911,9 @@ func (m *NetworkManager) ContinueRequest(requestID fetch.RequestID) error { return nil } -func (m *NetworkManager) FulfillRequest(request *Request, opts *FulfillOptions) error { +func (m *NetworkManager) FulfillRequest(request *Request, opts FulfillOptions) error { responseCode := int64(http.StatusOK) - if opts != nil && opts.Status != 0 { + if opts.Status != 0 { responseCode = opts.Status } diff --git a/internal/js/modules/k6/browser/tests/frame_manager_test.go b/internal/js/modules/k6/browser/tests/frame_manager_test.go index a67cac91352..70d1269f0d0 100644 --- a/internal/js/modules/k6/browser/tests/frame_manager_test.go +++ b/internal/js/modules/k6/browser/tests/frame_manager_test.go @@ -198,7 +198,7 @@ func TestFrameManagerRequestStartedWithRoutes(t *testing.T) { name: "fulfill_request", routePath: "/data/first", routeHandler: func(route *common.Route) { - err := route.Fulfill(&common.FulfillOptions{ + err := route.Fulfill(common.FulfillOptions{ Body: `{"data": "Fulfilled data"}`, ContentType: "application/json", Headers: []common.HTTPHeader{ From 0ed75286a401852e6c8e98ae153a09cbb8daefc9 Mon Sep 17 00:00:00 2001 From: AgnesToulet <35176601+AgnesToulet@users.noreply.github.com> Date: Tue, 29 Jul 2025 15:43:46 +0200 Subject: [PATCH 12/15] remove extra return --- internal/js/modules/k6/browser/common/network_manager.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/js/modules/k6/browser/common/network_manager.go b/internal/js/modules/k6/browser/common/network_manager.go index 5bb184a598b..298ef580050 100644 --- a/internal/js/modules/k6/browser/common/network_manager.go +++ b/internal/js/modules/k6/browser/common/network_manager.go @@ -876,7 +876,6 @@ func (m *NetworkManager) AbortRequest(requestID fetch.RequestID, errorReason str } else { return fmt.Errorf("fail to abort request (id: %s): %w", requestID, err) } - return nil } return nil From 55db13065729ccd81042d0be119aea913fc2d104 Mon Sep 17 00:00:00 2001 From: AgnesToulet <35176601+AgnesToulet@users.noreply.github.com> Date: Tue, 29 Jul 2025 16:22:15 +0200 Subject: [PATCH 13/15] update log level to keep it consistent --- internal/js/modules/k6/browser/common/frame_manager.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/js/modules/k6/browser/common/frame_manager.go b/internal/js/modules/k6/browser/common/frame_manager.go index 86c0a1f401f..8af9b1850b0 100644 --- a/internal/js/modules/k6/browser/common/frame_manager.go +++ b/internal/js/modules/k6/browser/common/frame_manager.go @@ -576,7 +576,7 @@ func (m *FrameManager) requestStarted(req *Request) { } if err := route.Continue(); err != nil { - m.logger.Warnf("FrameManager:requestStarted", + m.logger.Errorf("FrameManager:requestStarted", "fmid:%d rurl:%s error continuing request: %v", m.ID(), req.URL(), err) } } From adc5394b6e735ad94abce20f47bbc92d62b29cbf Mon Sep 17 00:00:00 2001 From: AgnesToulet <35176601+AgnesToulet@users.noreply.github.com> Date: Fri, 1 Aug 2025 09:32:58 +0200 Subject: [PATCH 14/15] support buffer in body input --- .../k6/browser/browser/route_mapping.go | 18 +++++++++++++----- internal/js/modules/k6/browser/common/http.go | 2 +- .../k6/browser/common/network_manager.go | 4 ++-- .../k6/browser/tests/frame_manager_test.go | 2 +- 4 files changed, 17 insertions(+), 9 deletions(-) diff --git a/internal/js/modules/k6/browser/browser/route_mapping.go b/internal/js/modules/k6/browser/browser/route_mapping.go index eb880af128c..f5f2742ea7f 100644 --- a/internal/js/modules/k6/browser/browser/route_mapping.go +++ b/internal/js/modules/k6/browser/browser/route_mapping.go @@ -7,6 +7,7 @@ import ( "go.k6.io/k6/internal/js/modules/k6/browser/common" "go.k6.io/k6/internal/js/modules/k6/browser/k6ext" + jsCommon "go.k6.io/k6/js/common" ) // mapRoute to the JS module. @@ -18,8 +19,11 @@ func mapRoute(vu moduleVU, route *common.Route) mapping { }) }, "fulfill": func(opts sobek.Value) *sobek.Promise { - fopts := parseFulfillOptions(vu.Context(), opts) + fopts, err := parseFulfillOptions(vu.Context(), opts) return k6ext.Promise(vu.Context(), func() (any, error) { + if err != nil { + return nil, err + } return nil, route.Fulfill(fopts) }) }, @@ -29,10 +33,10 @@ func mapRoute(vu moduleVU, route *common.Route) mapping { } } -func parseFulfillOptions(ctx context.Context, opts sobek.Value) common.FulfillOptions { +func parseFulfillOptions(ctx context.Context, opts sobek.Value) (common.FulfillOptions, error) { fopts := common.FulfillOptions{} if !sobekValueExists(opts) { - return fopts + return fopts, nil } rt := k6ext.Runtime(ctx) @@ -40,7 +44,11 @@ func parseFulfillOptions(ctx context.Context, opts sobek.Value) common.FulfillOp for _, k := range obj.Keys() { switch k { case "body": - fopts.Body = obj.Get(k).String() + bytesBody, err := jsCommon.ToBytes(obj.Get(k).Export()) + if err != nil { + return fopts, err + } + fopts.Body = bytesBody case "contentType": fopts.ContentType = obj.Get(k).String() case "headers": @@ -58,5 +66,5 @@ func parseFulfillOptions(ctx context.Context, opts sobek.Value) common.FulfillOp } } - return fopts + return fopts, nil } diff --git a/internal/js/modules/k6/browser/common/http.go b/internal/js/modules/k6/browser/common/http.go index ed48cf6b38b..1198dee7503 100644 --- a/internal/js/modules/k6/browser/common/http.go +++ b/internal/js/modules/k6/browser/common/http.go @@ -720,7 +720,7 @@ type Route struct { // FulfillOptions are response fields that can be set when fulfilling a request. type FulfillOptions struct { - Body string + Body []byte ContentType string Headers []HTTPHeader Status int64 diff --git a/internal/js/modules/k6/browser/common/network_manager.go b/internal/js/modules/k6/browser/common/network_manager.go index 298ef580050..b7a782b7228 100644 --- a/internal/js/modules/k6/browser/common/network_manager.go +++ b/internal/js/modules/k6/browser/common/network_manager.go @@ -930,8 +930,8 @@ func (m *NetworkManager) FulfillRequest(request *Request, opts FulfillOptions) e action = action.WithResponseHeaders(headers) } - if opts.Body != "" { - b64Body := base64.StdEncoding.EncodeToString([]byte(opts.Body)) + if len(opts.Body) > 0 { + b64Body := base64.StdEncoding.EncodeToString(opts.Body) action = action.WithBody(b64Body) } diff --git a/internal/js/modules/k6/browser/tests/frame_manager_test.go b/internal/js/modules/k6/browser/tests/frame_manager_test.go index 70d1269f0d0..3b2417ca1b5 100644 --- a/internal/js/modules/k6/browser/tests/frame_manager_test.go +++ b/internal/js/modules/k6/browser/tests/frame_manager_test.go @@ -199,7 +199,7 @@ func TestFrameManagerRequestStartedWithRoutes(t *testing.T) { routePath: "/data/first", routeHandler: func(route *common.Route) { err := route.Fulfill(common.FulfillOptions{ - Body: `{"data": "Fulfilled data"}`, + Body: []byte(`{"data": "Fulfilled data"}`), ContentType: "application/json", Headers: []common.HTTPHeader{ {Name: "Access-Control-Allow-Origin", Value: "*"}, From 67f24be3c8cb74ac463af2cf62248fecba3024e0 Mon Sep 17 00:00:00 2001 From: AgnesToulet <35176601+AgnesToulet@users.noreply.github.com> Date: Fri, 1 Aug 2025 09:36:33 +0200 Subject: [PATCH 15/15] return error for unsupported options --- internal/js/modules/k6/browser/browser/route_mapping.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/js/modules/k6/browser/browser/route_mapping.go b/internal/js/modules/k6/browser/browser/route_mapping.go index f5f2742ea7f..442f4c145d8 100644 --- a/internal/js/modules/k6/browser/browser/route_mapping.go +++ b/internal/js/modules/k6/browser/browser/route_mapping.go @@ -2,6 +2,7 @@ package browser import ( "context" + "fmt" "github.com/grafana/sobek" @@ -63,6 +64,9 @@ func parseFulfillOptions(ctx context.Context, opts sobek.Value) (common.FulfillO } case "status": fopts.Status = obj.Get(k).ToInteger() + // As we don't support all fields that PW supports, we return an error to inform the user + default: + return fopts, fmt.Errorf("unsupported fulfill option: '%s'", k) } }