Skip to content

Commit 2c806e4

Browse files
internal: (cy.prompt) do not retry on cert failures (#32624)
* internal: (cy.prompt) do not retry on cert failures * tests and clean up * additional cleanup and add error message for prompt * fix unit tests * Update report_studio_error.ts * Update packages/network/lib/agent.ts * cursor comment * fix build * fix build * fix test * fix build * fix build * fix build * fix build * fix build * revert * Update packages/driver/src/cypress/error_messages.ts * Update packages/driver/cypress/e2e/commands/prompt/prompt-initialization-error.cy.ts * fix build
1 parent 3bf8993 commit 2c806e4

File tree

16 files changed

+155
-98
lines changed

16 files changed

+155
-98
lines changed

packages/driver/cypress/e2e/commands/prompt/prompt-initialization-error.cy.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,25 @@ describe('src/cy/commands/prompt', () => {
3434
cy['commandFns']['prompt'].__resetPrompt()
3535
cy.prompt(['Hello, world!'])
3636
})
37+
38+
it('errors with a proxy error', (done) => {
39+
const backendStub = cy.stub(Cypress, 'backend').log(false)
40+
41+
const error = new Error('UNABLE_TO_VERIFY_LEAF_SIGNATURE')
42+
43+
;(error as any).code = 'UNABLE_TO_VERIFY_LEAF_SIGNATURE'
44+
45+
backendStub.callThrough()
46+
backendStub.withArgs('wait:for:prompt:ready').resolves({ success: false, error })
47+
48+
cy.on('fail', (err) => {
49+
expect(err.message).to.include('`cy.prompt` requires an internet connection to work. To continue, you may need to configure Cypress with your proxy settings.')
50+
done()
51+
})
52+
53+
cy.visit('http://www.foobar.com:3500/fixtures/dom.html')
54+
55+
cy['commandFns']['prompt'].__resetPrompt()
56+
cy.prompt(['Hello, world!'])
57+
})
3758
})

