grafana/k6 · error

fail to fulfill request (id: %s): %w

Error message

fail to fulfill request (id: %s): %w

What it means

The CDP command Fetch.fulfillRequest failed while delivering a mocked response to an intercepted request. context.Canceled is already swallowed; any other CDP failure — stale interception ID, target destroyed mid-fulfill, malformed response rejected by chromium — is returned here.

Source

Thrown at internal/js/modules/k6/browser/common/network_manager.go:1013

	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")
			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,
		}
	}

View on GitHub (pinned to 93accf6570)

Solutions

  1. Fulfill synchronously and exactly once per intercepted request
  2. Ensure fulfill() happens while the interception is still valid: before navigation completes or unroute() is called
  3. Keep header values ASCII-safe and the body a valid string/Buffer; avoid huge bodies that race the target lifetime
  4. Scope route patterns narrowly so only one handler matches

Example fix

// before
page.route('**/login', async r => {
  await sleep(2000);
  await r.fulfill({ status: 200, body: 'ok' }); // page may have navigated on
});

// after
page.route('**/login', r => r.fulfill({ status: 200, body: 'ok' })); // immediate
Defensive patterns

Strategy: try-catch

Validate before calling

if (page.isClosed()) { /* skip fulfilling */ }

Try / catch

try {
  await route.fulfill({ status: 200, body: 'ok' });
} catch (e) {
  if (/fail to fulfill request/.test(String(e))) return; // stale interception
  throw e;
}

Prevention

When it happens

Trigger: route.fulfill({status, body, headers}) executed after navigation destroyed the request, after unroute() disabled Fetch, or fulfilling the same route twice. Also when the composed response is rejected by the browser (e.g. invalid header values or content that breaks CDP framing) at the protocol level.

Common situations: Mocking API responses in handlers that await before fulfilling; tests where the page navigates immediately after a click while the handler is still mocking; double handlers for the same URL pattern (two page.route calls both matching).

Related errors


AI-assisted analysis of grafana/k6@93accf6570 (2026-08-15). Data as JSON: /api/errors/0cb54d7d86364443. Report an issue: GitHub.