Testcafe: How to error on 4xx or 5xx response? - node.js

I'm working on a legacy app that makes a lot of calls to external sources. I'm trying to refactor this app, and I've written testcafe tests to help inform me when I've made a mistake. I'm NOT running my tests with --skip-js-errors, but when I get 404 errors and the console prints this out:
the test doesn't stop. I'd like errors like these to be something I am informed about. How do I make 4xx and 5xx network responses fail testcafe? I'm using angular 1.2 if that matters. If I could, I would change all the remote calls to throw an exception on 4xx or 5xx, but this is legacy code I don't understand, and I'm sure doing that would break a feature.

I suggest you extend RequestLogger to check a request. You can throw an error based on the request status. For example:
import EventEmitter from 'events';
import { RequestHook } from 'testcafe';
fixture `test`
.page('https://testcafe.devexpress.com/Details2/')
class FailedRequestsLogger extends RequestHook {
constructor (...args) {
super(...args);
this.events = new EventEmitter();
this.failedRequestPromise = new Promise(resolve => this.events.once('failed-request', resolve));
}
onRequest (request) {
}
onResponse (response) {
if (response.statusCode >= 400)
this.events.emit('failed-request', response.statusCode);
}
waitForFailedRequest (action) {
return Promise.race([
action(),
this.failedRequestPromise.then(statusCode => Promise.reject(new Error(`Request failed with the ${statusCode} status code`)))
])
}
}
const logger = new FailedRequestsLogger();
test.requestHooks(logger)('test', async t => {
await logger.waitForFailedRequest(async () => {
await t.click('body');
await t.wait(10000);
});
});

Related

How to send a custom status and error message if no data found using Node.JS/express and axios?

