I am beginning learning the basics of developing APIs and am following along with a video on YouTube by a developer named Ania Kubow. There are three javascript libraries in use which are ExpressJS, Cheerio & Axios. I have picked up what she is telling us and going well enough. I am however, getting an error when executing the code so far. Below here is the main part of the example API:
Note:The "app" variable is referring to Express JS
app.get('/news', (req, res) => {
axios.get('https://www.theguardian.com/environment/climate-crisis')
.then((response) => {
// Store the html retrieved via the axios http GET request
const html = response.data;
// Load the retrieved html into the cheerio instance and store to $ cheerio selector
const $ = cheerio.load(html);
// DEBUG purpose stuff
var loopCounter = 0;
var climate_A_Elements = $('a:contains(climate)', html).length;
console.log('Number of articles found: ' climate_A_Elements);
//
// Get all articles that contain 'climate' in the <a /> element
var allFoundArticles = $('a:contains("climate")', html);
// Iterate through all found articles using cheerio forEach loop
allFoundArticles.each(function () {
// Assign article title and url it is located at to variables
var title = $(this).text();
var url = $(this).attr('href');
// Push the previously defined vars to the previously defined array
articlesData.push({
title,
url,
});
// Output the article details to page as response
res.json(articlesData);
// Add to loop count for debugging purposes and log in console
loopCounter = 1;
console.log('Loops: ' loopCounter);
}).catch(err => {
console.log(err);
})
})
})
After completing one iteration of the Cheerio each loop, the application errors and gives the below error output:
node:internal/errors:484
ErrorCaptureStackTrace(err);
^
Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client
at new NodeError (node:internal/errors:393:5)
at ServerResponse.setHeader (node:_http_outgoing:644:11)
at ServerResponse.header (C:\Users\etomm\Downloads\Daniel\Dev\Web\climate-change-api\node_modules\express\lib\response.js:794:10)
at ServerResponse.json (C:\Users\etomm\Downloads\Daniel\Dev\Web\climate-change-api\node_modules\express\lib\response.js:275:10)
at Element.<anonymous> (C:\Users\etomm\Downloads\Daniel\Dev\Web\climate-change-api\index.js:65:21)
at LoadedCheerio.each (C:\Users\etomm\Downloads\Daniel\Dev\Web\climate-change-api\node_modules\cheerio\lib\api\traversing.js:519:26)
at C:\Users\etomm\Downloads\Daniel\Dev\Web\climate-change-api\index.js:52:30
at process.processTicksAndRejections (node:internal/process/task_queues:95:5) {
code: 'ERR_HTTP_HEADERS_SENT'
}
Node.js v18.12.1
[nodemon] app crashed - waiting for file changes before starting...
I have of course, left a question as a comment on the video mentioned above, but realistically I don't expect to receive a response, so after trying to find the issue for a while decided to try here for some help and guidance.
Any guidance would be greatly appreciated. If you require anything to help further please ask.
Thankyou.
I tried changing to the version of node installed on the video mentioned to see if it was a version issue, but it didn't solve anything. I tried refactoring the code and stepping through a few times with no results and tried searching the web but couldn't find the answer to this particular issue.
CodePudding user response:
You are sending response mulitple times in a single request Try to push you result in an array then send it after your loop executed successfully. Try this code.
app.get('/news', (req, res) => {
axios.get('https://www.theguardian.com/environment/climate-crisis')
.then((response) => {
// artical data array
const articlesData = [];
// Store the html retrieved via the axios http GET request
const html = response.data;
// Load the retrieved html into the cheerio instance and store
//to $ cheerio selector
const $ = cheerio.load(html);
// DEBUG purpose stuff
var loopCounter = 0;
var climate_A_Elements = $('a:contains(climate)', html).length;
console.log('Number of articles found: ' climate_A_Elements);
//
// Get all articles that contain 'climate' in the <a /> element
var allFoundArticles = $('a:contains("climate")', html);
// Iterate through all found articles using cheerio forEach
//loop
allFoundArticles.each(function () {
// Assign article title and url it is located at to variables
var title = $(this).text();
var url = $(this).attr('href');
// Push the previously defined vars to the previously defined
//array
articlesData.push({
title,
url,
});
// Output the article details to page as response
// Add to loop count for debugging purposes and log in
//console
loopCounter = 1;
console.log('Loops: ' loopCounter);
})
res.json(articlesData);
}).catch(err => {
console.log(err);
})
})
CodePudding user response:
You should always ask yourself why you got this error, in your case because you put res.json inside the loop, you should always call res.json /sending the response once time , The recommendation from folks will help you,
CodePudding user response:
Asad's answer is correct; you're sending responses for each element in the loop. The request is completed after the first iteration, so when the second iteration occurs, Express complains that you're trying to respond a second time to an already-completed response.
The minimal failing pattern is:
app.get("/news", (req, res) => {
for (let i = 0; i < 2; i ) {
res.json({message: "this won't work"});
}
});
The fix is:
app.get("/news", (req, res) => {
for (let i = 0; i < 2; i ) {
// do whatever you need to in the loop
}
// respond once after the loop is finished
res.json({message: "this works"});
});
Here's a complete, runnable example:
const app = require("express")();
const axios = require("axios");
const cheerio = require("cheerio");
app.get("/news", (req, res) => {
axios
.get("https://www.theguardian.com/environment/climate-crisis")
.then((response) => {
// Store the html retrieved via the axios http GET request
const html = response.data;
// Load the retrieved html into the cheerio instance and store to $ cheerio selector
const $ = cheerio.load(html);
// DEBUG purpose stuff
var loopCounter = 0;
var climate_A_Elements = $("a:contains(climate)", html).length;
console.log("Number of articles found: " climate_A_Elements);
//
// Get all articles that contain 'climate' in the <a /> element
var allFoundArticles = $('a:contains("climate")', html);
// Iterate through all found articles using cheerio forEach loop
///////////////////////////
const articlesData = []; // <--------- Added
///////////////////////////
allFoundArticles
.each(function () {
// Assign article title and url it is located at to variables
var title = $(this).text();
var url = $(this).attr("href");
// Push the previously defined vars to the previously defined array
articlesData.push({
title,
url,
});
// Add to loop count for debugging purposes and log in console
loopCounter = 1;
console.log("Loops: " loopCounter);
})
// Output the article details to page as response
//////////////////////////
res.json(articlesData); // <--------- Moved down
//////////////////////////
});
});
app.listen(3000);
Test it:
$ curl localhost:3000/news | jq 'length'
14
$ curl localhost:3000/news | jq '.[0].title'
"Australia news Australia???s banks likely to reduce lending to regions and sectors at risk of climate change impacts, regulator says "
Dropping the extraneous code gives a pretty simple result:
const app = require("express")();
const axios = require("axios");
const cheerio = require("cheerio");
app.get("/news", (req, res) => {
const url = "https://www.theguardian.com/environment/climate-crisis";
axios.get(url).then(({data}) => {
const $ = cheerio.load(data);
const articles = [...$('a:contains("climate")')].map(e => ({
title: $(e).text().trim(),
url: $(e).attr("href"),
}));
res.json(articles);
})
.catch(err => {
res.status(500).json({message: err.message});
});
});
app.listen(3000);
Or with async
/await
:
app.get("/news", async (req, res) => {
try {
const url = "https://www.theguardian.com/environment/climate-crisis";
const {data} = await axios.get(url);
const $ = cheerio.load(data);
const articles = [...$('a:contains("climate")')].map(e => ({
title: $(e).text().trim(),
url: $(e).attr("href"),
}));
res.json(articles);
}
catch (err) {
res.status(500).json({message: err.message});
}
});
CodePudding user response:
I had a problem with the same error.
You are trying to send a response multiple times with res.json(articlesData)
Try this:
app.get('/news', (req, res) => {
axios.get('https://www.theguardian.com/environment/climate-crisis')
.then((response) => {
// Store the html retrieved via the axios http GET request
const html = response.data;
// Load the retrieved html into the cheerio instance and store to $ cheerio selector
const $ = cheerio.load(html);
// DEBUG purpose stuff
var loopCounter = 0;
var climate_A_Elements = $('a:contains(climate)', html).length;
console.log('Number of articles found: ' climate_A_Elements);
//
// Get all articles that contain 'climate' in the <a /> element
var allFoundArticles = $('a:contains("climate")', html);
// Iterate through all found articles using cheerio forEach loop
allFoundArticles.each(function () {
// Assign article title and url it is located at to variables
var title = $(this).text();
var url = $(this).attr('href');
// Push the previously defined vars to the previously defined array
articlesData.push({
title,
url,
});
// Output the article details to page as response
// Add to loop count for debugging purposes and log in console
loopCounter = 1;
console.log('Loops: ' loopCounter);
}).then(res.json(articlesData))
.catch(err => {
console.log(err);
})
})
})