How to match intercept on response - cypress

Here is an example of code
The first thing I do is intercept the request, then I want to wait until response will contain expected status in body. But the test is failing after default timeout - 30000ms with the error like this:
Timed out retrying after 30000ms: expected 'Running' to equal 'Completed'
So the test is failing because status is Running, and the expected is Completed. how to increase timeout in this case?
cy.intercept('GET', Cypress.config().baseUrl + 'api/scans/' + scanID).as('getStatus');
cy.visit('/')
cy.wait('#getStatus', {responseTimeout: 80000}).its('response.body.status')
.should('eq', 'Completed')

Continuing from Hiram's answer, if you don't know how many requests to wait for you can use a function to repeat the wait until the desired status arrives.
Of course, the "Completed" status may never arrive so you want to set a maximum number of request/responses to check (in lieu of the timeout).
To simulate your situation, I used a simple web site that makes three requests in 300ms intervals.
The response body has an id which happens to be the same as the parameter in the request, so we want to wait for response.body.id === 3.
Simple app with 3 requests
<script>
setTimeout(() => fetch('https://jsonplaceholder.typicode.com/todos/1'), 300)
setTimeout(() => fetch('https://jsonplaceholder.typicode.com/todos/2'), 600)
setTimeout(() => fetch('https://jsonplaceholder.typicode.com/todos/3'), 900)
</script>
Test
it('waits for a particular response', () => {
cy.visit('../app/intercept-last.html')
cy.intercept('**/jsonplaceholder.typicode.com/todos/*')
.as('todos')
function waitFor(alias, partialResponse, maxRequests, level = 0) {
if (level === maxRequests) {
throw `${maxRequests} requests exceeded` // fail the test
}
cy.wait(alias).then(interception => {
const isMatch = Cypress._.isMatch(interception.response, partialResponse)
if (!isMatch) {
waitFor(alias, partialResponse, maxRequests, level+1)
}
})
}
waitFor('#todos', { body: { id: 3 } }, 100) // if no error, value has arrived
cy.get('#todos.last') // now use get() instead of wait()
.its('response.body.id').should('eq', 3)
})
Your test
cy.intercept('GET', Cypress.config().baseUrl + 'api/scans/' + scanID)
.as('getStatus')
cy.visit('/')
function waitFor(alias, partialResponse, maxRequests, level = 0) {
if (level === maxRequests) {
throw `${maxRequests} requests exceeded`
}
cy.wait(alias).then(interception => {
const isMatch = Cypress._.isMatch(interception.response, partialResponse)
if (!isMatch) {
waitFor(alias, partialResponse, maxRequests, level+1)
}
})
}
const timeout = 80_000 // timeout you want to apply
const pollingRate = 500 // rate at which app sends requests
const maxRequests = (timeout / pollingRate) + 10; // number of polls plus a few more
waitFor('#getStatus', { body: { status: 'Completed' } }, maxRequests)
cy.get('#getStatus.last')
.its('response.body.status').should('eq', 'Completed')

Another solution for the same need with a polling process :
cy.intercept('GET', Cypress.config().baseUrl + 'api/scans/' + scanID).as('polling');
const waitPolling = (res) => {
const {response: {body: {status }}} = res;
if (status !== 'Completed') {
cy.wait('#polling').then(waitPolling);
}
}
cy.wait('#polling').then(waitPolling);

The response timeout isn't ignored, a response is getting caught within the timeout you gave, but it has a different property to the one you expected.
I suppose since the status can be 'Running' or 'Completed' that two responses are coming through. (Maybe also two requests)
I suggest you wait twice
cy.wait('#getStatus', {responseTimeout: 80000})
.its('response.body.status').should('eq', 'Running')
cy.wait('#getStatus', {responseTimeout: 80000})
.its('response.body.status').should('eq', 'Completed')
If no luck with that, take a look at what is happening on the network tab.

Related

Rxjs, retry 3 times, wait 30 seconds, then repeat

