Recursive Promise in javascript
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
using an XMLHttpRequest
. Then, on load, check the HTTP Status for something in the 300 range, or if it has a responseURL
attached to the object and that url is different than the it was one handed.
If neither of these are true, I resolve(url)
. Otherwise, I recursively call getRedirectUrl()
on the response URL, and resolve()
.
Here's my code:
function getRedirectUrl(url, maxRedirects) {
maxRedirects = maxRedirects || 0;
if (maxRedirects > 10) {
throw new Error("Redirected too many times.");
}
var xhr = new XMLHttpRequest();
var p = new Promise(function (resolve) {
xhr.onload = function () {
var redirectsTo;
if (this.status < 400 && this.status >= 300) {
redirectsTo = this.getResponseHeader("Location");
} else if (this.responseURL && this.responseURL != url) {
redirectsTo = this.responseURL;
}
if (redirectsTo) {
// check that redirect address doesn't redirect again
// **problem line**
p.then(function () { self.getRedirectUrl(redirectsTo, maxRedirects + 1); });
resolve();
} else {
resolve(url);
}
}
xhr.open('HEAD', url, true);
xhr.send();
});
return p;
}
Then to use this function I do something like:
getRedirectUrl(myUrl).then(function (url) { ... });
The issue is that resolve();
in getRedirectUrl
will call the then()
from the calling function before it calls the getRedirectUrl
recursive call, and at that point, the URL is undefined
.
I tried, rather than p.then(...getRedirectUrl...)
doing return self.getRedirectUrl(...)
but this will never resolve.
My guess is that the pattern I'm using (that I basically came up with on the fly) isn't right, altogether.
Solution 1:
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;
});
}
Solution 2:
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'));