If no user is found (it's just an example to understand error handling), I want to send a custom message such as
res.status(404).send('no user');
my client never receives that and instead I get:
[AxiosError: Request failed with status code 404]
What am I doing wrong? I cannot find any other solution and have been researching for a while now. Also wonder how I could send a custom status (if no data found it's 404 but what if I want to send 200)?
node express
router.get('/getuser', async (req, res) => {
try {
const user = await User.findOne({_id: req.user._id});
if (!user) {
res.status(404).send('no user');
} else {
res.status(200).send(user)
}
} catch(error) {
res.status(500).send(error)
}
});
frontend
const trycatch = async () => {
try {
const response = await axios.get(example.com/trycatch)
return response.data;
} catch (error) {
console.log(error)
}
}
Responses with a status code of 400 or more are treated as errors by axios. As a consumer, you can only react to that in the catch (error) clause, for example:
catch (error) {
switch (error.response.status) {
case 404: return "no user"; break;
default: return error.response.data;
}
}
But you can influence which statuses cause errors with the validateStatus option of the axios request.
Summary: Either you distribute your code between the try and the catch block, where the try block code handles successes (status < 400) and the catch block handles errors (status ≥ 400). Or you use validateStatus to change what counts as a success and what counts as an error.

Node.js concurrent API GETs, return as soon as one responses

I have a list of APIs I want to call GET simultaneously on all of them and return as soon as one API finishes the request with a response code of 200.
I tried using a for-loop and break, but that doesn't seem to work. It would always use the first API
import axios from 'axios';
const listOfApi = ['https://example.com/api/instanceOne', 'https://example.com/api/instanceTwo'];
let response;
for (const api of listOfApi) {
try {
response = await axios.get(api, {
data: {
url: 'https://example.com/',
},
});
break;
} catch (error) {
console.error(`Error occurred: ${error.message}`);
}
}
You can use Promise.race() to see which of an array of promises finishes first while running all the requests in parallel in flight at the same time:
import axios from 'axios';
const listOfApi = ['https://example.com/api/instanceOne', 'https://example.com/api/instanceTwo'];
Promise.any(listOfApi.map(api => {
return axios.get(api, {data: {url: 'https://example.com/'}}).then(response => {
// skip any responses without a status of 200
if (response.status !== 200) {
throw new Error(`Response status ${response.status}`, {cause: response});
}
return response;
});
})).then(result => {
// first result available here
console.log(result);
}).catch(err => {
console.log(err);
});
Note, this uses Promise.any() which finds the first promise that resolves successfully (skipping promises that reject). You can also use Promise.race() if you want the first promise that resolves or rejects.
I think jfriend00's answer is good, but I want to expand on it a bit and show how it would look with async/await, because that's what you are already using.
As mentioned, you can use Promise.any (or Promise.race). Both take an array of promises as argument. Promise.any will yield the result of the first promise that resolves successfully, while Promise.race will simply wait for the first promise that finishes (regardless of whether it was fulfilled or rejected) and yield its result.
To keep your code in the style of async/await as it originally was, you can map the array using an async callback function, which will effectively return a promise. This way, you don't have to "branch off into .then territory" and can keep the code more readable and easier to expand with conditions, etc.
This way, the code can look as follows:
import axios from 'axios';
const listOfApi = ['https://example.com/api/instanceOne', 'https://example.com/api/instanceTwo'];
try {
const firstResponse = await Promise.any(listOfApi.map(async api => {
const response = await axios.get(api, {
data: {
url: 'https://example.com/',
},
});
if (response.status !== 200) {
throw new Error(`Response status ${response.status}`, {cause: response});
}
return response;
}));
// DO SOMETHING WITH firstResponse HERE
} catch (error) {
console.error('Error occured:', error);
}
Side note: I changed your console.error slightly. Logging only error.message is a common mistake that hinders you from effective debugging later on, because it will lack a lot of important information because it prints only the message and not the error stack, the error name or any additional properties the error may have. Using .stack and not .message will already be better as it includes name and stack then, but what's best is to supply the error as separate argument to console.error so that inspect gets called on it and it can print the whole error object, with stack and any additional properties you may be interested in. This is very valuable when you encounter an error in production that is not so easy to reproduce.

How can I change the result status in Axios with an adapter?

The why
We're using the axios-retry library, which uses this code internally:
axios.interceptors.response.use(null, error => {
Since it only specifies the error callback, the Axios documentation says:
Any status codes that falls outside the range of 2xx cause this function to trigger
Unfortunately we're calling a non-RESTful API that can return 200 with an error code in the body, and we need to retry that.
We've tried adding an Axios interceptor before axios-retry does and changing the result status in this case; that did not trigger the subsequent interceptor error callback though.
What did work was specifying a custom adapter. However this is not well-documented and our code does not handle every case.
The code
const axios = require('axios');
const httpAdapter = require('axios/lib/adapters/http');
const settle = require('axios/lib/core/settle');
const axiosRetry = require('axios-retry');
const myAdapter = async function(config) {
return new Promise((resolve, reject) => {
// Delegate to default http adapter
return httpAdapter(config).then(result => {
// We would have more logic here in the production code
if (result.status === 200) result.status = 500;
settle(resolve, reject, result);
return result;
});
});
}
const axios2 = axios.create({
adapter: myAdapter
});
function isErr(error) {
console.log('retry checking response', error.response.status);
return !error.response || (error.response.status === 500);
}
axiosRetry(axios2, {
retries: 3,
retryCondition: isErr
});
// httpstat.us can return various status codes for testing
axios2.get('http://httpstat.us/200')
.then(result => {
console.log('Result:', result.data);
})
.catch(e => console.error('Service returned', e.message));
This works in the error case, printing:
retry checking response 500
retry checking response 500
retry checking response 500
retry checking response 500
Service returned Request failed with status code 500
It works in the success case too (change the URL to http://httpstat.us/201):
Result: { code: 201, description: 'Created' }
The issue
Changing the URL to http://httpstat.us/404, though, results in:
(node:19759) UnhandledPromiseRejectionWarning: Error: Request failed with status code 404
at createError (.../node_modules/axios/lib/core/createError.js:16:15)
at settle (.../node_modules/axios/lib/core/settle.js:18:12)
A catch on the httpAdapter call will catch that error, but how do we pass that down the chain?
What is the correct way to implement an Axios adapter?
If there is a better way to handle this (short of forking the axios-retry library), that would be an acceptable answer.
Update
A coworker figured out that doing .catch(e => reject(e)) (or just .catch(reject)) on the httpAdapter call appears to handle the issue. However we'd still like to have a canonical example of implementing an Axios adapter that wraps the default http adapter.
Here's what worked (in node):
const httpAdapter = require('axios/lib/adapters/http');
const settle = require('axios/lib/core/settle');
const customAdapter = config =>
new Promise((resolve, reject) => {
httpAdapter(config).then(response => {
if (response.status === 200)
// && response.data contains particular error
{
// log if desired
response.status = 503;
}
settle(resolve, reject, response);
}).catch(reject);
});
// Then do axios.create() and pass { adapter: customAdapter }
// Now set up axios-retry and its retryCondition will be checked
Workaround with interceptor and custom error
const axios = require("axios").default;
const axiosRetry = require("axios-retry").default;
axios.interceptors.response.use(async (response) => {
if (response.status == 200) {
const err = new Error("I want to retry");
err.config = response.config; // axios-retry using this
throw err;
}
return response;
});
axiosRetry(axios, {
retries: 1,
retryCondition: (error) => {
console.log("retryCondition");
return false;
},
});
axios
.get("https://example.com/")
.catch((err) => console.log(err.message)); // gonna be here anyway as we'll fail due to interceptor logic

Fail cypress test when 500 xhr request occurs

I am writing end-to-end tests for a web application in cypress and I would like cypress to fail the test if the applications sends any request with a http 500 error response.
I am using cypress in version 3.1.5. I already tried the following, but I get an promise error.
cy.server();
cy.route({
url: '**',
onResponse: (xhr) => {
cy.log(xhr);
}
});
I hope to find a more elegant solution to this problem, because this sounds to me like a pretty standard use-case.
Try using below code refer doc
cy.intercept('**').as('all')
cy.wait('#all').its('response.statusCode').should('not.be.oneOf', [500])
OR
Checking status code using - should('be.oneOf', [200,300])
cy.wait('#all').its('response.statusCode').should('be.oneOf', [200,300])
If you're using a Cypress version that supports intercept (v7.x.x or later), then you can do this in your test file:
beforeEach(() => {
cy.intercept('**', request => {
request.on('response', function (response) {
expect(response.statusCode).is.lessThan(500); // Test will fail if an 500 error happen
});
});
});
Not sure if there's some canonical way, but you can monkey-patch the XMLHttpRequest API and throw on >= 500 status responses.
If your app is using Fetch API, SendBeacon, or something else, you can do something similar.
// put this in support/index.js
Cypress.on('window:before:load', win => {
const origSend = win.XMLHttpRequest.prototype.send;
win.XMLHttpRequest.prototype.send = function () {
// deferring because your app may be using an abstraction (jquery),
// which may set this handler *after* it sets the `send` handler
setTimeout(() => {
const origHandler = this.onreadystatechange;
this.onreadystatechange = function () {
if ( this.readyState === 4 && this.status >= 500 ) {
throw new Error(`Server responded to "${this.url}" with ${this.status}`);
}
if ( origHandler ) {
return origHandler.apply(this, arguments);
}
};
return origSend.apply(this, arguments);
});
};
});
WTBS, this is not gonna be very useful because you app may want to handle that 500 instead, while this will prevent it.
Better solution is to make sure you app simply throws unhandled 500 responses.
You can throw an error inside the on request, that should work.
cy.server();
cy.route({
url: '**',
onResponse: (xhr) => {
if(xhr.status === 500) {
throw new Error('Failing test caused by a unhandled request with status 500')
}
cy.log(xhr);
}
})

NodeJs http status exception handling

I have created nodejs + express application. Now in my application when exception caught errors are send as follows
app.get('/data', (req, res) => {
if(!req.params.token){
return res.status(403).send('Access token not provided');
}
//do something here
});
Instead of sending res.status(403).send('Access token not provided'); can I send something like this
exception.js
class Forbidden {
constructor(message,stack = null){
this.code = 403;
this.message = message
this.stack = stack;
}
}
app.js
var httpForbidden = require('exception.js');
app.get('/data', (req, res) => {
if(!req.params.token){
return new httpForbidden ('Access token not provided');
}
//do something here
});
And also how can I caught all exceptions in once place ?
You could use something like this:
class httpError {}
class httpForbidden extends httpError {
constructor(message, stack = null) {
super();
this.code = 403;
this.message = message
this.stack = stack;
}
}
app.get('/', (req, res) => {
if (!req.params.token) {
throw new httpForbidden('Access token not provided');
}
...
});
app.use((err, req, res, next) => {
if (err instanceof httpError) {
return res.status(err.code).send(err.message);
}
res.sendStatus(500);
});
This uses an Express error handling middleware that will check if the error that got thrown is an instance of httpError (which would be the superclass of all the HTTP error classes that you'd want to create) and, if so, would generate a particular response according to the code and the message (or generate a generic 500 error response otherwise).
I like to create a separate function, along with other utility functions ( say in lib.js), which creates a properly formatted JSON response object and selects the appropriate logger to log response depending upon the HTTP status code.
lib.js
var logger = require("./loggger");
module.exports.sendResponse = function (res,code,message,data) {
if(code<100 || code>599) {
throw new Error("response cannot be sent. Invalid http-code was provided.");
}
var responseLogger = code>=500 ? logger.error : logger.debug;
var responseObject = {
"code" : code,
"message" : message
};
if(data) {
responseObject.data = data;
}
responseLogger(responseObject);
res.status(code).json(responseObject);
};
app.js
var lib = require("./lib");
/*
Relevant Express server code
*/
app.get('/data', function (req,res) {
if(!req.params.token){
return lib.sendResponse(res,403,"Access token not provided");
}
// Rest of business logic
});
Note : You can write your own logging functionality, but I strongly suggest to build it upon some standard logging library like winston)
Below method is deprecated as the boom is changes to #hapi/boom,
https://hapi.dev/family/boom/?v=8.0.1
here you find whole documentation of #hapi/boom library
-----deprecated-------
You can use boom library instead, which provides a set of utilities for returning HTTP errors
HTTP 4xx Errors
Boom.badRequest([message], [data])
Boom.unauthorized([message],[scheme], [attributes])
HTTP 5xx Errors
Boom.badImplementation([message], [data]) - (alias: internal)
Boom.notImplemented([message], [data])
for more api documentation visit here
You can use:
res.code(403).json({message: '...', stack: '...'});
and send whatever you want. But you do it with calling methods on the response object.
And also how can I caught all exceptions in once place ?
Very bad idea. You should handle all errors where they happen so that you can still have some context to handle them in a reasonable way. Otherwise you can just throw exceptions and return 500 errors.

Resources