5

I want to program Node.js http proxy that will be able to modify response body. I have done this so far:

http = require('http'),
httpProxy = require('http-proxy');

var proxy = httpProxy.createProxyServer();
http.createServer( function (req, res){
    //here I want to change the body I guess
    proxy.web(req, res, {
        target: req.url
    });
}).listen(8013);

I have tried to use res.write(), but it gives me an error "Can't set headers after they are sent". Well I don't want to change headers, I want to change body.
How do I change the body? Any suggestions will be appreciated.

Jakub Tětek
  • 161
  • 1
  • 1
  • 7

2 Answers2

5

You get that error because writing to a HTTP response necessarily changes the headers; the Content-Length must be correct.

My approach to this problem was to buffer the entire response body, use cheerio to parse and fiddle, and then send the result out to the client.

I accomplished this by monkey patching res.writeHead, res.write, and res.end before handing the request off to the http-proxy module.

function requestHandler(req, res) {
    var writeHead = res.writeHead, write = res.write, end = res.end;

    res.writeHead = function(status, reason, headers) {
        if (res.headersSent) return req.socket.destroy(); // something went wrong; abort
        if (typeof reason == 'object') headers = reason;
        headers = headers || {};
        res.headers = headers;
        if (headers['content-type'] && headers['content-type'].substr(0,9) == 'text/html') { // we should only fiddle with HTML responses
            delete headers['transfer-encoding']; // since we buffer the entire response, we'll send a proper Content-Length later with no Transfer-Encoding.

            var buf = new Buffer();
            res.write = function(data, encoding) {
                if (Buffer.isBuffer(data)) buf = Buffer.concat([buf, data]); // append raw buffer
                else buf = Buffer.concat([buf, new Buffer(data, encoding)]); // append string with optional character encoding (default utf8)

                if (buf.length > 10 * 1024 * 1024) error('Document too large'); // sanity check: if the response is huge, bail.
                // ...we don't want to let someone bring down the server by filling up all our RAM.
            }

            res.end = function(data, encoding) {
                if (data) res.write(data, encoding);

                var $ = cheerio.load(buf.toString());

                // This is where we can modify the response.  For example,
                $('body').append('<p>Hi mom!</p>');

                buf = new Buffer($.html()); // we have to convert back to a buffer so that we can get the *byte count* (rather than character count) of the body

                res.headers['content-type'] = 'text/html; charset=utf-8'; // JS always deals in UTF-8.
                res.headers['content-length'] = buf.length;

                // Finally, send the modified response out using the real `writeHead`/`end`:
                writeHead.call(res, status, res.headers);
                end.call(res, buf);
            }


        } else {
            writeHead.call(res, status, headers); // if it's not HTML, let the response go through normally
        }
    }

    proxy.web(req, res, {
        target: req.url
    });

    function error(msg) { // utility function to report errors
        if (res.headersSent) end.call(res, msg);
        else {
            msg = new Buffer(msg);
            writeHead.call(res, 502, { 'Content-Type': 'text/plain', 'Content-Length': msg.length });
            end.call(res, msg);
        }
    }
}
josh3736
  • 139,160
  • 33
  • 216
  • 263
  • I have tried your code and at this time the headers are always empty so the "modify html" part is never called. Any idea? I just copied your code and tried against another local server. – Kev Apr 24 '14 at 12:13
  • I'm trying to use this, and it seems to be close, but my response body is always empty. Any idea why? It looks like the "real" `write` is never called - does it need to be? – xdumaine Oct 07 '14 at 12:51
  • `end.call(res, buf)` writes `buf` to the real response and ends it in one call. What is `buf` at that point? – josh3736 Oct 07 '14 at 18:14
  • 1
    +1. In addition, I think that `Buffer`'s constructor needs some value, so `new Buffer()` might be changed to `new Buffer(0)` – Ron Klein Aug 19 '15 at 22:41
  • The real write() does need to be called, indirectly -- it is invoked by the real end(), and if the monkey patch is left in place this will just loop back to the dummy write and not output anything at all. Doing `res.write = write;` before `end.call(res, buf);` worked for me. – Another Code Nov 14 '15 at 00:44
1

Check out this sample from the git repository of http-proxy module

https://github.com/nodejitsu/node-http-proxy/blob/master/examples/middleware/modifyResponse-middleware.js

Murukesh
  • 600
  • 7
  • 15