2012-04-17

Node.js: expanding shortened URLs

This blog post explains how one can use Node.js to expand a URL that has been shortened by a service such as t.co (built into Twitter) and bit.ly. We’ll look at a simple implementation and at an advanced implementation that uses promises.

The minimum

You need to install Mikeal Rogers’ request module:
     npm install request
That module automatically follows all redirects from the shortened URL. Once you are at your final destination, you only need to find out where you are:
    var request = require("request");
    
    function expandUrl(shortUrl) {
        request( { method: "HEAD", url: shortUrl, followAllRedirects: true },
            function (error, response) {
                console.log(response.request.href);
            });
    }

Prettier with promises

If you want to write a function that returns the expanded URL, more work is needed. You have the option of using a callback, but promises usually lead to prettier code. Let’s use Kris Kowal’s Q module:
    npm install q
The “promising” code looks as follows.
    var Q = require("q");
    var request = require("request");
    function expandUrl(shortUrl) {
        return Q.ncall(request, null, {
            method: "HEAD",
            url: shortUrl,
            followAllRedirects: true
        // If a callback receives more than one (non-error) argument
        // then the promised value is an array. We want element 0.
        }).get('0').get('request').get('href');
    }
Node that the callback created by deferred.node() automatically handles errors. Invoking the function works like this:
    expandUrl("http://t.co/Zc3cUoly")
    .then(function (longUrl) {
        console.log(longUrl);
    });

Related reading

18 comments:

Mathias Bynens said...

It can be done without any dependencies in just a few lines of code. See https://github.com/mathiasbynens/node-unshorten.

Mario Volke said...

You should use a HEAD request. For unshortening of URLs you usually don't need the actual content of a page.

Axel Rauschmayer said...

 Good point. Fixed.

Kris Kowal said...

Might be able to make that even prettier:

var Q = require("q");
var request = require("request");

function expandUrl(shortUrl) {
var deferred = Q.defer();
request(
{ method: "HEAD", url: shortUrl, followAllRedirects: true },
deferred.node()
);
return deferred.promise
.get('request')
.get('href');
}

The error propagates through the .get promises.

Axel Rauschmayer said...

 Very nice. I wasn’t aware of this feature. When I first read it, it wasn’t obvious to me what method `node` did. If I figured it out correctly, then better names might be: createNodejsCallback or getNodejsCallback (or something similar). Rationale: `node` is too overloaded a term and I would like to see the word `callback`, along with an indicator of whether a new instance is created each time or the instance is cached.

Kris Kowal said...

Added https://github.com/kriskowal/q/issues/55

Thanks.

Kris Kowal said...

It could be even further abbreviated with Q.ncall.


    var Q = require("q");
    var request = require("request");
    function expandUrl(shortUrl) {
        return Q.ncall(request, null, {
            method: "HEAD",
            url: shortUrl,
            followAllRedirects: true
        }).get('request').get('href');
    }


As in my previous post, the error, if there is one, propagates through the .get promises.  The null is this for the function.

Axel Rauschmayer said...

 Enough now, cannot handle any more beauty. ;-)

Alas, it doesn’t completely work:

>     expandUrl("http://t.co/Zc3cUoly").then(function (longUrl) {
...         console.log(longUrl);
...     });
{ promiseSend: [Function],
  valueOf: [Function] }

medikoo said...

...and with `deferred` promise implementation it would look like:

var request = require('deferred').promisify(require("request"));

function expandUrl(shortUrl) {
return request( { method: "HEAD", url: shortUrl, followAllRedirects: true } )
.get(0, 'request', 'href');
}

expandUrl("http://t.co/Zc3cUoly")(console.log).end();

;)

Axel Rauschmayer said...

I’ve adopted this solution, with a small bug fix (the promised value is an array, hence you need a get('0') first.

medikoo said...

With 'deferred' promise implementation, it may look as easy as:

var request = require('deferred').promisify(require("request"));function expandUrl(shortUrl) {    return request( { method: "HEAD", url: shortUrl, followAllRedirects: true } )        .get(0, 'request', 'href');}expandUrl("http://t.co/Zc3cUoly")(console.log).end();

;-)

medikoo said...

I wonder what's the secret of providing readable source code on disquiss both 'code' and 'pre' seem to fail :/

Axel Rauschmayer said...

 pre works for me. Try again, possibly with br at the end.

medikoo said...

hmm.. 'br' in 'pre'? It's definitely not conventional method, I'try it.

medikoo said...

With 'deferred' promise implementation, it may look as easy as:

var request = require('deferred').promisify(require("request"));

function expandUrl(shortUrl) {
    return request( { method: "HEAD", url: shortUrl, followAllRedirects: true } )
        .get(0, 'request', 'href');
}

expandUrl("http://t.co/Zc3cUoly")(console.log).end();

;-)

medikoo said...

It works, I need to note it somewhere ;-)
You can remove this broken post, thanks

Axel Rauschmayer said...

 You should be able to edit existing comments. If it works, you could edit the root of this tree and I’ll remove the duplicate. If it doesn’t work, I’ll remove the root of this tree.

medikoo said...

I think it's better to remove this tree, as after this post is fixed discussion below wouldn't make sense to newcomers.

Other thing, when rules for formatting are not intuitive, it would be good to have some small note explaining them (disqus just says what elements we can use, but it doesn't say that 'pre' won't work as expected).
e.g. "Please use 'pre' with 'br' for line endings for code markup"

Web Analytics