I am trying to make the following XHR
request retry if it times out. If I remove the cy.wait('@formRequest')
, it seems like the test doesn't stop to wait for the response properly. But if I leave the cy.wait
in, the test will fail if the request times out. So I tried to add some callbacks (ontimeout) to the cy.request
call (shown in the second code block), but that doesn't seem to work either. Any idea how I can make this cypress command retry if the response is not 200 OR if a response is not received at all??
I'm pretty new to this, so some explanation would be greatly appreciated.
Thanks in advance!
// this works, but doesn't handle timeout
Cypress.Commands.add('form_request', (url, formData) => {
function recursive_request (url, formData, attempt) {
let xhr
const maxRetry = 3
if (attempt > maxRetry) {
throw new Error(`form_request() max retry reached, ${maxRetry}`)
}
if (attempt !== 0) {
cy.log(`Backing off retry. Attempt: ${attempt}/${maxRetry}.`)
cy.wait(30000)
}
return cy
.server()
.route('POST', url)
.as('formRequest')
.window()
.then((win) => {
xhr = new win.XMLHttpRequest()
xhr.open('POST', url)
xhr.send(formData)
})
.wait('@formRequest')
.then((resp) => {
if (xhr.status !== 200) {
return recursive_request(url, formData, attempt + 1)
}
return resp
})
}
return recursive_request(url, formData, 0)
})
Cypress.Commands.add('form_request', (url, formData) => {
function recursive_request (url, formData, attempt) {
let xhr
const maxRetry = 3
if (attempt > maxRetry) {
throw new Error(`form_request() max retry reached, ${maxRetry}`)
}
if (attempt !== 0) {
cy.log(`Backing off retry. Attempt: ${attempt}/${maxRetry}.`)
cy.wait(30000)
}
return cy
.server()
// .route('POST', url)
.route({
'method': 'POST',
'url': url
}, {
ontimeout: (xhr) => {
return new Cypress.Promise ((resolve) => {
resolve(recursive_request(url, formData, attempt + 1))
})
},
onreadystatechange: (xhr) => {
if (xhr.status !== 200 && xhr.readyState === 4) {
return new Cypress.Promise ((resolve) => {
resolve(recursive_request(url, formData, attempt + 1))
})
}
}
})
.as('formRequest')
.window()
.then((win) => {
xhr = new win.XMLHttpRequest()
xhr.open('POST', url)
xhr.send(formData)
})
.wait('@formRequest')
.then((resp) => {
if (xhr.status !== 200) {
return recursive_request(url, formData, attempt + 1)
}
return resp
})
}
return recursive_request(url, formData, 0)
})
edit: I tried to make the following custom cypress command. However, I am getting an illegal invocation error. Looking through the documentation on jQuery.ajax(), it's not clear to me what is illegal about it.
I also have a couple questions...
#1. how does passing in the {timeout: 30000} object work? What is this called? My basic understanding of promise chaining is that you pass in some handler function to use whatever the previous call yielded. I have not seen the pattern .then(arg, function () {...})
before, what is this called?
#2. will cy.log(...) work here? I suspect that it won't because that would be mixing sync/async code, correct? (sorry, I would play with it myself right now but I can't get it to run past the illegal invocation error). Would const log = Cypress.log(...)
work instead synchronously here?
527 | }
528 |
> 529 | Cypress.$.ajax({
| ^
530 | url,
531 | method: 'POST',
532 | data: formData,
Cypress.Commands.add('form_request', (url, formData) => {
const waits = { request: 2000, retry: 10000 }
const maxRetry = 3
function recursive_request(url, formData, attempt = 0) {
return new Cypress.Promise((resolve) => {
if (attempt > maxRetry) {
throw new Error(`form_request() max retry reached, ${maxRetry}`)
}
const retry = (reason) => {
console.log(`${reason} - retrying in ${waits.retry}`) // #2
return setTimeout(() => {
recursive_request(url, formData, attempt + 1)
.then(data => resolve(data)); // resolve from recursive result
}, waits.retry)
}
Cypress.$.ajax({
url,
method: 'POST',
data: formData,
timeout: waits.request,
})
.done(function (data, textStatus, jqXHR) {
if (textStatus !== 'success') {
retry('Status not success')
}
resolve(data)
})
.catch(function (jqXHR, textStatus, errorThrown) {
retry(errorThrown)
})
})
}
cy.wrap(null)
.then({ timeout: 30000 }, // #1
() => {
return recursive_request(url, formData)
})
.then((result) => {
return result
})
})
Test-level retries
I would take a look at test-retries (Cypress v 5.0+) to handle the timeout issue,
const waitTime = 1000; // shortened for demo
Cypress.Commands.add('form_request', (url, formData) => {
return cy
.request({
method: 'POST',
url,
body: formData,
timeout: waitTime
})
.then((resp) => {
if (resp.status !== 200) {
throw new Error(`Status not 200`)
}
return resp
})
})
let attempts = 0;
it('repeats', { retries: 3 }, () => {
const wait = attempts ? waitTime : 0; // initial wait = 0
const url = attempts < 2
? 'http://nothing' // initially use a non-reachable url
: 'http://example.com'; // 3rd attempt succeeds
attempts++;
cy.wait(wait).form_request(url, { name: 'Fred' })
})
Request-level retries
If you want to retry at the request level (not the test level), then @Ackroydd is correct - you should ditch the cy.route()
because, in short, Cypress commands are not built to catch fails.
Here is a basic example using jquery.ajax
. You may get a more succinct function using something like axios.retry.
Refer to Waiting for promises for the Cypress pattern used.
Recursive request
const waits = { request: 2000, retry: 10000 }
const maxRetry = 3
function recursive_request(url, formData, attempt = 0) {
return new Cypress.Promise ((resolve) => {
if (attempt > maxRetry) {
throw new Error(`form_request() max retry reached, ${maxRetry}`)
}
const retry = (reason) => {
console.log(`${reason} - retrying in ${waits.retry}`);
return setTimeout(() => {
recursive_request(url, formData, attempt + 1)
.then(data => resolve(data)); // resolve from recursive result
}, waits.retry)
}
Cypress.$.ajax({
url,
method: 'POST',
data: formData,
timeout: waits.request,
})
.done(function(data, textStatus, jqXHR) {
if (textStatus !== 'success') {
retry('Status not success')
}
resolve(data)
})
.catch(function(jqXHR, textStatus, errorThrown) {
retry(errorThrown)
})
})
}
Test
it('retries form post', () => {
cy.wrap(null)
.then(
{ timeout: 30000 }, // timeout must be above maximum expected,
// i.e maxRetry * waits.retry
() => recursive_request('http://localhost:3000', { name: 'Fred' })
)
.then(result => console.log(result))
})
Flaky server used to test
const express = require("express");
const app = express();
const port = 3000;
const obj = {
name: 'Fred'
};
let attempt = 0;
app.post("*", (req, res) => {
if (attempt >= 3) {
attempt = 0; // reset to allow multiple test runs
res.status(201).json(obj)
}
});
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`);
});