packages/driver/src/cy/commands/prompt/index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { CypressInternal, CyPromptDriverDefaultShape, CyPromptMoreInfoNeede
33
import type Emitter from 'component-emitter'
44
import $errUtils from '../../../cypress/error_utils'
55
import $stackUtils from '../../../cypress/stack_utils'
6+
import { isNonRetriableCertErrorCode } from '@packages/server/lib/cloud/network/non_retriable_cert_error_codes'
67

78
interface CyPromptDriver { default: CyPromptDriverDefaultShape }
89

@@ -39,6 +40,14 @@ const initializeModule = async (Cypress: Cypress.Cypress): Promise<CyPromptDrive
3940
const { success, error } = await Cypress.backend('wait:for:prompt:ready')
4041

4142
if (error) {
43+
if ('code' in error && isNonRetriableCertErrorCode(error.code as string)) {
44+
$errUtils.throwErrByPath('prompt.promptProxyError', {
45+
args: {
46+
error,
47+
},
48+
})
49+
}
50+
4251
$errUtils.throwErrByPath('prompt.promptDownloadError', {
4352
args: {
4453
error,

packages/driver/src/cypress/error_messages.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1354,6 +1354,12 @@ export default {
13541354
docsUrl: 'https://on.cypress.io/prompt-download-error',
13551355
}
13561356
},
1357+
promptProxyError: {
1358+
message: stripIndent`\
1359+
\`cy.prompt\` requires an internet connection to work. To continue, you may need to configure Cypress with your proxy settings.
1360+
`,
1361+
docsUrl: 'https://on.cypress.io/proxy-configuration',
1362+
},
13571363
promptTestingTypeError: stripIndent`\
13581364
\`cy.prompt\` is currently only supported in end-to-end tests.
13591365
`,

packages/server/lib/cloud/api/cy-prompt/get_cy_prompt_bundle.ts

Lines changed: 48 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ import { asyncRetry, linearDelay } from '../../../util/async_retry'
22
import { isRetryableError } from '../../network/is_retryable_error'
33
import fetch from 'cross-fetch'
44
import os from 'os'
5-
import { agent } from '@packages/network'
5+
import { strictAgent } from '@packages/network'
66
import { PUBLIC_KEY_VERSION } from '../../constants'
77
import { createWriteStream } from 'fs'
88
import { verifySignatureFromFile } from '../../encryption'
9+
import { HttpError } from '../../network/http_error'
10+
import { SystemError } from '../../network/system_error'
911

1012
const pkg = require('@packages/root')
1113
const _delay = linearDelay(500)
@@ -15,39 +17,56 @@ export const getCyPromptBundle = async ({ cyPromptUrl, projectId, bundlePath }:
1517
let responseManifestSignature: string | null = null
1618

1719
await (asyncRetry(async () => {
18-
const response = await fetch(cyPromptUrl, {
19-
// @ts-expect-error - this is supported
20-
agent,
21-
method: 'GET',
22-
headers: {
23-
'x-route-version': '1',
24-
'x-cypress-signature': PUBLIC_KEY_VERSION,
25-
...(projectId ? { 'x-cypress-project-slug': projectId } : {}),
26-
'x-cypress-cy-prompt-mount-version': '1',
27-
'x-os-name': os.platform(),
28-
'x-cypress-version': pkg.version,
29-
},
30-
encrypt: 'signed',
31-
})
32-
33-
if (!response.ok) {
34-
throw new Error(`Failed to download cy-prompt bundle: ${response.statusText}`)
35-
}
20+
try {
21+
const response = await fetch(cyPromptUrl, {
22+
// @ts-expect-error - this is supported
23+
agent: strictAgent,
24+
method: 'GET',
25+
headers: {
26+
'x-route-version': '1',
27+
'x-cypress-signature': PUBLIC_KEY_VERSION,
28+
...(projectId ? { 'x-cypress-project-slug': projectId } : {}),
29+
'x-cypress-cy-prompt-mount-version': '1',
30+
'x-os-name': os.platform(),
31+
'x-cypress-version': pkg.version,
32+
},
33+
encrypt: 'signed',
34+
})
35+
36+
if (!response.ok) {
37+
const err = await HttpError.fromResponse(response)
38+
39+
throw err
40+
}
3641

37-
responseSignature = response.headers.get('x-cypress-signature')
38-
responseManifestSignature = response.headers.get('x-cypress-manifest-signature')
42+
responseSignature = response.headers.get('x-cypress-signature')
43+
responseManifestSignature = response.headers.get('x-cypress-manifest-signature')
3944

40-
await new Promise<void>((resolve, reject) => {
41-
const writeStream = createWriteStream(bundlePath)
45+
await new Promise<void>((resolve, reject) => {
46+
const writeStream = createWriteStream(bundlePath)
4247

43-
writeStream.on('error', reject)
44-
writeStream.on('finish', () => {
45-
resolve()
48+
writeStream.on('error', reject)
49+
writeStream.on('finish', () => {
50+
resolve()
51+
})
52+
53+
// @ts-expect-error - this is supported
54+
response.body?.pipe(writeStream)
4655
})
56+
} catch (error) {
57+
if (HttpError.isHttpError(error)) {
58+
throw error
59+
}
60+
61+
if (error.errno || error.code) {
62+
const sysError = new SystemError(error, cyPromptUrl, error.code, error.errno)
4763

48-
// @ts-expect-error - this is supported
49-
response.body?.pipe(writeStream)
50-
})
64+
sysError.stack = error.stack
65+
throw sysError
66+
}
67+
68+
throw error
69+
}
5170
}, {
5271
maxAttempts: 3,
5372
retryDelay: _delay,

packages/server/lib/cloud/api/cy-prompt/post_cy_prompt_session.ts

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import { asyncRetry, linearDelay } from '../../../util/async_retry'
22
import { isRetryableError } from '../../network/is_retryable_error'
3-
import fetch from 'cross-fetch'
43
import os from 'os'
5-
import { agent } from '@packages/network'
4+
import { ParseKinds, postFetch } from '../../network/fetch'
65

76
const pkg = require('@packages/root')
87
const routes = require('../../routes') as typeof import('../../routes')
@@ -14,28 +13,16 @@ interface PostCyPromptSessionOptions {
1413
const _delay = linearDelay(500)
1514

1615
export const postCyPromptSession = async ({ projectId }: PostCyPromptSessionOptions) => {
17-
return await (asyncRetry(async () => {
18-
const response = await fetch(routes.apiRoutes.cyPromptSession(), {
19-
// @ts-expect-error - this is supported
20-
agent,
21-
method: 'POST',
16+
return await (asyncRetry(() => {
17+
return postFetch<{ cyPromptUrl: string }>(routes.apiRoutes.cyPromptSession(), {
18+
parse: ParseKinds.JSON,
2219
headers: {
2320
'Content-Type': 'application/json',
2421
'x-os-name': os.platform(),
2522
'x-cypress-version': pkg.version,
2623
},
2724
body: JSON.stringify({ projectSlug: projectId, cyPromptMountVersion: 1 }),
2825
})
29-
30-
if (!response.ok) {
31-
throw new Error(`Failed to create cy-prompt session: ${response.statusText}`)
32-
}
33-
34-
const data = await response.json()
35-
36-
return {
37-
cyPromptUrl: data.cyPromptUrl,
38-
}
3926
}, {
4027
maxAttempts: 3,
4128
retryDelay: _delay,

packages/server/lib/cloud/api/cy-prompt/report_cy_prompt_error.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ interface CyPromptError {
1717
name: string
1818
stack: string
1919
message: string
20+
code?: string | number
21+
errno?: string | number
2022
cyPromptMethod: string
2123
cyPromptMethodArgs?: string
2224
}
@@ -82,6 +84,8 @@ export function reportCyPromptError ({
8284
name: stripPath(errorObject.name ?? `Unknown name`),
8385
stack: stripPath(errorObject.stack ?? `Unknown stack`),
8486
message: stripPath(errorObject.message ?? `Unknown message`),
87+
code: 'code' in errorObject ? errorObject.code as string : undefined,
88+
errno: 'errno' in errorObject ? errorObject.errno as number : undefined,
8589
cyPromptMethod,
8690
cyPromptMethodArgs: cyPromptMethodArgsString ? stripPath(cyPromptMethodArgsString) : undefined,
8791
}],

packages/server/lib/cloud/api/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ import type { CreateInstanceRequestBody, CreateInstanceResponse } from './create
3737

3838
import { transformError } from './axios_middleware/transform_error'
3939
import { DecryptionError } from './cloud_request_errors'
40-
import { isNonRetriableCertErrorCode } from '../network/nonretriable_cert_error_codes'
40+
import { isNonRetriableCertErrorCode } from '../network/non_retriable_cert_error_codes'
4141

4242
const THIRTY_SECONDS = humanInterval('30 seconds')
4343
const SIXTY_SECONDS = humanInterval('60 seconds')

packages/server/lib/cloud/api/studio/post_studio_session.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ interface PostStudioSessionOptions {
1313
const _delay = linearDelay(500)
1414

1515
export const postStudioSession = async ({ projectId }: PostStudioSessionOptions) => {
16-
return await (asyncRetry(async () => {
16+
return await (asyncRetry(() => {
1717
return postFetch<{ studioUrl: string, protocolUrl: string }>(routes.apiRoutes.studioSession(), {
1818
parse: ParseKinds.JSON,
1919
headers: {

packages/server/lib/cloud/network/is_retryable_error.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { SystemError } from './system_error'
22
import { HttpError } from './http_error'
33
import Debug from 'debug'
4-
import { isNonRetriableCertErrorCode } from './nonretriable_cert_error_codes'
4+
import { isNonRetriableCertErrorCode } from './non_retriable_cert_error_codes'
55

66
const debug = Debug('cypress-verbose:server:is-retryable-error')
77

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
export const NON_RETRIABLE_CERT_ERROR_CODES = Object.freeze({
2+
// The leaf certificate signature can’t be verified
3+
UNABLE_TO_VERIFY_LEAF_SIGNATURE: 'UNABLE_TO_VERIFY_LEAF_SIGNATURE',
4+
// The certificate is a self-signed certificate and not in trusted root store
5+
DEPTH_ZERO_SELF_SIGNED_CERT: 'DEPTH_ZERO_SELF_SIGNED_CERT',
6+
// A self-signed certificate exists somewhere in the chain
7+
SELF_SIGNED_CERT_IN_CHAIN: 'SELF_SIGNED_CERT_IN_CHAIN',
8+
// The issuer certificate is not available locally
9+
UNABLE_TO_GET_ISSUER_CERT_LOCALLY: 'UNABLE_TO_GET_ISSUER_CERT_LOCALLY',
10+
})
11+
12+
type NonRetriableCertErrorCode = typeof NON_RETRIABLE_CERT_ERROR_CODES[keyof typeof NON_RETRIABLE_CERT_ERROR_CODES]
13+
14+
export const isNonRetriableCertErrorCode = (errorCode: string | number): errorCode is NonRetriableCertErrorCode => {
15+
return Object.values(NON_RETRIABLE_CERT_ERROR_CODES).includes(errorCode as NonRetriableCertErrorCode)
16+
}

0 commit comments

Comments
 (0)