Skip to content

chore(protocol): make timeout a protocol-level field#41712

Merged
Skn0tt merged 12 commits into
mainfrom
skn0tt-protocol-timeout-field
Jul 14, 2026
Merged

chore(protocol): make timeout a protocol-level field#41712
Skn0tt merged 12 commits into
mainfrom
skn0tt-protocol-timeout-field

Conversation

@Skn0tt

@Skn0tt Skn0tt commented Jul 9, 2026

Copy link
Copy Markdown
Member

Follows #41141, which made signal a protocol-level field. This does the same for timeout: every message carries it on Metadata, computed once on the client and read from progress.timeout on the server, instead of each method declaring its own timeout: float.

Metadata:
  timeout: float  # 👈 0 means "no timeout"

0 is the sentinel for "no deadline", matching what ProgressController.run already does. It's required on sendMessageToServer so the 0 is spelled out where it's genuinely meant, rather than hidden behind a ?? 0.

Most of the diff is mechanical - spec deletions plus regenerated channels/validator, staged separately for review.

Follows #41141, which made signal a protocol-level field. This does the
same for timeout: every message carries it on Metadata, computed once on
the client and read from progress.timeout on the server, instead of each
method declaring its own timeout: float. 0 is the sentinel for no deadline.
@Skn0tt Skn0tt requested a review from dgozman July 9, 2026 14:31
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

…ut-field

# Conflicts:
#	packages/playwright-core/src/client/channels.d.ts
#	packages/playwright-core/src/server/channels.d.ts
#	packages/protocol/src/validator.ts
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

Comment thread utils/generate_channels.js Outdated
addScheme(`${derived}${titleCase(methodName)}ErrorDetails`, `tType('${errorDetailsName}')`);
}
channels_ts.push(` ${methodName}(params: ${paramsName}, ${isDispatcher ? `progress: Progress` : `signal: AbortSignal | undefined`}): Promise<${resultName}>;`);
channels_ts.push(` ${methodName}(params: ${paramsName}, ${isDispatcher ? `progress: Progress` : `options?: CommandOptions`}): Promise<${resultName}>;`);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are options now optional? Let's make them mandatory (like we had with the signal), so every caller thinks about it?


