I\'m writing a Javascript Promise
that finds the final redirect URL of a link.
What I\'m doing is making a HEAD
request in a Promise<
The problem is that the promise you return from getRedirectUrl()
needs to include the entire chain of logic to get to the URL. You're just returning a promise for the very first request. The .then()
you're using in the midst of your function isn't doing anything.
To fix this:
Create a promise that resolves to redirectUrl
for a redirect, or null
otherwise:
function getRedirectsTo(xhr) {
if (xhr.status < 400 && xhr.status >= 300) {
return xhr.getResponseHeader("Location");
}
if (xhr.responseURL && xhr.responseURL != url) {
return xhr.responseURL;
}
return null;
}
var p = new Promise(function (resolve) {
var xhr = new XMLHttpRequest();
xhr.onload = function () {
resolve(getRedirectsTo(xhr));
};
xhr.open('HEAD', url, true);
xhr.send();
});
Use .then()
on that to return the recursive call, or not, as needed:
return p.then(function (redirectsTo) {
return redirectsTo
? getRedirectUrl(redirectsTo, redirectCount+ 1)
: url;
});
Full solution:
function getRedirectsTo(xhr) {
if (xhr.status < 400 && xhr.status >= 300) {
return xhr.getResponseHeader("Location");
}
if (xhr.responseURL && xhr.responseURL != url) {
return xhr.responseURL;
}
return null;
}
function getRedirectUrl(url, redirectCount) {
redirectCount = redirectCount || 0;
if (redirectCount > 10) {
throw new Error("Redirected too many times.");
}
return new Promise(function (resolve) {
var xhr = new XMLHttpRequest();
xhr.onload = function () {
resolve(getRedirectsTo(xhr));
};
xhr.open('HEAD', url, true);
xhr.send();
})
.then(function (redirectsTo) {
return redirectsTo
? getRedirectUrl(redirectsTo, redirectCount + 1)
: url;
});
}
If you're in an environment that supports async
/await
(virtually all modern environments do), you can write an async function
that looks a bit more like a recursive function pattern we all know and love. It's not possible to completely avoid a Promise
due to the nature of XMLHttpRequest
only retrieving a value via the load
event (rather than exposing a Promise
itself), but the recursive nature of the function that makes the call should look familiar.
Having four more years of JavaScript experience than I had when I originally wrote this question, I cleaned up the code a bit, but it works essentially the same way.
// creates a simple Promise that resolves the xhr once it has finished loading
function createXHRPromise(url) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
// addEventListener('load', ...) is basically the same as setting
// xhr.onload, but is better practice
xhr.addEventListener('load', () => resolve(xhr));
// throw in some error handling so that the calling function
// won't hang
xhr.addEventListener('error', reject);
xhr.addEventListener('abort', reject);
xhr.open('HEAD', url, true);
xhr.send();
});
}
async function getRedirectUrl(url, maxRetries = 10) {
if (maxRetries <= 0) {
throw new Error('Redirected too many times');
}
const xhr = await createXHRPromise(url);
if (xhr.status >= 300 && xhr.status < 400) {
return getRedirectUrl(xhr.getResponseHeader("Location"), maxRetries - 1);
} else if (xhr.responseURL && xhr.responseURL !== url) {
return getRedirectUrl(xhr.responseURL, maxRetries - 1);
}
return url;
}
async
/await
async function
is syntactic sugar for a Promise
await
is syntactic sugar for Promise.then()
return
within an async function
is syntactic sugar for resolve()
throw
within an async function
is syntactic sugar for reject()
If an async function
returns either another async function
call or a Promise
, the function/promise will resolve before the original call resolves, exactly the same way that resolving a Promise
would in the Promise
pattern.
So, you can call getRedirectUrl(someUrl).then(...).catch(...)
exactly the same way the original question would have.
It should probably be noted that using an XHR to resolve a redirected URL will fail for any URL that doesn't include the proper CORS header.
As an added bonus, async/await makes an iterative approach trivial.
async function getRedirectUrl(url, maxRetries = 10) {
for (let i = 0; i < maxRetries; i++) {
const xhr = await createXHRPromise(url);
if (xhr.status >= 300 && xhr.status < 400) {
url = xhr.getResponseHeader("Location");
} else if (xhr.responseURL && xhr.responseURL !== url) {
url = xhr.responseURL;
} else {
return url;
}
}
throw new Error('Redirected too many times');
}
Here's the simplified solution:
const recursiveCall = (index) => {
return new Promise((resolve) => {
console.log(index);
if (index < 3) {
return resolve(recursiveCall(++index))
} else {
return resolve()
}
})
}
recursiveCall(0).then(() => console.log('done'));
Please check below example for Recursive Promise in javascript/typescript, It will not resolve the promise until number incremented to greater then 13.
below code is suitable for typescript and modify it slightly for javascript.
async iterate(number: number): Promise<any> {
return new Promise((resolve, reject) => {
let number = 0;
if (number > 13) {
// recursive terminate condition
resolve(number);
return;
} else {
number = number + 1;
// recursive call
this.iterate(number).then(resolve);
}
});
}
this.iterate().then((resolvedData: any) => {
// wait until number is not greater than 13
console.log(resolvedData);
});
Please check below example it will return factorial
of a given number as we did in many programming languages.
I have implemented below example using JavaScript
promises.
let code = (function(){
let getFactorial = n =>{
return new Promise((resolve,reject)=>{
if(n<=1){
resolve(1);
}
resolve(
getFactorial(n-1).then(fact => {
return fact * n;
})
)
});
}
return {
factorial: function(number){
getFactorial(number).then(
response => console.log(response)
)
}
}
})();
code.factorial(5);
code.factorial(6);
code.factorial(7);
The following has two functions:
The secret sauce is the sub Promise whose's successful completion will trigger a call to resolve() from the parent promise.
function _getRedirectUrl( url ) {
return new Promise( function (resolve) {
const redirectUrl = {
"https://mary" : "https://had",
"https://had" : "https://a",
"https://a" : "https://little",
"https://little" : "https://lamb",
}[ url ];
setTimeout( resolve, 500, redirectUrl || url );
} );
}
function getRedirectUrl( url ) {
return new Promise( function (resolve) {
console.log("* url: ", url );
_getRedirectUrl( url ).then( function (redirectUrl) {
// console.log( "* redirectUrl: ", redirectUrl );
if ( url === redirectUrl ) {
resolve( url );
return;
}
getRedirectUrl( redirectUrl ).then( resolve );
} );
} );
}
function run() {
let inputUrl = $( "#inputUrl" ).val();
console.log( "inputUrl: ", inputUrl );
$( "#inputUrl" ).prop( "disabled", true );
$( "#runButton" ).prop( "disabled", true );
$( "#outputLabel" ).text( "" );
getRedirectUrl( inputUrl )
.then( function ( data ) {
console.log( "output: ", data);
$( "#inputUrl" ).prop( "disabled", false );
$( "#runButton" ).prop( "disabled", false );
$( "#outputLabel").text( data );
} );
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
Input:
<select id="inputUrl">
<option value="https://mary">https://mary</option>
<option value="https://had">https://had</option>
<option value="https://a">https://a</option>
<option value="https://little">https://little</option>
<option value="https://lamb">https://lamb</option>
</select>
Output:
<label id="outputLabel"></label>
<button id="runButton" onclick="run()">Run</button>
As another illustration of recursive Promises, I used it to solve a maze. The Solve()
function is invoked recursively to advance one step in a solution to a maze, else it backtracks when it encounters a dead end. The setTimeout
function is used to set the animation of the solution to 100ms per frame (i.e. 10hz frame rate).
const MazeWidth = 9
const MazeHeight = 9
let Maze = [
"# #######",
"# # #",
"# ### # #",
"# # # #",
"# # # ###",
"# # # #",
"# ### # #",
"# # #",
"####### #"
].map(line => line.split(''));
const Wall = '#'
const Free = ' '
const SomeDude = '*'
const StartingPoint = [1, 0]
const EndingPoint = [7, 8]
function PrintDaMaze()
{
//Maze.forEach(line => console.log(line.join('')))
let txt = Maze.reduce((p, c) => p += c.join('') + '\n', '')
let html = txt.replace(/[*]/g, c => '<font color=red>*</font>')
$('#mazeOutput').html(html)
}
function Solve(X, Y) {
return new Promise( function (resolve) {
if ( X < 0 || X >= MazeWidth || Y < 0 || Y >= MazeHeight ) {
resolve( false );
return;
}
if ( Maze[Y][X] !== Free ) {
resolve( false );
return;
}
setTimeout( function () {
// Make the move (if it's wrong, we will backtrack later)
Maze[Y][X] = SomeDude;
PrintDaMaze()
// Check if we have reached our goal.
if (X == EndingPoint[0] && Y == EndingPoint[1]) {
resolve(true);
return;
}
// Recursively search for our goal.
Solve(X - 1, Y)
.then( function (solved) {
if (solved) return Promise.resolve(solved);
return Solve(X + 1, Y);
} )
.then( function (solved) {
if (solved) return Promise.resolve(solved);
return Solve(X, Y - 1);
} )
.then( function (solved) {
if (solved) return Promise.resolve(solved);
return Solve(X, Y + 1);
} )
.then( function (solved) {
if (solved) {
resolve(true);
return;
}
// Backtrack
setTimeout( function () {
Maze[Y][X] = Free;
PrintDaMaze()
resolve(false);
}, 100);
} );
}, 100 );
} );
}
Solve(StartingPoint[0], StartingPoint[1])
.then( function (solved) {
if (solved) {
console.log("Solved!")
PrintDaMaze()
}
else
{
console.log("Cannot solve. :-(")
}
} );
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<pre id="mazeOutput">
</pre>