Bacon.retry not retrying on 'Access-Control-Allow-Origin' errors - ajax

I have a buggy Web service that sporadically sends a 500-error "XMLHttpRequest cannot load http://54.175.3.41:3124/solve. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://local.xxx.me:8080' is therefore not allowed access. The response had HTTP status code 500."
I use Bacon.retry to wrap the ajax call. When it fails, it'll just retry. However, what I notice is that the stream won't produce a value when the server fails. It's as if Bacon.retry doesn't retry (which is in fact what's happening, when I look under the hood in the dev console).
I'm using BaconJS 0.7.65.
The observable Bacon.retry looks like this:
var ajaxRequest = Bacon.fromPromise($.ajax(//...));
var observable = Bacon.retry({
source: function() { return ajaxRequest; },
retries: 50,
delay: function() { return 100; }
});
The code that calls the observable looks like this:
stream.flatMap(function(valuesOrObservables) {
return Bacon.fromArray(valuesOrObservables)
.flatMapConcat(function(valueOrObservable) {
switch(valueOrObservable.type) { //we calculate this beforehand
case 'value' :
return valueOrObservable.value;
case 'observable' :
return Bacon.fromArray(valueOrObservable.observables)
.flatMapConcat(function(obs) { return obs; })
}
})
})
Observations:
if I add an error handler to the observable, it still does not work.
for some reason, #retry is called 50 times even when it succeeds.

I'm not sure entirely about Bacon but in RxJS Ajax calls are usually wrapped around AsyncSubjects so re subscribing to an error'd stream will just fire off the same error, you generally have to re-execute the method that produces the observable.
So something like retry would be (again sorry this is in Rx):
Rx.Observable.defer(() => callAjaxReturnObservable())
.retry(50)
.subscribe();
EDIT 1
Trying Baconize this and clarify my earlier answer:
var observable = Bacon.retry({
source : function() { return Bacon.fromPromise($.ajax(/**/)); },
retries : 50,
delay: function() { return 100; }
});
If you don't have the fromPromise inside of the source function, then every time you retry the downstream will just receive the same exception.

Related

How to make cypress wait for a response that depends on another response?

From response A (/list.json) my app receives a list of items. Based on the output of A, my app makes another set of requests B for individual items (/one.txt, /two.txt, ...).
Now in my test I want to make sure that all responses B return HTTP 200.
Waiting (cy.wait) for response A is fine. However, waiting for responses B is more difficult, because I have to start waiting just upon receiving response A where I learn about responses B.
I tried 2 options:
start waiting inside of cy.wait of response A - code,
start waiting outside of cy.wait of response A - code
Neither of those work. With option 1 I get
`cy.wait()` timed out waiting `5000ms` for the 1st request to the route: `one.txt`. No request ever occurred
And with option 2 I get a pass, even though /two.txt doesn't exist. Looks like cy.wait for responses B is added after the responses were received
Since all requests are triggered off the visit, and are dynamic, you need a single intercept that handles all requests.
To me that means adding some javascript and dynamic aliases.
// avoid status code 304, disable browser cache
Cypress.automation('remote:debugger:protocol', {
command: 'Network.clearBrowserCache'
})
describe('spec', () => {
it('test', () => {
let items = [];
cy.intercept('GET', '*', (req) => {
const slug = req.url.substring(req.url.lastIndexOf('/') + 1)
if (slug === 'list.json') {
req.alias = 'list'
}
if (items.includes(slug)) {
req.alias = 'item'
}
req.continue((res) => {
if (slug === 'list.json')) {
items = res.body;
}
})
})
cy.visit('https://demo-cypress.netlify.app');
cy.wait('#list') // wait for list
.then(() => { // now items is populated
for (let item of items) { // really just need the count
cy.wait('#item').then(interception => { // wait n-times
expect(interception.response.statusCode).to.eq(200);
})
}
})
})
})

How to wait for a successful response in Cypress tests

Background
I use 3 back-end servers to provide fault tolerance for one of my online SaaS application. All important API calls, such as getting user data, contact all 3 servers and use value of first successfully resolved response, if any.
export function getSuccessValueOrThrow$<T>(
observables$: Observable<T>[],
tryUntilMillies = 30000,
): Observable<T> {
return race(
...observables$.map(observable$ => {
return observable$.pipe(
timeout(tryUntilMillies),
catchError(err => {
return of(err).pipe(delay(5000), mergeMap(_err => throwError(_err)));
}),
);
})
);
}
getSuccessValueOrThrow$ get called as following:
const shuffledApiDomainList = ['server1-domain', 'server2-domain', 'server3-domain';
const sessionInfo = await RequestUtils.getSuccessValueOrThrow(
...(shuffledApiDomainList.map(shuffledDomain => this.http.get<SessionDetails>(`${shuffledDomain}/file/converter/comm/session/info`))),
).toPromise();
Note: if one request resolve faster than others, usually the case, race rxjs function will cancel the other two requests. On Chrome dev network tab it will look like bellow where first request sent out was cancelled due to being too slow.
Question:
I use /file/converter/comm/session/info (lets call it Endpoint 1) to get some data related to a user. This request dispatched to all 3 back-end servers. If one resolve, then remaining 2 request will be cancelled, i.e. they will return null.
On my Cypress E2E test I have
cy.route('GET', '/file/converter/comm/session/info').as('getSessionInfo');
cy.visit('https://www.ps2pdf.com/compress-mp4');
cy.wait('#getSessionInfo').its('status').should('eq', 200)
This sometimes fails if the since getSessionInfo alias was hooked on to a request that ultimately get cancelled by getSuccessValueOrThrow$ because it wasn't the request that succeeded.Bellow image shows how 1 out of 3 request aliased with getSessionInfo succeeded but the test failed since the first request failed.
In Cypress, how do I wait for a successful i.e. status = 200 request?
Approach 1
Use .should() callback and repeat the cy.wait call if status was not 200:
function waitFor200(routeAlias, retries = 2) {
cy.wait(routeAlias).then(xhr => {
if (xhr.status === 200) return // OK
else if (retries > 0) waitFor200(routeAlias, retries - 1); // wait for the next response
else throw "All requests returned non-200 response";
})
}
// Usage example.
// Note that no assertions are chained here,
// the check has been performed inside this function already.
waitFor200('#getSessionInfo');
// Proceed with your test
cy.get('button').click(); // ...
Approach 2
Revise what it is that you want to test in the first place.
Chances are - there is something on the page that tells the user about a successful operation. E.g. show/hide a spinner or a progress bar, or just that the page content is updated to show new data fetched from the backend.
So in this approach you would remove cy.wait() altogether, and focus on what the user sees on the page - do some assertions on the actual page content.
cy.wait() yields an object containing the HTTP request and response properties of the XHR. The error you're getting is because you're looking for property status in the XHR object, but it is a property of the Response Object. You first have to get to the Response Object:
cy.wait('#getSessionInfo').should(xhr => {
expect(xhr.response).to.have.property('status', 200);
});
Edit: Since our backend uses graphql, all calls use the single /graphql endpoint. So I had to come up with a solution to differentiate each call.
I did that by using the onResponse() method of cy.route() and accumulating the data in Cypress environment object:
cy.route({
method: 'GET',
url: '/file/converter/comm/session/info',
onResponse(xhr) {
if (xhr.status === 200) {
Cypress.env('sessionInfo200') = xhr;
}
}
})
You can then use it like this:
cy.wrap(Cypress.env()).should('have.property', 'sessionInfo200');
I wait like this:
const isOk = cy.wait("#getSessionInfo").then((xhr) => {
return (xhr.status === 200);
});

how to handle these function in a more idiomatic rxjs way

I am new to rxjs (the project is stuck on rxjs 5 for the time being) and I don't really need to refactor sthese functions but as I'm trying to get up to speed with rxjs, how would I not use toPromise for an http call:
try {
const response = await this.http.post(`${TokenUrl}`, payload, options).toPromise();
} catch(err) {
// whatever
}
I also have this setInterval that periodically pings a server:
this.timerId = setInterval(() => {
this.blobStorageService.ping();
}, 60000);
I tried using interal but could not quite get the syntax right.

Axios Reponse Interceptor : unable to handle an expired refresh_token (401)

I have the following interceptor on my axios reponse :
window.axios.interceptors.response.use(
response => {
return response;
},
error => {
let errorResponse = error.response;
if (errorResponse.status === 401 && errorResponse.config && !errorResponse.config.__isRetryRequest) {
return this._getAuthToken()
.then(response => {
this.setToken(response.data.access_token, response.data.refresh_token);
errorResponse.config.__isRetryRequest = true;
errorResponse.config.headers['Authorization'] = 'Bearer ' + response.data.access_token;
return window.axios(errorResponse.config);
}).catch(error => {
return Promise.reject(error);
});
}
return Promise.reject(error);
}
);
The _getAuthToken method is :
_getAuthToken() {
if (!this.authTokenRequest) {
this.authTokenRequest = window.axios.post('/api/refresh_token', {
'refresh_token': localStorage.getItem('refresh_token')
});
this.authTokenRequest.then(response => {
this.authTokenRequest = null;
}).catch(error => {
this.authTokenRequest = null;
});
}
return this.authTokenRequest;
}
The code is heavily inspired by https://github.com/axios/axios/issues/266#issuecomment-335420598.
Summary : when the user makes a call to the API and if his access_token has expired (a 401 code is returned by the API) the app calls the /api/refresh_token endpoint to get a new access_token. If the refresh_token is still valid when making this call, everything works fine : I get a new access_token and a new refresh_token and the initial API call requested by the user is made again and returned correctly.
The problem occurs when the refresh_token has also expired.
In that case, the call to /api/refresh_token returns a 401 and nothing happens. I tried several things but I'm unable to detect that in order to redirect the user to the login page of the app.
I found that in that case the if (!this.authTokenRequest) statement inside the _getAuthToken method returns a pending Promise that is never resolved. I don't understand why this is a Promise. In my opinion it should be null...
I'm a newbie with Promises so I may be missing something !
Thanks for any help !
EDIT :
I may have found a way much simpler to handle this : use axios.interceptors.response.eject() to disable the interceptor when I call the /api/refresh_token endpoint, and re-enable it after.
The code :
createAxiosResponseInterceptor() {
this.axiosResponseInterceptor = window.axios.interceptors.response.use(
response => {
return response;
},
error => {
let errorResponse = error.response;
if (errorResponse.status === 401) {
window.axios.interceptors.response.eject(this.axiosResponseInterceptor);
return window.axios.post('/api/refresh_token', {
'refresh_token': this._getToken('refresh_token')
}).then(response => {
this.setToken(response.data.access_token, response.data.refresh_token);
errorResponse.config.headers['Authorization'] = 'Bearer ' + response.data.access_token;
this.createAxiosResponseInterceptor();
return window.axios(errorResponse.config);
}).catch(error => {
this.destroyToken();
this.createAxiosResponseInterceptor();
this.router.push('/login');
return Promise.reject(error);
});
}
return Promise.reject(error);
}
);
},
Does it looks good or bad ? Any advice or comment appreciated.
Your last solution looks not bad. I would come up with the similar implementation as you if I were in the same situation.
I found that in that case the if (!this.authTokenRequest) statement inside the _getAuthToken method returns a pending Promise that is never resolved. I don't understand why this is a Promise. In my opinion it should be null...
That's because this.authTokenRequest in the code was just assigned the Promise created from window.axios.post. Promise is an object handling kind of lazy evaluation, so the process you implement in then is not executed until the Promise was resolved.
JavaScript provides us with Promise object as kind of asynchronous event handlers which enables us to implement process as then chain which is going to be executed in respond with the result of asynchronous result. HTTP requests are always inpredictable, because HTTP request sometimes consumes much more time we expect, and also sometimes not. Promise is always used when we use HTTP request in order to handle the asynchronous response of it with event handlers.
In ES2015 syntax, you can implement functions with async/await syntax to hanle Promise objects as it looks synchronous.

Integration test redux-observable that makes multiple ajax requests with debounce

I have a redux-observable epic that polls an endpoint, getting progress updates until the progress is 100%. The polling interval is acheived using debounceTime like so:
function myEpic(action$, store, dependencies) {
return action$.ofType('PROCESSING')
.do(action => console.log(`RECEIVED ACTION: ${JSON.stringify(action)}`))
.debounceTime(1000, dependencies.scheduler)
.mergeMap(action => (
dependencies.ajax({ url: action.checkUrl })
.map((resp) => {
if (parseInt(resp.progress, 10) === 100) {
return { type: 'SUCCESS' };
}
return { checkUrl: resp.check_url, progress: resp.progress, type: 'PROCESSING' };
})));
}
This works fine but I'd like to write an integration test that tests the state of the store when progress is at 25%, then at 50%, then at 100%.
In my integration tests I can set dependencies.scheduler to be new VirtualTimeScheduler().
This is how I'm trying to do it at the moment (using jest):
describe('my integration test', () => {
const scheduler = new VirtualTimeScheduler();
beforeEach(() => {
// Fake ajax responses
const ajax = (request) => {
console.log(`FAKING REQUEST FOR URL: ${request.url}`);
if (request.url === '/check_url_1') {
return Observable.of({ progress: 25, check_url: '/check_url_2' });
} else if (request.url === '/check_url_2') {
return Observable.of({ progress: 50, check_url: '/check_url_3' });
} else if (request.url === '/check_url_3') {
return Observable.of({ progress: 100 });
}
return null;
};
store = configureStore(defaultState, { ajax, scheduler });
});
it('should update the store properly after each call', () => {
store.dispatch({ checkUrl: '/check_url_1', progress: 0, type: 'PROCESSING' });
scheduler.flush();
console.log('CHECK CORRECT STATE FOR PROGRESS 25');
scheduler.flush();
console.log('CHECK CORRECT STATE FOR PROGRESS 50');
scheduler.flush();
console.log('CHECK CORRECT STATE FOR PROGRESS 100');
});
});
My expected output would be:
RECEIVED ACTION: {"checkUrl":"/check_url_1","progress":0,"type":"PROCESSING"}
FAKING REQUEST FOR URL: /check_url_1
CHECK CORRECT STATE FOR PROGRESS 25
RECEIVED ACTION: {"checkUrl":"/check_url_2","progress":25,"type":"PROCESSING"}
FAKING REQUEST FOR URL: /check_url_2
CHECK CORRECT STATE FOR PROGRESS 50
RECEIVED ACTION: {"checkUrl":"/check_url_3","progress":50,"type":"PROCESSING"}
# CHECK CORRECT STATE FOR PROGRESS 100
But instead the output I get is
RECEIVED ACTION: {"checkUrl":"/check_url_1","progress":0,"type":"PROCESSING","errors":null}
FAKING REQUEST FOR URL: /check_url_1
RECEIVED ACTION: {"checkUrl":"/check_url_2","progress":25,"type":"PROCESSING","errors":null}
CHECK CORRECT STATE FOR PROGRESS 25%
CHECK CORRECT STATE FOR PROGRESS 50%
CHECK CORRECT STATE FOR PROGRESS 100%
At which time the test finishes. I'm configuring the store so that I can mock ajax requests and the scheduler used for debounceTime like as recommended here
So my question is how can I test the state of my store after each of the three ajax requests?
Interestingly enough, I played around with your code and am fairly confident you just found a bug in the debounceTime operator, which causes the apparent swallowing the scheduled debounce. The bad news is that even if that bug is fixed, you're code still wouldn't do what you're looking for order wise.
Bear with me as shit is about to get real:
Epic receives action PROCESSING and schedules debounce, yielding execution to your test
Your test calls scheduler.flush() and the VirtualScheduler executes the scheduled debounce work, which will pass along the original PROCESSING action to the mergeMap
Fake ajax is made, which synchronously emits a response
Response is mapped to the second PROCESSING action
Your epic emits that second action synchronously
The second action is recursively received by your epic and given to the debounce
The debounceTime operator now schedules that second action on the VirtualScheduler but the debounceTime operator is in the middle of executing the previously scheduled work still from the first action.
The call stack unwinds a bunch up until it reaches inside the previously scheduled debounce work from the first action that had just next()'d the first action. The rxjs code for debounceTime then sets this.lastValue = null and this.hasValue = false This is the rxjs bug, it needs to be done before nexting into the destination
The stack unwinds some more to the running flush() method of the VirtualScheduler, which now dequeues the second scheduled debounced action because it was added the scheduled work array synchronously, before this the flushing finished. Remember, we've only called scheduler.flush() ONCE so far, which is the function we're in back in at this point.
The second scheduled debounce work is run, but this.hasValue === false because the first scheduled one set it, so the debounceTime operator does not emit anything.
Stack unwinds to our first scheduler.flush()
We console.log('CHECK CORRECT STATE FOR PROGRESS 25')
All the other scheduler.flush() calls do nothing as there's nothing scheduled.
This is technically a bug, but it's not surprising that no one has run into it since running debounce synchronously without any delay defeats the point of it, except when you're testing, of course. This ticket is basically the same thing and OJ says RxJS doesn't make reentrancy guarantees, but I that might be up for debate in this case. I've filed a PR with the fix to discuss
Remember, this bug wouldn't have solved your underlying question about the ordering, but would have prevented the actions from being swallowed.
Off the top of my head I'm not sure how you would do what you'd like to do specifically if you want to maintain 100% synchronous behavior (VirtualScheduler). You'd need some way of yielding to your test in between debounces. For me when and if I write integration tests I mock out very little, if anything. e.g. let the debounces actually debounce either naturally or by mocking out setTimeout to advance them quicker but still keeping them async which will yield back to your test allowing you to check the state, but making your test also async.
For anyone wanting to reproduce, here's the StackBlitz code I used
The answer was to re-write the test asynchronously. Also note-worthy is that I had to mock the ajax requests by returning an Observable.fromPromise rather than just a regular Observable.of, otherwise they would still get swallowed up by the debounce. Something along these lines (using jest):
describe('my integration test', () => {
const scheduler = new VirtualTimeScheduler();
beforeEach(() => {
// Fake ajax responses
const ajax = request => (
Observable.fromPromise(new Promise((resolve) => {
if (request.url === '/check_url_1') {
resolve({ response: { progress: 25, check_url: '/check_url_2' } });
} else if (request.url === '/check_url_2') {
resolve({ response: { progress: 50, check_url: '/check_url_3' } });
} else {
resolve({ response: { progress: 100 } });
}
}))
);
store = configureStore(defaultState, { ajax, timerInterval: 1 });
});
it('should update the store properly after each call', (done) => {
let i = 0;
store.subscribe(() => {
switch (i) {
case 0:
console.log('CHECK CORRECT STATE FOR PROGRESS 0');
break;
case 1:
console.log('CHECK CORRECT STATE FOR PROGRESS 25');
break;
case 2:
console.log('CHECK CORRECT STATE FOR PROGRESS 50');
break;
case 3:
console.log('CHECK CORRECT STATE FOR PROGRESS 100');
done();
break;
default:
}
i += 1;
});
store.dispatch({ checkUrl: '/check_url_1', progress: 0, type: 'PROCESSING' });
});
});
I also set the timer interval to 1 by passing it as a dependency. In my epic I set it like this: .debounceTime(dependencies.timerInterval || 1000)

Resources