I am writing a Next.js api page that parse the data from a series of URLs. All of the URLs that contain the tick character ' do not return data while the URLs that without it do return data. When I run the same code in Chrome's dev console or a RESTful Client, the URLs with the tick character ' successfully return data. Both scenarios successfully connect (HTTP 200), but results are not found when running in Next.js project. Does anyone know what is causing this difference in behavior?
UPDATES:
- I have tried both
encodeURIandencodeURIComponent. They do not fix the issue. - I have also tried using
AYR%27KA. Does not fix issue. - When I compare the URL passed to
fetchto theresponse.url, the values match in the browser but in Next.jsAYR'KAchanges toAYR%27KA. - After working with @Phil and doing a lot of digging, this issue is caused by Node.js's URL default component. Here is the NPM link and here is the link to their github. When node-fetch builds a
Requestobject, they callURL.parseon the passed string. Here is a link toURL.parsein the file "url.js" on their github. On lines 353-360, they escape all url component characters from an array namedautoEscape. On line 70 whereautoEscape, we can see that the apostrophe/tick mark character'is included in the list of always escaped characters with a comment acknowledging that this is being done despite it not being a restrict character. (NOTE: This update was made on July 24th 2021 and references line numbers and variable names of the latest version of the code at this time.)
*NOTE: To prevent CORS, run in Browser Dev Tools from the site here
const worksEveryWhereURL = "https://robertsspaceindustries.com/api/starmap/star-systems/STANTON",
brokenInNodeURL = "https://robertsspaceindustries.com/api/starmap/star-systems/AYR'KA";
async function getJSONFrom(url){
const opts = {method: "POST"};
let response, json;
console.log("url: ",url)
response = await fetch( url, {method:"POST"} );
console.log("response url: ",response.url)
json = response.json();
return json;
}
function testResponse(json) {
try {
return json.data.resultset.length > 0;
} catch(err) {
return false;
}
}
const example1 = await getJSONFrom( worksEveryWhereURL );
const example2 = await getJSONFrom( brokenInNodeURL );
console.log( testResponse(example1) ); //"true" in chrome and "true" in next.js api page
console.log( testResponse(example2) ); //"true" in chrome and "false" in next.js api page
//Output in browser console when run in browser:
//url: https://robertsspaceindustries.com/api/starmap/star-systems/STANTON
//response url: https://robertsspaceindustries.com/api/starmap/star-systems/STANTON
//url: https://robertsspaceindustries.com/api/starmap/star-systems/AYR'KA
//response url: https://robertsspaceindustries.com/api/starmap/star-systems/AYR'KA <-- matches before fetch
//true
//true <-- expected result
//Output in Command Prompt window when run in Next api page:
//url: https://robertsspaceindustries.com/api/starmap/star-systems/STANTON
//response url: https://robertsspaceindustries.com/api/starmap/star-systems/STANTON
//url: https://robertsspaceindustries.com/api/starmap/star-systems/AYR'KA
//response url: https://robertsspaceindustries.com/api/starmap/star-systems/AYR%27KA <-- why changed?
//true
//false <-- unexpect result, note change
Just for reference, here is the code that I am using as an api page that is returning an empty resultset:
export default async function handler(req, res) {
const worksEveryWhereURL = "https://robertsspaceindustries.com/api/starmap/star-systems/STANTON",
brokenInNodeURL = "https://robertsspaceindustries.com/api/starmap/star-systems/AYR'KA";
async function getJSONFrom(url){
const opts = {method: "POST"};
let response, json;
response = await fetch( url, {method:"POST"} );
json = response.json();
return json;
}
function testResponse(json) {
try {
return json.data.resultset.length > 0;
} catch(err) {
return false;
}
}
const example1 = await getJSONFrom( worksEveryWhereURL );
const example2 = await getJSONFrom( brokenInNodeURL );
console.log( testResponse(example1) ); //"true" in chrome and "true" in next.js api page
console.log( testResponse(example2) ); //"true" in chrome and "false" in next.js api page
}
//Output in Command Prompt window when run in Next api page:
//url: https://robertsspaceindustries.com/api/starmap/star-systems/STANTON
//response url: https://robertsspaceindustries.com/api/starmap/star-systems/STANTON
//url: https://robertsspaceindustries.com/api/starmap/star-systems/AYR'KA
//response url: https://robertsspaceindustries.com/api/starmap/star-systems/AYR%27KA <-- why changed?
//true
//false <-- unexpect result, note change
When the response returns incorrectly, it looks like this:
{
"success": 1,
"code": "OK",
"msg": "OK",
"data": {
"rowcount": 0,
"totalrows": 0,
"estimatedrows": false,
"pagesize": 0,
"pagecount": null,
"page": 1,
"offset": 0,
"startrow": 0,
"resultset": []
}
}
While a correct response looks like:
{
"success": 1,
"code": "OK",
"msg": "OK",
"data": {
"rowcount": 1,
"totalrows": 1,
"estimatedrows": false,
"pagesize": 0,
"pagecount": null,
"page": 1,
"offset": 0,
"startrow": 0,
"resultset": [
{
"id": "398",
"code": "AYR'KA",
"description": "Primarily a military system focused on supporting, housing, and training Xi’an ground troops. Its close proximity to the Perry Line made it strategically important during the Human / Xi’an cold war. Once relations normalized, Xi’an forces from the Perry Line systems withdrew to here. ",
"frost_line": "179.10000000",
...
}
]
}
}
From Update 4: Code snippets from url.js on github that are causing my target URL to be unreachable (not my code; code from a dependency's dependency).
-Lines 56-70 including their comments:
/*
* RFC 2396: characters reserved for delimiting URLs.
* We actually just auto-escape these.
*/
delims = [
'<', '>', '"', '`', ' ', '\r', '\n', '\t'
],
// RFC 2396: characters not allowed for various reasons.
unwise = [
'{', '}', '|', '\\', '^', '`'
].concat(delims),
// Allowed by RFCs, but cause of XSS attacks. Always escape these.
autoEscape = ['\''].concat(unwise),
-And here are lines 348-361 including comments were the previous list of characters are used:
/*
* First, make 100% sure that any "autoEscape" chars get
* escaped, even if encodeURIComponent doesn't think they
* need to be.
*/
for (var i = 0, l = autoEscape.length; i < l; i++) {
var ae = autoEscape[i];
if (rest.indexOf(ae) === -1) { continue; }
var esc = encodeURIComponent(ae);
if (esc === ae) {
esc = escape(ae);
}
rest = rest.split(ae).join(esc);
}