Skip to content

Commit ab98b49

Browse files
authored
fix: Back out of axios switchover for the createInstance cloud API request (#31486)
* use old create instance method for proxy support * changelog * conform legacy createInstance to the newer signature * link to proxy env issue as todo * changelog * system test snapshots * fix snap
1 parent b743972 commit ab98b49

File tree

6 files changed

+130
-68
lines changed

6 files changed

+130
-68
lines changed

cli/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ _Released 4/22/2025 (PENDING)_
66
**Bugfixes:**
77

88
- The [`cy.press()`](http://on.cypress.io/api/press) command no longer errors when used in specs subsequent to the first spec in run mode. Fixes [#31466](https://github.com/cypress-io/cypress/issues/31466).
9+
- Fixed an issue where certain proxy conditions prevented test runs from being recorded. Fixes [#31485](https://github.com/cypress-io/cypress/issues/31485).
910

1011
**Misc:**
1112

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
/**
2+
* The axios Cloud instance should not be used.
3+
*/
14
import os from 'os'
25

36
import axios, { AxiosInstance } from 'axios'

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,15 @@ import { isAxiosError } from 'axios'
55

66
const MAX_RETRIES = 3
77

8-
interface CreateInstanceResponse {
8+
export interface CreateInstanceResponse {
99
spec: string | null
1010
instanceId: string | null
1111
claimedInstances: number
1212
estimatedWallClockDuration: number | null
1313
totalInstances: number
1414
}
1515

16-
interface CreateInstanceRequestBody {
16+
export interface CreateInstanceRequestBody {
1717
spec: string | null
1818
groupId: string
1919
machineId: string

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

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,22 @@ import type { ProjectBase } from '../../project-base'
2525
import type { AfterSpecDurations } from '@packages/types'
2626
import { PUBLIC_KEY_VERSION } from '../constants'
2727

28-
import { createInstance } from './create_instance'
28+
// axios implementation disabled until proxy issues can be diagnosed/fixed
29+
// TODO: https://github.com/cypress-io/cypress/issues/31490
30+
//import { createInstance } from './create_instance'
31+
import type { CreateInstanceRequestBody, CreateInstanceResponse } from './create_instance'
32+
2933
import { transformError } from './axios_middleware/transform_error'
3034

3135
const THIRTY_SECONDS = humanInterval('30 seconds')
3236
const SIXTY_SECONDS = humanInterval('60 seconds')
3337
const TWO_MINUTES = humanInterval('2 minutes')
3438

35-
const DELAYS: number[] = process.env.API_RETRY_INTERVALS
36-
? process.env.API_RETRY_INTERVALS.split(',').map(_.toNumber)
37-
: [THIRTY_SECONDS, SIXTY_SECONDS, TWO_MINUTES]
39+
function retryDelays (): number[] {
40+
return process.env.API_RETRY_INTERVALS
41+
? process.env.API_RETRY_INTERVALS.split(',').map(_.toNumber)
42+
: [THIRTY_SECONDS, SIXTY_SECONDS, TWO_MINUTES]
43+
}
3844

3945
const runnerCapabilities = {
4046
'dynamicSpecsInSerialMode': true,
@@ -181,16 +187,18 @@ const retryWithBackoff = (fn) => {
181187
throw err.cause
182188
})
183189
.catch(isRetriableError, (err) => {
184-
if (retryIndex >= DELAYS.length) {
190+
const delays = retryDelays()
191+
192+
if (retryIndex >= delays.length) {
185193
throw err
186194
}
187195

188-
const delayMs = DELAYS[retryIndex]
196+
const delayMs = delays[retryIndex]
189197

190198
errors.warning(
191199
'CLOUD_API_RESPONSE_FAILED_RETRYING', {
192200
delayMs,
193-
tries: DELAYS.length - retryIndex,
201+
tries: delays.length - retryIndex,
194202
response: err,
195203
},
196204
)
@@ -443,7 +451,24 @@ export default {
443451
.catch(tagError)
444452
},
445453

446-
createInstance,
454+
createInstance (runId: string, body: CreateInstanceRequestBody, timeout: number = 0): Bluebird<CreateInstanceResponse> {
455+
return retryWithBackoff((attemptIndex) => {
456+
return rp.post({
457+
body,
458+
url: recordRoutes.instances(runId),
459+
json: true,
460+
encrypt: preflightResult.encrypt,
461+
timeout: timeout ?? SIXTY_SECONDS,
462+
headers: {
463+
'x-route-version': '5',
464+
'x-cypress-run-id': runId,
465+
'x-cypress-request-attempt': attemptIndex,
466+
},
467+
})
468+
.catch(RequestErrors.StatusCodeError, transformError)
469+
.catch(tagError)
470+
}) as Bluebird<CreateInstanceResponse>
471+
},
447472

448473
postInstanceTests (options) {
449474
const { instanceId, runId, timeout, ...body } = options
Lines changed: 86 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
import chai from 'chai'
22
import nock from 'nock'
3-
import sinon from 'sinon'
43
import sinonChai from 'sinon-chai'
5-
import os from 'os'
64
import pkg from '@packages/root'
75

8-
import { createInstance } from '../../../../lib/cloud/api/create_instance'
6+
import { createInstance as axiosCreateInstance, CreateInstanceRequestBody, CreateInstanceResponse } from '../../../../lib/cloud/api/create_instance'
7+
import api from '../../../../lib/cloud/api'
98

109
chai.use(sinonChai)
1110

@@ -14,11 +13,14 @@ const { expect } = chai
1413
const API_BASEURL = 'http://localhost:1234'
1514
const OS_PLATFORM = 'linux'
1615

16+
const AXIOS_LABEL = 'axios createInstance'
17+
const REQUEST_LABEL = 'request createInstance'
18+
1719
context('API createInstance', () => {
1820
let nocked
1921
const runId = 'run-id-123'
2022

21-
const instanceRequestData: Parameters<typeof createInstance>[1] = {
23+
const instanceRequestData: CreateInstanceRequestBody = {
2224
spec: null,
2325
groupId: 'groupId123',
2426
machineId: 'machineId123',
@@ -32,7 +34,7 @@ context('API createInstance', () => {
3234
},
3335
}
3436

35-
const instanceResponseData: Awaited<ReturnType<typeof createInstance>> = {
37+
const instanceResponseData: CreateInstanceResponse = {
3638
instanceId: 'instance-id-123',
3739
claimedInstances: 0,
3840
estimatedWallClockDuration: null,
@@ -43,65 +45,96 @@ context('API createInstance', () => {
4345
beforeEach(() => {
4446
nocked = nock(API_BASEURL)
4547
.matchHeader('x-cypress-run-id', runId)
46-
// sinon stubbing on the `os` package doesn't work for `createInstance`
47-
//.matchHeader('x-os-name', OS_PLATFORM)
4848
.matchHeader('x-cypress-version', pkg.version)
4949
.post(`/runs/${runId}/instances`)
5050

51-
sinon.stub(os, 'platform').returns(OS_PLATFORM)
52-
})
53-
54-
afterEach(() => {
55-
(os.platform as sinon.SinonStub).restore()
56-
})
57-
58-
describe('when the request succeeds', () => {
59-
beforeEach(() => {
60-
nocked.reply(200, instanceResponseData)
61-
})
62-
63-
it('returns the created instance', async () => {
64-
const response = await createInstance(runId, instanceRequestData)
65-
66-
for (let k in instanceResponseData) {
67-
expect(instanceResponseData[k]).to.eq(response[k])
68-
}
69-
})
51+
api.setPreflightResult({ encrypt: false })
7052
})
7153

72-
describe('when the request times out 3 times', () => {
73-
const timeout = 100
74-
75-
beforeEach(() => {
76-
nocked
77-
.times(3)
78-
.delayConnection(5000)
79-
.reply(200, instanceResponseData)
80-
})
54+
;[
55+
{
56+
label: AXIOS_LABEL,
57+
fn: axiosCreateInstance,
58+
},
59+
{
60+
label: REQUEST_LABEL,
61+
fn: api.createInstance,
62+
},
63+
].forEach(function ({ label, fn: createInstance }) {
64+
describe(label, function () {
65+
describe('when the request succeeds', () => {
66+
beforeEach(() => {
67+
nocked.reply(200, instanceResponseData)
68+
})
69+
70+
it('returns the created instance', async () => {
71+
const response = await createInstance(runId, instanceRequestData)
72+
73+
for (let k in instanceResponseData) {
74+
expect(instanceResponseData[k]).to.eq(response[k])
75+
}
76+
})
77+
})
8178

82-
it('throws an aggregate error', () => {
83-
return createInstance(runId, instanceRequestData, timeout)
84-
.then(() => {
85-
throw new Error('should have thrown here')
86-
}).catch((err) => {
87-
for (const error of err.errors) {
88-
expect(error.message).to.eq(`timeout of ${timeout}ms exceeded`)
89-
expect(error.isApiError).to.be.true
79+
describe('when the request times out 4 times', () => {
80+
const timeout = 10
81+
let oldIntervals
82+
83+
beforeEach(() => {
84+
oldIntervals = process.env.API_RETRY_INTERVALS
85+
process.env.API_RETRY_INTERVALS = '0,0,0'
86+
nocked
87+
.times(4)
88+
.delayConnection(5000)
89+
.reply(200, instanceResponseData)
90+
})
91+
92+
afterEach(() => {
93+
process.env.API_RETRY_INTERVALS = oldIntervals
94+
})
95+
96+
// axios throws an AggregateError
97+
if (AXIOS_LABEL === label) {
98+
it('throws an aggregate error', () => {
99+
return createInstance(runId, instanceRequestData, timeout)
100+
.then(() => {
101+
throw new Error('should have thrown here')
102+
}).catch((err) => {
103+
for (const error of err.errors) {
104+
expect(error.message).to.eq(`timeout of ${timeout}ms exceeded`)
105+
expect(error.isApiError).to.be.true
106+
}
107+
})
108+
})
109+
// request/promise throws the most recent error
110+
} else {
111+
it('throws a tagged error', async () => {
112+
let thrown: Error | undefined = undefined
113+
114+
try {
115+
await createInstance(runId, instanceRequestData, timeout)
116+
} catch (e) {
117+
thrown = e
118+
}
119+
120+
expect(thrown).not.to.be.undefined
121+
expect((thrown as Error & { isApiError?: boolean }).isApiError).to.be.true
122+
})
90123
}
91124
})
92-
})
93-
})
94125

95-
describe('when the request times out once and then succeeds', () => {
96-
beforeEach(() => {
97-
nocked.delayConnection(5000).reply(200, instanceResponseData)
98-
nocked.delayConnection(0).reply(200, instanceResponseData)
99-
})
126+
describe('when the request times out once and then succeeds', () => {
127+
beforeEach(() => {
128+
nocked.delayConnection(5000).reply(200, instanceResponseData)
129+
nocked.delayConnection(0).reply(200, instanceResponseData)
130+
})
100131

101-
it('returns the instance response data', async () => {
102-
const data = await createInstance(runId, instanceRequestData, 100)
132+
it('returns the instance response data', async () => {
133+
const data = await createInstance(runId, instanceRequestData, 100)
103134

104-
expect(data).to.deep.eq(instanceResponseData)
135+
expect(data).to.deep.eq(instanceResponseData)
136+
})
137+
})
105138
})
106139
})
107140
})

system-tests/__snapshots__/record_spec.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1169,7 +1169,7 @@ exports['e2e record api interaction errors create instance 500 does not proceed
11691169
11701170
We encountered an unexpected error communicating with our servers.
11711171
1172-
Request failed with status code 500
1172+
StatusCodeError: 500 - "Internal Server Error"
11731173
11741174
Because you passed the --parallel flag, this run cannot proceed since it requires a valid response from our servers.
11751175
@@ -1195,7 +1195,7 @@ exports['e2e record api interaction errors create instance 500 without paralleli
11951195
11961196
We encountered an unexpected error communicating with our servers.
11971197
1198-
Request failed with status code 500
1198+
StatusCodeError: 500 - "Internal Server Error"
11991199
12001200
Because you passed the --record flag, this run cannot proceed since it requires a valid response from our servers.
12011201
@@ -1413,7 +1413,7 @@ exports['e2e record api interaction errors create instance errors and exits on c
14131413
14141414
We encountered an unexpected error communicating with our servers.
14151415
1416-
Request failed with status code 500
1416+
StatusCodeError: 500 - "Internal Server Error"
14171417
14181418
Because you passed the --record flag, this run cannot proceed since it requires a valid response from our servers.
14191419
@@ -4420,9 +4420,9 @@ We will retry 1 more time in X second(s)...
44204420
44214421
We encountered an unexpected error communicating with our servers.
44224422
4423-
Request failed with status code 500
4423+
StatusCodeError: 500 - "Internal Server Error"
44244424
4425-
We will retry 2 more times in ...
4425+
We will retry 3 more times in X second(s)...
44264426
44274427
44284428
────────────────────────────────────────────────────────────────────────────────────────────────────

0 commit comments

Comments
 (0)