-
Couldn't load subscription status.
- Fork 1.4k
Browser: add route.fulfill #4961
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
d48013d
f033f0e
49e4e61
fabd831
3f5b581
1926565
9d91850
50596d6
9955970
4a66aa0
4452b9e
0ed7528
55db130
adc5394
67f24be
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| package browser | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
|
|
||
| "github.com/grafana/sobek" | ||
|
|
||
| "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. | ||
| 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) | ||
| }) | ||
| }, | ||
| "fulfill": func(opts sobek.Value) *sobek.Promise { | ||
| 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) | ||
| }) | ||
| }, | ||
| "request": func() mapping { | ||
| return mapRequest(vu, route.Request()) | ||
| }, | ||
| } | ||
| } | ||
|
|
||
| func parseFulfillOptions(ctx context.Context, opts sobek.Value) (common.FulfillOptions, error) { | ||
| fopts := common.FulfillOptions{} | ||
| if !sobekValueExists(opts) { | ||
| return fopts, nil | ||
| } | ||
|
|
||
| rt := k6ext.Runtime(ctx) | ||
| obj := opts.ToObject(rt) | ||
| for _, k := range obj.Keys() { | ||
| switch k { | ||
| case "body": | ||
| 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": | ||
| headers := obj.Get(k).ToObject(rt) | ||
| headersKeys := headers.Keys() | ||
| fopts.Headers = make([]common.HTTPHeader, len(headersKeys)) | ||
| for i, hk := range headersKeys { | ||
| fopts.Headers[i] = common.HTTPHeader{ | ||
| Name: hk, | ||
| Value: headers.Get(hk).String(), | ||
| } | ||
| } | ||
| 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) | ||
| } | ||
| } | ||
|
|
||
| return fopts, nil | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,9 +2,11 @@ package common | |
|
|
||
| import ( | ||
| "context" | ||
| "encoding/base64" | ||
| "errors" | ||
| "fmt" | ||
| "net" | ||
| "net/http" | ||
| "net/url" | ||
| "strconv" | ||
| "strings" | ||
|
|
@@ -642,7 +644,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 +858,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 { | ||
AgnesToulet marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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 +874,14 @@ 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 | ||
| } | ||
|
|
||
| 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 +891,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,32 +901,69 @@ 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) { | ||
| 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.Status != 0 { | ||
| responseCode = opts.Status | ||
| } | ||
|
|
||
| action := fetch.FulfillRequest(request.interceptionID, responseCode) | ||
|
|
||
| if opts.ContentType != "" { | ||
AgnesToulet marked this conversation as resolved.
Show resolved
Hide resolved
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm wondering here... Is it better to keep it like this or actually remove this field from the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe it's better to be as close to Playwright as possible in UX terms. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes I agree, this would only be "under the hood". The API spec would stay the same but we wouldn't bother with this field in the other layers of the codebase. |
||
| 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 len(opts.Body) > 0 { | ||
| b64Body := base64.StdEncoding.EncodeToString(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. | ||
| // Most probably this happens when trying to fail a site's background request | ||
| // 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): %w", | ||
| 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. | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking at https://playwright.dev/docs/api/class-route#route-fulfill - you are not handling
path,jsonandresponse. Can we have a comment here. Also I feel like handling all butresponseis likely fairly quick 🤔 .But if not maybe lets throw an exception instead of silently doing the wrong thing
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should be ok to wait for feedback for
responseandjson.Instead of working with
pathwe should probably encourage users to work with the current idiomatic k6 way of reading a file and then passing it the bytes tobody.So, yeah, happy with throwing an error if a user tries to work with those fields that aren't implemented.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated in 67f24be
I agree that
jsonshould be quick to implement and used by a lot of users but wanted to keep the scope of the PR as small as possible. We can definitely iterate on it in future releases.