Friday, 26 May 2017

How to wrap an event emitter with a Promise less weird-like?

I'm trying to wrap http.get with a Promise. Here's what I've got:

import Http from 'http';

export function get(url) {
    let req;
    return new Promise((resolve, reject) => {
        req = Http.get(url, async res => {
            if(res.statusCode !== 200) {
                return reject(new Error(`Request failed, got status ${res.statusCode}`));
            }
            let contentLengthStr = res.headers['content-length'];
            if(contentLengthStr) {
                let contentLength = parseInt(contentLengthStr, 10);
                if(contentLength >= 0 && contentLength <= 2*1024**3) {
                    let buf = Buffer.allocUnsafe(contentLength);
                    let offset = 0;
                    res.on('data', chunk => {
                        if(chunk.length + offset > contentLength) {
                            return reject(new Error(`Received too much data, expected ${contentLength} bytes`));
                        }
                        chunk.copy(buf, offset);
                        offset += chunk.length;
                    });
                    res.on('end', () => {
                        if(offset === contentLength) {
                            resolve(buf);
                        } else {
                            return reject(new Error(`Expected ${contentLength} bytes, received ${offset}`));
                        }
                    })
                } else {
                    return reject(new Error(`Bad Content-Length header: ${contentLengthStr}`));
                }
            } else {
                return reject(new Error(`Missing Content-Length header not supported`));
            }
        });
    }).catch(err => {
        req.abort();
        throw err;
    })
}

It seems to work OK, but it feels a bit clunky.

Firstly, async/await appear to be of no help here. I can't throw nor return inside of res.on('end'. Returning just returns out of the end callback, but there's no real way for me to break out of the Http.get(url, res => { function from inside there. throwing doesn't "bubble up" to the Promise I created either because the data/end events don't fire synchronously. I have to call reject manually.

The part that really bothers me though is that if the server sends me more data than they said they were going to send me via the Content-Length header, I want to abort the request and stop listening for events. To do that I've rejected the Promise, and then immediately catch it so that I can abort the request, and then I rethrow the error so that the caller can handler it. To do this I have to declare the req variable above the Promise, and then initialise it inside the Promise, so that I can access it after the Promise.

The flow of everything just feels really clunky. Is there a nicer way to write this? (Assume I have available all ES6/ES2017+/next features)



via mpen

No comments:

Post a Comment