I'm using rxjs to connect to a WebSocket service, and in case of failure, I want to retry 3 times, wait 30 seconds, then repeat infinitely, how can I do this?
I found a solution, first, create the following operator:
function retryWithDelay<T>(
repetitions: number,
delay: number
): (a: Observable<T>) => Observable<T> {
let count = repetitions;
return (source$: Observable<T>) =>
source$.pipe(
retryWhen((errors) =>
errors.pipe(
delayWhen(() => {
count--;
if (count === 0) {
count = repetitions;
return timer(delay);
}
return timer(0);
})
)
)
);
}
Then, use use it like this:
function connect(url: string) {
return webSocket({ url })
.pipe(retryWithDelay(3, 30000));
}
You can do this by doing the following:
//emit value every 200ms
const source = Rx.Observable.interval(200);
//output the observable
const example = source
.map(val => {
if (val > 5) {
throw new Error('The request failed somehow.');
}
return val;
})
.retryWhen(errors => errors
//log error message
.do(val => console.log(`Some error that occur ${val}, pauze for 30 seconds.`))
//restart in 30 seconds
.delayWhen(val => Rx.Observable.timer(30 * 1000))
);
const subscribe = example
.subscribe({
next: val => console.log(val),
error: val => console.log(`This will never happen`)
});
See the working example: https://jsbin.com/goxowiburi/edit?js,console
Be aware that this is an infinite loop and you are not introducing unintended consequences into your code.

RxJS: How to loop and handle multiple http call

Im using NestJS. I want to get all data from paginated API (i dont know the total page). Right now im using while loop to get all the data until the API returns 204 No Content, this is my code so far:
async getProduct() {
let productFinal: ProductItem[] = [];
let products: ProductItem[] = [];
let offset = 1;
let state = COLLECTING_STATE.InProgress;
let retryCount = 1;
do {
const path = `product?limit=50&offset=${offset}`;
products = await this.httpService
.get(path, { headers, validateStatus: null })
.pipe(
concatMap((response) => {
// if the statusCode is "204", the loop is complete
if (response.status === 204) {
state = COLLECTING_STATE.Finish;
}
// check if the response is error
if (response.status < 200 || response.status >= 300) {
// log error
Logger.error(
`[ERROR] Error collecting product on offset: ${offset}. StatusCode: ${
response.status
}. Error: ${JSON.stringify(response.data)}. Retrying... (${retryCount})`,
undefined,
'Collect Product'
);
// increment the retryCount
retryCount++;
// return throwError to trigger retry event
return throwError(`[ERROR] Received status ${response.status} from HTTP call`);
}
// return the data if OK
return of(response.data.item);
}),
catchError((err) => {
if (err?.code) {
// log error
Logger.error(
`Connection error: ${err?.code}. Retrying... (${retryCount})`,
undefined,
'Collect Product'
);
// increment the retryCount
retryCount++;
}
return throwError(err);
}),
// retry three times
retry(3),
// if still error, then stop the loop
catchError((err) => {
Logger.error(
`[ERROR] End retrying. Error: ${err?.code ?? err}`,
undefined,
'Collect Product'
);
state = COLLECTING_STATE.Finish;
return of(err);
})
)
.toPromise();
// set retryCount to 1 again
retryCount = 1;
// check if products is defined
if (products?.length > 0) {
// if so, push the product to final variable
productFinal = union(products, productFinal);
}
// increment the offset
offset++;
// and loop while the state is not finish
} while ((state as COLLECTING_STATE) !== COLLECTING_STATE.Finish);
return productFinal;
}
The endpoint product?limit=50&offset=${offset} is from third-party service, it doesn't have one endpoint to grab all the data so this is the only way, it has a maximum limit of 50 per offset, and it didn't have a nextPage or totalPage information on the response so i have to make offset variable and increment it after the previous request is complete.
How do I replace the while loop with the RxJS operator? And can it be optimized to make more than one request at a time (maybe four or five), thus taking less time to get all data?
Based on answer from RxJS Observable Pagination, but increment offset every time request is made:
const { of, timer, defer, EMPTY, from, concat } = rxjs; // = require("rxjs")
const { map, tap, mapTo, mergeMap, take } = rxjs.operators; // = require("rxjs/operators")
// simulate network request
function fetchPage({ limit, offset }) {
// 204 resposne
if (offset > 20) {
return of({ status: 204, items: null });
}
// regular data response
return timer(100).pipe(
tap(() =>
console.log(`-> fetched elements from ${offset} to ${offset+limit}`)
),
mapTo({
status: 200,
items: Array.from({ length: limit }).map((_, i) => offset + i)
})
);
}
const limit = 10;
function getItems(offset = 0) {
return defer(() => fetchPage({ limit, offset })).pipe(
mergeMap(({ status, items }) => {
if (status === 204) {
return EMPTY;
}
const items$ = from(items);
const next$ = getItems(offset + limit);
return concat(items$, next$);
})
);
}
// process only first 100 items, without fetching all of the data
getItems()
.pipe(take(100))
.subscribe({
next: console.log,
error: console.error,
complete: () => console.log("complete")
});
<script src="https://unpkg.com/rxjs#6.6.2/bundles/rxjs.umd.min.js"></script>
Regarding possible optimization to make parallel requests - I don't think it will work well. Instead you could show data progressively, as soon as items are loading. Or change API as was suggested in the comments.