async uncheck(options: channels.ElementHandleUncheckOptions & TimeoutOptions = {}) {
return await this._elementChannel.uncheck({ ...options, timeout: this._frame._timeout(options) }, options.signal);
return await this._elementChannel.uncheck({ ...options }, { signal: options.signal, timeout: this._frame._timeout(options) });

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's make all the helper methods like this._frame._timeout(options) handle both timeout and signal - this way the code will be much easier everywhere.

Comment thread utils/generate_channels.js Outdated
export type { Binary, Channel, ${structNames.join(', ')} } from '@protocol/structs';
`];
` + (target === 'Dispatcher' ? '' : `
export type CommandOptions = { signal?: AbortSignal, timeout?: number };

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make both properties required: { signal: AbortSignal | undefined, timeout: number } to ensure all callers doing the right thing?

Make signal and timeout required fields on the generated CommandOptions
struct, and pass them explicitly at every channel call site. Internal
calls that opt out of a deadline use timeout: 0 as the disabled sentinel.
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@Skn0tt Skn0tt requested a review from dgozman July 13, 2026 12:21
this._timeoutSettings.setDefaultTimeout(timeout);
}

_timeout(options?: types.TimeoutOptions): channels.CommandOptions {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Call this _timeoutOptions() now?
  • Rename channels.CommandOptions to channels.TimeoutOptions?

if (this._connection.isRemote())
throw new Error(`Path is not available when connecting remotely. Use saveAs() to save a local copy.`);
return (await this._channel.pathAfterFinished({}, undefined)).value;
return (await this._channel.pathAfterFinished({}, { signal: undefined, timeout: 0 })).value;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WDYT about exporting a kNoTimeout constant from some common helper, e.g. TimeoutSettings.ts?

};
return await this._wrapApiCall(async () => {
const browser = Browser.from((await this._channel.launch(launchOptions, options.signal)).browser);
const browser = Browser.from((await this._channel.launch(launchOptions, { signal: options.signal, timeout: new TimeoutSettings().launchTimeout(options) })).browser);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TimeoutSettings.launchTimeout might as well handle the signal now.

noDefaults: params.noDefaults,
artifactsDir: params.artifactsDir,
}, undefined);
}, { signal: undefined, timeout: new TimeoutSettings().timeout(params) });

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, let TimeoutSettings.timeout() handle the signal as well.

}

export async function connectToEndpoint(parentConnection: Connection, params: channels.LocalUtilsConnectParams): Promise<Connection> {
export async function connectToEndpoint(parentConnection: Connection, params: channels.LocalUtilsConnectParams, timeout: number): Promise<Connection> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accept TimeoutOptions instead of a number?

Skn0tt and others added 2 commits July 13, 2026 16:57
Addresses review feedback: rename CommandOptions to TimeoutOptions,
add a shared kNoTimeout sentinel, have TimeoutSettings resolvers return
the full { signal, timeout } options, and thread TimeoutOptions through
connectToEndpoint.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 110d728d-5d20-42f6-93b9-e80ab89b3f66
Inline options.signal in the resolvers and rename the loose input alias
to TimeoutInput to distinguish it from the resolved channels.TimeoutOptions.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 110d728d-5d20-42f6-93b9-e80ab89b3f66
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

Skn0tt and others added 5 commits July 14, 2026 08:47
_timeoutOptions was a pure alias for _timeoutSettings.timeout(options);
inline it at the call sites. Frame keeps its helpers since they pick
between the timeout and navigationTimeout resolvers.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 110d728d-5d20-42f6-93b9-e80ab89b3f66
Rename _timeoutOptions/_navigationTimeoutOptions back to
_timeout/_navigationTimeout on Frame and update cross-file callers.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 110d728d-5d20-42f6-93b9-e80ab89b3f66
reload/goBack/goForward/ariaSnapshot resolved their timeout via
this._mainFrame, an unnecessary indirection since Page has its own
_timeoutSettings. Call it directly, matching the pre-existing style.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 110d728d-5d20-42f6-93b9-e80ab89b3f66
Remove the per-dispatch metadataWithDefaults spread (the client always
sends metadata.timeout, no method passes timeout via params, and the
validator produces a fresh validMetadata) and the paramsForMetadata
copy. Timeout is still folded into callMetadata.params for trace/inspector
visibility, now isolated in a clearly-commented block to follow up on.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 110d728d-5d20-42f6-93b9-e80ab89b3f66
Non-JS and raw protocol consumers (e.g. the DebugController test
backend) no longer have to send a timeout just to satisfy the schema.
The JS client still always sends one; a missing timeout falls back to
the disabled sentinel.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 110d728d-5d20-42f6-93b9-e80ab89b3f66
log: [],
};

// TODO(skn0tt): promote to top-level metadata instead of smuggling through params.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'd like to do this in a follow-up so this one doesn't balloon

@Skn0tt Skn0tt requested a review from dgozman July 14, 2026 07:27
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

Comment thread packages/playwright-core/src/client/electron.ts Outdated
Comment thread packages/playwright-core/src/client/elementHandle.ts Outdated
Comment thread packages/playwright-core/src/client/fetch.ts Outdated
waiter.rejectOnEvent<Frame>(this._page!, Events.Page.FrameDetached, new Error('Navigating frame was detached!'), frame => frame === this);
const timeout = this._page!._timeoutSettings.navigationTimeout(options);
const { timeout } = this._page!._timeoutSettings.navigationTimeout(options);
waiter.rejectOnTimeout(timeout, `Timeout ${timeout}ms exceeded.`);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a follow up, we can merge rejectOnTimeout and rejectOnSignal.

Comment thread packages/playwright-core/src/client/frame.ts Outdated
Comment thread packages/playwright-core/src/client/frame.ts Outdated
async _expect(expression: string, options: Omit<channels.FrameExpectParams, 'expression'>, signal: AbortSignal | undefined): Promise<ExpectResult> {
const params: channels.FrameExpectParams = { expression, ...options, isNot: !!options.isNot };
params.expectedValue = serializeArgument(options.expectedValue);
async _expect(expression: string, options: Omit<channels.FrameExpectParams, 'expression'> & { timeout: number }, signal: AbortSignal | undefined): Promise<ExpectResult> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a follow up, either put both signal and timeout into options, or put them both into a separate TimeoutOptions argument.

Comment thread packages/playwright-core/src/client/page.ts Outdated
}

async _expectScreenshot(options: ExpectScreenshotOptions): Promise<{ actual?: Buffer, previous?: Buffer, diff?: Buffer, errorMessage?: string, log?: string[], timedOut?: boolean}> {
const { timeout, ...optionsWithoutTimeout } = options;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow up with supporting the signal here?

- unify signal into the timeout() destructure across waitForEvent methods
- pass _timeout()/timeout() directly for screenshots
- use kNoTimeout sentinel for isHidden/isVisible/newContext

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 110d728d-5d20-42f6-93b9-e80ab89b3f66
@github-actions

Copy link
Copy Markdown
Contributor

Test results for "MCP"

1 failed
❌ [firefox] › mcp/annotate.spec.ts:447 › should switch screencast to -s session on show --annotate @mcp-macos-latest-firefox

7759 passed, 1249 skipped


Merge workflow run.

@github-actions

Copy link
Copy Markdown
Contributor

Test results for "tests 1"

4 flaky ⚠️ [chromium-library] › library/video.spec.ts:645 › screencast › should capture full viewport `@chromium-ubuntu-22.04-arm-node20`
⚠️ [chromium-library] › library/chromium/oopif.spec.ts:282 › should click `@chromium-ubuntu-22.04-node24`
⚠️ [chromium-library] › library/beforeunload.spec.ts:130 › should support dismissing the dialog multiple times `@chromium-ubuntu-22.04-node20`
⚠️ [chromium-library] › library/video.spec.ts:717 › screencast › should work with video+trace `@chromium-ubuntu-22.04-node22`

49579 passed, 1156 skipped


Merge workflow run.

@Skn0tt Skn0tt merged commit 33173da into main Jul 14, 2026
47 of 48 checks passed
@Skn0tt Skn0tt deleted the skn0tt-protocol-timeout-field branch July 14, 2026 13:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants