102

I'm trying to wrap http.request into Promise:

 new Promise(function(resolve, reject) {
    var req = http.request({
        host: '127.0.0.1',
        port: 4000,
        method: 'GET',
        path: '/api/v1/service'
    }, function(res) {
        if (res.statusCode < 200 || res.statusCode >= 300) {
            // First reject
            reject(new Error('statusCode=' + res.statusCode));
            return;
        }
        var body = [];
        res.on('data', function(chunk) {
            body.push(chunk);
        });
        res.on('end', function() {
            try {
                body = JSON.parse(Buffer.concat(body).toString());
            } catch(e) {
                reject(e);
                return;
            }
            resolve(body);
        });
    });
    req.on('error', function(err) {
        // Second reject
        reject(err);
    });
    req.write('test');
}).then(function(data) {
    console.log(data);
}).catch(function(err) {
    console.log(err);
});

If I recieve errornous statusCode from remote server it will call First reject and after a bit of time Second reject. How to make properly so it calls only single reject (I think First reject is proper one in this case)? I think I need to close res myself, but there is no close() method on ClientResponse object.

UPD: Second reject triggers very rarely - why?

happy_marmoset
  • 2,137
  • 3
  • 20
  • 25
  • Do you understand that a second `reject()` on the same promise does nothing? – jfriend00 Jul 23 '16 at 05:12
  • Yes, I have learned it after creating question. I learned it from here http://stackoverflow.com/questions/18217640/what-happens-if-i-reject-resolve-multiple-times-in-kriskowals-q. And he says "it should be avoided to reduce confusion of a reader". That is I'm exactly trying to do. I want to get some Promise implementation like `lie.js`, and add `Promise.enableStrictMode()` which should make firing exception on second resolve/reject call, not just ignore. – happy_marmoset Jul 23 '16 at 17:00
  • What I would do is change the inner `reject` on non 2xx and 3xx to a `resolve` and handle http error codes elsewhere. – ThisGuyCantEven Feb 13 '23 at 15:47

6 Answers6

150

Your code is almost fine. To restate a little, you want a function that wraps http.request with this form:

function httpRequest(params, postData) {
    return new Promise(function(resolve, reject) {
        var req = http.request(params, function(res) {
            // on bad status, reject
            // on response data, cumulate it
            // on end, parse and resolve
        });
        // on request error, reject
        // if there's post data, write it to the request
        // important: end the request req.end()
    });
}

Notice the addition of params and postData so this can be used as a general purpose request. And notice the last line req.end() -- which must always be called -- was missing from the OP code.

Applying those couple changes to the OP code...

function httpRequest(params, postData) {
    return new Promise(function(resolve, reject) {
        var req = http.request(params, function(res) {
            // reject on bad status
            if (res.statusCode < 200 || res.statusCode >= 300) {
                return reject(new Error('statusCode=' + res.statusCode));
            }
            // cumulate data
            var body = [];
            res.on('data', function(chunk) {
                body.push(chunk);
            });
            // resolve on end
            res.on('end', function() {
                try {
                    body = JSON.parse(Buffer.concat(body).toString());
                } catch(e) {
                    reject(e);
                }
                resolve(body);
            });
        });
        // reject on request error
        req.on('error', function(err) {
            // This is not a "Second reject", just a different sort of failure
            reject(err);
        });
        if (postData) {
            req.write(postData);
        }
        // IMPORTANT
        req.end();
    });
}

This is untested, but it should work fine...

var params = {
    host: '127.0.0.1',
    port: 4000,
    method: 'GET',
    path: '/api/v1/service'
};
// this is a get, so there's no post data

httpRequest(params).then(function(body) {
    console.log(body);
});

And these promises can be chained, too...

httpRequest(params).then(function(body) {
    console.log(body);
    return httpRequest(otherParams);
}).then(function(body) {
    console.log(body);
    // and so on
});
danh
  • 62,181
  • 10
  • 95
  • 136
  • 2
    This is a great answer. I am new to nodejs and async processing, but I am trying to execute a sequence of HTTP requests. All those requests should be in sequence, and I cannot use any external libraries. I am not sure however if your answer allows to then run multiple HTTP requests in sequence. Does it? – Slav May 30 '17 at 20:56
  • 3
    @Slav, sure. I edited to illustrate right at the end. – danh May 30 '17 at 21:46
  • Came here because I was getting a lot of issues with `UnhandledPromiseRejectionWarning`s. Turns out I was missing the `return` in the Promises chaining. Thank you! – ocramot May 07 '20 at 14:41
  • 1
    Great transformation for error handling by rejecting with an error! It's crucial for try catches in async/await functions. – pegasuspect Mar 30 '21 at 13:14
  • i had to do `body.push(Buffer.from(chunk))` for it to work – galki Dec 01 '21 at 18:31
26

I know this question is old but the answer actually inspired me to write a modern version of a lightweight promisified HTTP client. Here is a new version that:

  • Use up to date JavaScript syntax
  • Validate input
  • Support multiple methods
  • Is easy to extend for HTTPS support
  • Will let the client decide on how to deal with response codes
  • Will also let the client decide on how to deal with non-JSON bodies

Code below:

function httpRequest(method, url, body = null) {
    if (!['get', 'post', 'head'].includes(method)) {
        throw new Error(`Invalid method: ${method}`);
    }

    let urlObject;

    try {
        urlObject = new URL(url);
    } catch (error) {
        throw new Error(`Invalid url ${url}`);
    }

    if (body && method !== 'post') {
        throw new Error(`Invalid use of the body parameter while using the ${method.toUpperCase()} method.`);
    }

    let options = {
        method: method.toUpperCase(),
        hostname: urlObject.hostname,
        port: urlObject.port,
        path: urlObject.pathname
    };

    if (body) {
        options.headers = {'Content-Length':Buffer.byteLength(body)};
    }

    return new Promise((resolve, reject) => {

        const clientRequest = http.request(options, incomingMessage => {

            // Response object.
            let response = {
                statusCode: incomingMessage.statusCode,
                headers: incomingMessage.headers,
                body: []
            };

            // Collect response body data.
            incomingMessage.on('data', chunk => {
                response.body.push(chunk);
            });

            // Resolve on end.
            incomingMessage.on('end', () => {
                if (response.body.length) {

                    response.body = response.body.join();

                    try {
                        response.body = JSON.parse(response.body);
                    } catch (error) {
                        // Silently fail if response is not JSON.
                    }
                }

                resolve(response);
            });
        });
        
        // Reject on request error.
        clientRequest.on('error', error => {
            reject(error);
        });

        // Write request body if present.
        if (body) {
            clientRequest.write(body);
        }

        // Close HTTP connection.
        clientRequest.end();
    });
}
Nicolas Bouvrette
  • 4,295
  • 1
  • 39
  • 53
  • 1
    Lovely work! I've just updated the content for it to pass with typescript and all its constraints: https://gist.github.com/aarohijohal/134d28598271c03747a80c2d4e732b68 – R.J. Dec 13 '19 at 00:01
  • `options.headers` is undefined, and setting 'Content-Length' will give a TypeError. – Serge N May 09 '20 at 10:19
  • quite nice, but I guess response.body = response.body.join(); must be response.body = response.body.join(''); , otherwise there will be random commas added in the content. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/join -> If omitted, the array elements are separated with a comma – wosis Dec 02 '20 at 14:35
  • Nice work! Just make sure to add PUT method as well to your validation list and also to allow data body for PUT. – ThisGuyCantEven Jul 14 '21 at 20:48
  • also forgot http PATCH – ThisGuyCantEven Mar 24 '22 at 00:15
13

There are other ways as well but here you can find a simple way to make http.request as a promise or async/await type.

Here is a working sample code:

var http = require('http');

function requestAsync(name) {

    return new Promise((resolve, reject) => {
        var post_options = {
            host: 'restcountries.eu',
            port: '80',
            path: `/rest/v2/name/${name}`,
            method: 'GET',
            headers: {
                'Content-Type': 'application/json'
            }
        };
        let post_req = http.request(post_options, (res) => {
            res.setEncoding('utf8');
            res.on('data', (chunk) => {
                resolve(chunk);
            });
            res.on("error", (err) => {
                reject(err);
            });
        });
        post_req.write('test');
        post_req.end();
    });
}

//Calling request function
//:1- as promise
requestAsync("india").then(countryDetails => {
    console.log(countryDetails);
}).catch((err) => {
    console.log(err);  
}); 

//:2- as await
let countryDetails = await requestAsync("india");
iSkore
  • 7,394
  • 3
  • 34
  • 59
Deepak
  • 1,510
  • 1
  • 14
  • 27
  • For me the `as await` one gives `SyntaxError: await is only valid in async function but it is promise` – Marek Dec 06 '20 at 09:39
  • ok nvm, problem was that it has to be called INSIDE of an async function. Thanks for great example! – Marek Dec 06 '20 at 09:49
  • ` post_req.write('test');` this line is not necessary, right? – tim Aug 01 '23 at 21:18
5

After reading all of these and a few articles, I thought I'd post a sort of "general" solution that handles both http and https:

const http = require("http");
const https = require("https");
const url_obj = require("url");

const request = async (url_string, method = "GET", postData = null) => {
  const url = url_obj.parse(url_string);
  const lib = url.protocol=="https:" ? https : http;
  const params = {
    method:method,
    host:url.host,
    port: url.port || url.protocol=="https:" ? 443 : 80,
    path: url.path || "/"
  };
  return new Promise((resolve, reject) => {
    const req = lib.request(params, res => {
      if (res.statusCode < 200 || res.statusCode >= 300) {
        return reject(new Error(`Status Code: ${res.statusCode}`));
      }
      const data = [];
      res.on("data", chunk => {
        data.push(chunk);
      });
      res.on("end", () => resolve(Buffer.concat(data).toString()));
    });
    req.on("error", reject);
    if (postData) {
      req.write(postData);
    }
    req.end();
  });
}

You could use like this:

request("google.com").then(res => console.log(res)).catch(err => console.log(err))

This is heavily inspired by this article, but replaces the hacky url parsing with the built in api.

ThisGuyCantEven
  • 1,095
  • 12
  • 21
-3

Hope this help.

const request = require('request');

async function getRequest() {
  const options = {
    url: 'http://example.com',
    headers: {
      'Authorization': 'Bearer xxx'
    }
  };

  return new Promise((resolve, reject) => {
    return request(options, (error, response, body) => {
      if (!error && response.statusCode == 200) {
        const json = JSON.parse(body);
        return resolve(json);
      } else {
        return reject(error);
      }
    });
  })
}
Binh Ho
  • 3,690
  • 1
  • 31
  • 31
-4

It's easier for you to use bluebird api, you can promisify request module and use the request function async as a promise itself, or you have the option of using the module request-promise, that makes you to not working to creating a promise but using and object that already encapsulates the module using promise, here's an example:

var rp = require('request-promise');

rp({host: '127.0.0.1',
    port: 4000,
    method: 'GET',
    path: '/api/v1/service'})
    .then(function (parsedBody) {
        // GET succeeded... 
    })
    .catch(function (err) {
        // GET failed... 
    });
  • I don't need a solution for my boss or work, I'm working as PHP/Python developer. Instead I need an understanding of anatomy of NodeJS external http request and learn how to promisify things. I have tried to look at `@request-promise-core`, but code is unreadable, I haven't learned anything. The good thing I have learning that Promise is cannot be rejected twice :) Second reject will be ignored. But still don't understand how to turn strict mode in Promise, so second reject will throw error instead of being ignored. – happy_marmoset Jul 23 '16 at 03:01
  • 2
    @happy_marmoset - A promise is done once it's been rejected. If you're expecting it to report a second error, then you're headed the wrong direction with your design. If you need two separate errors, then you will need two separate promises. The whole promise model is that once there's an error, it rejects and there is no more execution of that particular async operation. The caller, from outside the promise, can "handle" the rejection and decide what to do next, but the result of the async operation (what the promise communicates back) is done at the point it rejects. – jfriend00 Jul 23 '16 at 08:55
  • 1
    This suggestion sounds like 10 years ago when people used jquery and claimed to know javascript – Purefan Dec 11 '18 at 13:13
  • 1
    This is actually the best answer for people who just want to do http with promises instead of callbacks. If you also want to learn some stuff (like the OP) then sure, write your own wrapper -- knock yourself out... – Nick Perkins Dec 16 '18 at 20:37
  • 2
    `request` and its dependents `request-promise` and `request-promise-native` are deprecated as of Feb 11, 2020. – Raketenolli Mar 16 '20 at 14:45