Skip to content

Commit 9d9116c

Browse files
AgnesTouletpaulnegz
authored andcommitted
Browser: add route.fulfill (grafana#4961)
* Refactor: move route types and function to its own file * Refactor: add route mapping and return errors * Add fulfill function * Add fulfill test * add comments * move back route struct to http.go * add request mapping * remove forgotten log * fix linter issues * rename var * remove pointer to fulfilloptions * remove extra return * update log level to keep it consistent * support buffer in body input * return error for unsupported options
1 parent b1b53b5 commit 9d9116c

File tree

6 files changed

+197
-36
lines changed

6 files changed

+197
-36
lines changed

internal/js/modules/k6/browser/browser/page_mapping.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -933,9 +933,10 @@ func mapPageRoute(vu moduleVU, p *common.Page) func(path sobek.Value, handler so
933933
tq.Queue(func() error {
934934
defer close(done)
935935

936+
mr := mapRoute(vu, route)
936937
_, err = handler(
937938
sobek.Undefined(),
938-
vu.Runtime().ToValue(route),
939+
vu.Runtime().ToValue(mr),
939940
)
940941
if err != nil {
941942
done <- fmt.Errorf("executing page.route('%s') handler: %w", path, err)
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package browser
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/grafana/sobek"
8+
9+
"go.k6.io/k6/internal/js/modules/k6/browser/common"
10+
"go.k6.io/k6/internal/js/modules/k6/browser/k6ext"
11+
jsCommon "go.k6.io/k6/js/common"
12+
)
13+
14+
// mapRoute to the JS module.
15+
func mapRoute(vu moduleVU, route *common.Route) mapping {
16+
return mapping{
17+
"abort": func(reason string) *sobek.Promise {
18+
return k6ext.Promise(vu.Context(), func() (any, error) {
19+
return nil, route.Abort(reason)
20+
})
21+
},
22+
"fulfill": func(opts sobek.Value) *sobek.Promise {
23+
fopts, err := parseFulfillOptions(vu.Context(), opts)
24+
return k6ext.Promise(vu.Context(), func() (any, error) {
25+
if err != nil {
26+
return nil, err
27+
}
28+
return nil, route.Fulfill(fopts)
29+
})
30+
},
31+
"request": func() mapping {
32+
return mapRequest(vu, route.Request())
33+
},
34+
}
35+
}
36+
37+
func parseFulfillOptions(ctx context.Context, opts sobek.Value) (common.FulfillOptions, error) {
38+
fopts := common.FulfillOptions{}
39+
if !sobekValueExists(opts) {
40+
return fopts, nil
41+
}
42+
43+
rt := k6ext.Runtime(ctx)
44+
obj := opts.ToObject(rt)
45+
for _, k := range obj.Keys() {
46+
switch k {
47+
case "body":
48+
bytesBody, err := jsCommon.ToBytes(obj.Get(k).Export())
49+
if err != nil {
50+
return fopts, err
51+
}
52+
fopts.Body = bytesBody
53+
case "contentType":
54+
fopts.ContentType = obj.Get(k).String()
55+
case "headers":
56+
headers := obj.Get(k).ToObject(rt)
57+
headersKeys := headers.Keys()
58+
fopts.Headers = make([]common.HTTPHeader, len(headersKeys))
59+
for i, hk := range headersKeys {
60+
fopts.Headers[i] = common.HTTPHeader{
61+
Name: hk,
62+
Value: headers.Get(hk).String(),
63+
}
64+
}
65+
case "status":
66+
fopts.Status = obj.Get(k).ToInteger()
67+
// As we don't support all fields that PW supports, we return an error to inform the user
68+
default:
69+
return fopts, fmt.Errorf("unsupported fulfill option: '%s'", k)
70+
}
71+
}
72+
73+
return fopts, nil
74+
}

internal/js/modules/k6/browser/common/frame_manager.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -574,7 +574,11 @@ func (m *FrameManager) requestStarted(req *Request) {
574574

575575
return
576576
}
577-
route.Continue()
577+
578+
if err := route.Continue(); err != nil {
579+
m.logger.Errorf("FrameManager:requestStarted",
580+
"fmid:%d rurl:%s error continuing request: %v", m.ID(), req.URL(), err)
581+
}
578582
}
579583

580584
// Frames returns a list of frames on the page.

internal/js/modules/k6/browser/common/http.go

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -718,6 +718,14 @@ type Route struct {
718718
handled bool
719719
}
720720

721+
// FulfillOptions are response fields that can be set when fulfilling a request.
722+
type FulfillOptions struct {
723+
Body []byte
724+
ContentType string
725+
Headers []HTTPHeader
726+
Status int64
727+
}
728+
721729
// NewRoute creates a new Route that allows to modify a request's behavior.
722730
func NewRoute(logger *log.Logger, networkManager *NetworkManager, request *Request) *Route {
723731
return &Route{
@@ -728,37 +736,46 @@ func NewRoute(logger *log.Logger, networkManager *NetworkManager, request *Reque
728736
}
729737
}
730738

739+
// Request returns the request associated with the route.
731740
func (r *Route) Request() *Request { return r.request }
732741

733742
// Abort aborts the request with the given error code.
734-
func (r *Route) Abort(errorCode string) {
743+
func (r *Route) Abort(errorCode string) error {
735744
err := r.startHandling()
736745
if err != nil {
737-
r.logger.Errorf("Route:Abort", "rurl:%s err:%s", r.request.URL(), err)
738-
return
746+
return err
739747
}
740748

741749
if errorCode == "" {
742750
errorCode = "failed"
743751
}
744752

745-
r.networkManager.AbortRequest(r.request.interceptionID, errorCode)
753+
return r.networkManager.AbortRequest(r.request.interceptionID, errorCode)
746754
}
747755

748756
// Continue continues the request.
749-
func (r *Route) Continue() {
757+
func (r *Route) Continue() error {
758+
err := r.startHandling()
759+
if err != nil {
760+
return err
761+
}
762+
763+
return r.networkManager.ContinueRequest(r.request.interceptionID)
764+
}
765+
766+
// Fulfill fulfills the request with the given options for the response.
767+
func (r *Route) Fulfill(opts FulfillOptions) error {
750768
err := r.startHandling()
751769
if err != nil {
752-
r.logger.Errorf("Route:Continue", "rurl:%s err:%s", r.request.URL(), err)
753-
return
770+
return err
754771
}
755772

756-
r.networkManager.ContinueRequest(r.request.interceptionID)
773+
return r.networkManager.FulfillRequest(r.request, opts)
757774
}
758775

759776
func (r *Route) startHandling() error {
760777
if r.handled {
761-
return fmt.Errorf("route %s is already handled", r.request.URL())
778+
return fmt.Errorf("route is already handled")
762779
}
763780
r.handled = true
764781
return nil

internal/js/modules/k6/browser/common/network_manager.go

Lines changed: 63 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ package common
22

33
import (
44
"context"
5+
"encoding/base64"
56
"errors"
67
"fmt"
78
"net"
9+
"net/http"
810
"net/url"
911
"strconv"
1012
"strings"
@@ -642,7 +644,11 @@ func (m *NetworkManager) onRequestPaused(event *fetch.EventRequestPaused) {
642644

643645
// If no route was added, continue all requests
644646
if m.frameManager.page == nil || !m.frameManager.page.hasRoutes() {
645-
m.ContinueRequest(event.RequestID)
647+
err := m.ContinueRequest(event.RequestID)
648+
if err != nil {
649+
m.logger.Errorf("NetworkManager:onRequestPaused",
650+
"continuing request %s %s: %s", event.Request.Method, event.Request.URL, err)
651+
}
646652
}
647653
}()
648654

@@ -852,13 +858,12 @@ func (m *NetworkManager) Authenticate(credentials Credentials) error {
852858
return nil
853859
}
854860

855-
func (m *NetworkManager) AbortRequest(requestID fetch.RequestID, errorReason string) {
861+
func (m *NetworkManager) AbortRequest(requestID fetch.RequestID, errorReason string) error {
856862
m.logger.Debugf("NetworkManager:AbortRequest", "aborting request (id: %s, errorReason: %s)",
857863
requestID, errorReason)
858864
netErrorReason, ok := m.errorReasons[errorReason]
859865
if !ok {
860-
m.logger.Errorf("NetworkManager:AbortRequest", "unknown error code: %s", errorReason)
861-
return
866+
return fmt.Errorf("unknown error code: %s", errorReason)
862867
}
863868

864869
action := fetch.FailRequest(requestID, netErrorReason)
@@ -869,13 +874,14 @@ func (m *NetworkManager) AbortRequest(requestID fetch.RequestID, errorReason str
869874
if errors.Is(err, context.Canceled) {
870875
m.logger.Debug("NetworkManager:AbortRequest", "context canceled interrupting request")
871876
} else {
872-
m.logger.Errorf("NetworkManager:AbortRequest", "fail to abort request (id: %s): %s", requestID, err)
877+
return fmt.Errorf("fail to abort request (id: %s): %w", requestID, err)
873878
}
874-
return
875879
}
880+
881+
return nil
876882
}
877883

878-
func (m *NetworkManager) ContinueRequest(requestID fetch.RequestID) {
884+
func (m *NetworkManager) ContinueRequest(requestID fetch.RequestID) error {
879885
m.logger.Debugf("NetworkManager:ContinueRequest", "continuing request (id: %s)", requestID)
880886

881887
action := fetch.ContinueRequest(requestID)
@@ -885,7 +891,7 @@ func (m *NetworkManager) ContinueRequest(requestID fetch.RequestID) {
885891
// while the iteration is ending and therefore the browser context is being closed.
886892
if errors.Is(err, context.Canceled) {
887893
m.logger.Debug("NetworkManager:ContinueRequest", "context canceled continuing request")
888-
return
894+
return nil
889895
}
890896

891897
// This error message is an internal issue, rather than something that the user can
@@ -895,32 +901,69 @@ func (m *NetworkManager) ContinueRequest(requestID fetch.RequestID) {
895901
if strings.Contains(err.Error(), "Invalid InterceptionId") {
896902
m.logger.Debugf("NetworkManager:ContinueRequest", "invalid interception ID (%s) continuing request: %s",
897903
requestID, err)
898-
return
904+
return nil
899905
}
900906

901-
m.logger.Errorf("NetworkManager:ContinueRequest", "fail to continue request (id: %s): %s",
902-
requestID, err)
907+
return fmt.Errorf("fail to continue request (id: %s): %w", requestID, err)
903908
}
909+
910+
return nil
904911
}
905912

906-
func (m *NetworkManager) FulfillRequest(requestID fetch.RequestID, params fetch.FulfillRequestParams) {
907-
action := fetch.FulfillRequest(requestID, params.ResponseCode).
908-
WithResponseHeaders(params.ResponseHeaders).
909-
WithResponsePhrase(params.ResponsePhrase).
910-
WithBody(params.Body)
913+
func (m *NetworkManager) FulfillRequest(request *Request, opts FulfillOptions) error {
914+
responseCode := int64(http.StatusOK)
915+
if opts.Status != 0 {
916+
responseCode = opts.Status
917+
}
918+
919+
action := fetch.FulfillRequest(request.interceptionID, responseCode)
920+
921+
if opts.ContentType != "" {
922+
opts.Headers = append(opts.Headers, HTTPHeader{
923+
Name: "Content-Type",
924+
Value: opts.ContentType,
925+
})
926+
}
927+
928+
headers := toFetchHeaders(opts.Headers)
929+
if len(headers) > 0 {
930+
action = action.WithResponseHeaders(headers)
931+
}
932+
933+
if len(opts.Body) > 0 {
934+
b64Body := base64.StdEncoding.EncodeToString(opts.Body)
935+
action = action.WithBody(b64Body)
936+
}
911937

912938
if err := action.Do(cdp.WithExecutor(m.ctx, m.session)); err != nil {
913939
// Avoid logging as error when context is canceled.
914940
// Most probably this happens when trying to fail a site's background request
915941
// while the iteration is ending and therefore the browser context is being closed.
916942
if errors.Is(err, context.Canceled) {
917943
m.logger.Debug("NetworkManager:FulfillRequest", "context canceled fulfilling request")
918-
} else {
919-
m.logger.Errorf("NetworkManager:FulfillRequest", "fail to fulfill request (id: %s): %s",
920-
requestID, err)
944+
return nil
945+
}
946+
947+
return fmt.Errorf("fail to fulfill request (id: %s): %w",
948+
request.interceptionID, err)
949+
}
950+
951+
return nil
952+
}
953+
954+
func toFetchHeaders(headers []HTTPHeader) []*fetch.HeaderEntry {
955+
if len(headers) == 0 {
956+
return nil
957+
}
958+
959+
fetchHeaders := make([]*fetch.HeaderEntry, len(headers))
960+
for i, header := range headers {
961+
fetchHeaders[i] = &fetch.HeaderEntry{
962+
Name: header.Name,
963+
Value: header.Value,
921964
}
922-
return
923965
}
966+
return fetchHeaders
924967
}
925968

926969
// SetExtraHTTPHeaders sets extra HTTP request headers to be sent with every request.

internal/js/modules/k6/browser/tests/frame_manager_test.go

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,8 @@ func TestFrameManagerRequestStartedWithRoutes(t *testing.T) {
148148
name: "continue_request_with_matching_string_route",
149149
routePath: "/data/first",
150150
routeHandler: func(route *common.Route) {
151-
route.Continue()
151+
err := route.Continue()
152+
assert.NoError(t, err)
152153
},
153154
routeHandlerCallsCount: 1,
154155
apiHandlerCallsCount: 2,
@@ -157,7 +158,8 @@ func TestFrameManagerRequestStartedWithRoutes(t *testing.T) {
157158
name: "continue_request_with_non_matching_string_route",
158159
routePath: "/data/third",
159160
routeHandler: func(route *common.Route) {
160-
route.Continue()
161+
err := route.Continue()
162+
assert.NoError(t, err)
161163
},
162164
routeHandlerCallsCount: 0,
163165
apiHandlerCallsCount: 2,
@@ -166,7 +168,8 @@ func TestFrameManagerRequestStartedWithRoutes(t *testing.T) {
166168
name: "continue_request_with_multiple_matching_regex_route",
167169
routePath: "/data/.*",
168170
routeHandler: func(route *common.Route) {
169-
route.Continue()
171+
err := route.Continue()
172+
assert.NoError(t, err)
170173
},
171174
routeHandlerCallsCount: 2,
172175
apiHandlerCallsCount: 2,
@@ -175,7 +178,8 @@ func TestFrameManagerRequestStartedWithRoutes(t *testing.T) {
175178
name: "abort_first_request",
176179
routePath: "/data/first",
177180
routeHandler: func(route *common.Route) {
178-
route.Abort("failed")
181+
err := route.Abort("failed")
182+
assert.NoError(t, err)
179183
},
180184
routeHandlerCallsCount: 1,
181185
apiHandlerCallsCount: 0, // Second API call is not made because the first throws an error
@@ -184,7 +188,25 @@ func TestFrameManagerRequestStartedWithRoutes(t *testing.T) {
184188
name: "abort_second_request",
185189
routePath: "/data/second",
186190
routeHandler: func(route *common.Route) {
187-
route.Abort("failed")
191+
err := route.Abort("failed")
192+
assert.NoError(t, err)
193+
},
194+
routeHandlerCallsCount: 1,
195+
apiHandlerCallsCount: 1,
196+
},
197+
{
198+
name: "fulfill_request",
199+
routePath: "/data/first",
200+
routeHandler: func(route *common.Route) {
201+
err := route.Fulfill(common.FulfillOptions{
202+
Body: []byte(`{"data": "Fulfilled data"}`),
203+
ContentType: "application/json",
204+
Headers: []common.HTTPHeader{
205+
{Name: "Access-Control-Allow-Origin", Value: "*"},
206+
},
207+
Status: 200,
208+
})
209+
assert.NoError(t, err)
188210
},
189211
routeHandlerCallsCount: 1,
190212
apiHandlerCallsCount: 1,

0 commit comments

Comments
 (0)