Recursive function with timeout and check RxJs Observable value polling

I have the recursive function: repeatAlert that is called again if data.answered === null:
....
Edit
this.repeatAlert(id).subscribe( val => console.log(val));
console.log('1stCall Alert: ', new Date().getMinutes());
....
find(id: number): Observable<any> {
return this.http.get(`${this.resourceUrl}ByAlertId/${id}`
}
repeatAlert(id: number) {
this.find(id).subscribe((data: AlertInt) => {
if (data.answered === null ) {
this.sendNotification('Alert ', data.text);
console.log('Call Alert: ', new Date().getMinutes(), data.id);
setTimeout(() => {
if (data.answered === null) {
this.repeatAlert(id);
}
}, data.repeating * 1000 * 60);
}
});
}
When I change the value of data.answered in the database, I can't read with this observable find(id) the change of data.answered. So it keeps calling repeatAlert forever ...
What am I doing wrong?
Extra question: Is it better a loop or recursive function ?
You are doing polling. I suggest something like following:
find(id: number): Observable<any> {
return this.http.get(`${this.resourceUrl}ByAlertId/${id}`;
}
repeatAlert(id: number) {
// request data from the endpoint and execute necessary code
const data$ = this.find(id).pipe(
tap(data => {
if (data.answered === null) {
this.sendNotification('Alert ', data.text);
}
})
);
// polling will start if on the first try we don't have data.answered
const startPolling = (duration: number) => timer(duration, duration).pipe(
//take(10), // let's say we want to stop after 10 tries
concatMap(() => data$),
takeWhile(data => data.answered === null), // when to stop polling
);
// if data.answered is null on the first try switch to polling otherwise end
return data$.pipe(
switchMap(data => data.answered === null ?
startPolling(data.repeating * 1000 * 60) :
of(data)
),
);
}
Also note that I changed your repeatAlert, it's better to return Observable from the method and subscribe yourself to avoid memory leaks. You should subscribe and unsubscribe yourself. Also, I suggest you to use take(10) for example so that polling doesn't continue indefinitely, it's up to you.
timer(dueTime, period) works like this: It will emit first event after dueTime and continue emitting events after every period.
Edit takeWhile condition is true and not condition is false
I turns out that this code is also working
repeatAlert(id: number) {
this.alertServ.find(id).subscribe((data: AlertInt) => {
if (data.answered === null) {
this.sendNotification( 'Alert ', data.text);
setTimeout(() => {
this.repeatAlert(id);
}, data.repeating * 1000 * 60);
}
});
}
I forget in the backend to send the data.answered field ... so was always null

How to retry fetch in a loop when it throws an error

I have the following JS function:
func() {
return fetch({
...
}).then({
...
})catch({
...
})
}
In it I return a promise returned by fetch(). In the event that it fails (ie calls catch() block) I want to repeat the whole thing. Something like having the whole thing in a while (true) loop, but I can't figure out how to do this with promises.
Any suggestions?
you should have a close look to promises and async await.
async function fetchUntilSucceeded() {
let success = false;
while(!success) {
try {
let result = await fetch(...);
success = true;
//do your stuff with your result here
} catch {
//do your catch stuff here
}
}
}
If you just need the results:
async function fetchUntilSucceeded() {
while(true) {
try {
return await fetch(...);
}
}
}
But be careful with such code as it might never resolve! also it can send a lot of requests without any waittime in between.
You can simply write a loop and count down the attempts until one succeeds or you run out. async/await makes this easy. See below for a minimal, complete example.
Note that the fetch API uses the response.ok flag to ensure that the response status falls in the 200 range. Wrapping with a try/catch is only sufficient to cover connection failures. If the response indicates a bad request, a retry is likely inappropriate. This code resolves the promise in such cases but you could consider !response.ok as an error and retry if you wish.
const fetchWithRetry = async (url, opts, tries=2) => {
const errs = [];
for (let i = 0; i < tries; i++) {
// log for illustration
console.log(`trying GET '${url}' [${i + 1} of ${tries}]`);
try {
return await fetch(url, opts);
}
catch (err) {
errs.push(err);
}
}
throw errs;
};
fetchWithRetry("https://httpstat.us/400")
.then(response => console.log("response is OK? " + response.ok))
.catch(err => console.error(err));
fetchWithRetry("foo")
.catch(err => console.error(err.map(e => e.toString())));
fetchWithRetry("https://httpstat.us/200")
.then(response => response.text())
.then(data => console.log(data))
.catch(err => console.error(err));
Pass the tries parameter as -1 if you want an infinite number of retries (but this doesn't seem like the common case to me).

RxJS Observable: repeat using count and then using notifier

I have an Observable that emits Either = Success | Failure:
import { Observable } from 'rxjs';
type Success = { type: 'success' };
type Failure = { type: 'failure' };
type Either = Success | Failure;
const either$ = new Observable<Either>(observer => {
console.log('subscribe');
observer.next({ type: 'failure' });
observer.complete();
return () => {
console.log('unsubscribe');
};
});
I want to allow the user to "retry" the observable when the Observable completes and the last value was Failure.
(The retry{,When} operators do not help here because they work with errors on the error channel. For this reason, I believe we should think in terms of repeat instead.)
I want to:
Repeat the Observable n times until the last value is not Failure.
Then, allow the user to repeat manually. When a repeat notifier observable (repeat$) emits, repeat the observable again.
For example:
// subscribe
// next { type: 'failure' }
// unsubscribe
// retry 2 times:
// subscribe
// next { type: 'failure' }
// unsubscribe
// subscribe
// next { type: 'failure' }
// unsubscribe
// now, wait for repeat notifications…
// on retry notification:
// subscribe
// next { type: 'failure' }
// unsubscribe
I couldn't come up with something simpler, but the code does what you want.
See https://stackblitz.com/edit/typescript-yqcejk
defer(() => {
let retries = 0;
const source = new BehaviorSubject(null);
return merge(source, repeat$.pipe(filter(() => retries <= MAX_RETRIES)))
.pipe(
concatMapTo(either$),
tap(value => {
const action = value as Either;
if (action.type === 'failure') {
if (retries < MAX_RETRIES) {
retries += 1;
source.next(null);
}
} else {
retries = 0;
}
})
)
}).subscribe(console.log);
I had to manually count retries.
The code has two sources of events source for automatic retries and repeat$ for user retries. All events are mapped to either$ using concatMapTo. As a side-effect we either next() to retry or do nothing waiting for user to retry.
User retries are suppressed using filter(() => retries >= MAX_RETRIES) until MAX_RETRIES count is reached.

Resources