Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add mung.send and mung.sendAsync implementations #34

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 19 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Middleware for express responses.

This package allows synchronous and asynchronous transformation of an express response. This is a similar concept to the express middleware for a request but for a response. Note that the middleware is executed in LIFO order. It is implemented by monkey patching (hooking) the `res.end`, `res.json`, or `res.write` methods.
This package allows synchronous and asynchronous transformation of an express response. This is a similar concept to the express middleware for a request but for a response. Note that the middleware is executed in LIFO order. It is implemented by monkey patching (hooking) the `res.end`, `res.json`, `res.send`, or `res.write` methods.


## Getting started [![npm version](https://badge.fury.io/js/express-mung.svg)](https://badge.fury.io/js/express-mung)
Expand All @@ -19,7 +19,7 @@ Then in your middleware

Sample middleware (redact.js) to remove classified information.

````javascript
```javascript
'use strict';
const mung = require('express-mung');

Expand All @@ -31,12 +31,14 @@ function redact(body, req, res) {
}

module.exports = mung.json(redact);
````
```

then add to your `app.js` file (before the route handling middleware)
````javascript

```javascript
app.use(require('./redact'))
````
```

and [*That's all folks!*](https://www.youtube.com/watch?v=gBzJGckMYO4)

See the mocha [tests](https://github.com/richardschneider/express-mung/tree/master/test) for some more examples.
Expand Down Expand Up @@ -71,15 +73,25 @@ Asynchronously transform the HTTP headers of the response.

`fn(chunk, encoding, req, res)` receives the string or buffer as `chunk`, its `encoding` if applicable (`null` otherwise), `req` and `res`. It returns the modified body. If `undefined` is returned (i.e. nothing) then the original unmodified chunk is used.

### mung.send(fn, [options])

`fn(data, req, res)` receives the original `data` from original `res.send(data)`, `req` and `res`. It returns the modified data. If `undefined` is returned (i.e nothing) then the original unmodified data is used.

### mung.sendAsync(fn, [options])

Asynchronously transform the data of `res.send`.

`fn(data, req, res)` receives the original `data` from original `res.send(data)`, `req` and `res`. It returns a promise to a modified data. If `undefined` is returned (i.e nothing) then the original unmodified data is used.

### Notes

* when `mung.json*` receives a scalar value then the `content-type` is switched `text-plain`.

* when `mung.json*` detects that a response has been sent, it will abort.
* when `mung.json*` or `mung.send*` detects that a response has been sent, it will abort.

* sending a response while in `mung.headers*` is **undefined behaviour** and will most likely result in an error.

* when `mung.write` detects that a response has completed (i.e. if `res.end` has been called), it will abort.
* when `mung.write` or `mung.send*` detects that a response has completed (i.e. if `res.end` has been called), it will abort.

* calling `res.json` or `res.send` from `mung.write` can lead to unexpected behavior since they end the response internally.

Expand Down
92 changes: 92 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -211,4 +211,96 @@ mung.write = function write (fn, options = {}) {
}
}

mung.send = function (fn, options = {}) {
return function (req, res, next) {
const originalSend = res.send;

res.send = function (data) {
res.send = originalSend;
let modified;
try {
modified = fn(data, req, res);
} catch (err) {
return mung.onError(err, req, res, next);
}
// If the resp has completed, do nothing
if (res.finished) {
return res;
}
// If no returned value from fn, then set it back to the original value
if (modified === undefined) {
modified = data;
}

// Do not mung on errors
if (!options.mungError && res.statusCode >= 400) {
return originalSend.call(res, data);
}

return originalSend.call(res, modified);
}

next && next();
}
}

mung.sendAsync = function (fn, options = {}) {
return function (req, res, next) {
const originalSend = res.send;
const originalEnd = res.end;

// if end() is called outside async hook, do it actually
res.__inHook = false;
res.__isEnd = false;
res.end = function () {
res.__isEnd = true;
if (!res.__inHook) {
res.end = originalEnd;
return res.end();
}
};
res.send = function (data) {
res.end = originalEnd;
res.send = originalSend;

if (res.finished) {
return res;
}

// during async hook processing
res.__inHook = true;
try {
fn(data, req, res).then(modified => {
if (res.finished) {
return;
}
// If no returned value from the promise, then set it back to the original value
if (modified === undefined) {
modified = data;
}

// Do not mung on errors
if (!options.mungError && res.statusCode >= 400) {
originalSend.call(res, data);
return;
}
originalSend.call(res, modified);

if (res.__isEnd) {
res.end();
}
res.__inHook = false;
return;
}).catch(err => {
mung.onError(err, req, res, next)
});
} catch (err) {
mung.onError(err, req, res, next)
}
return res;
}
next && next();
}
}

module.exports = mung;
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

146 changes: 146 additions & 0 deletions test/send.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
'use strict';

let should = require('should'),
express = require('express'),
request = require('supertest'),
mung = require('..');

describe('mung send', () => {

let originalResponseTextBody
let modifiedResponseTextBody
let originalResponseJSONBody
let modifiedResponseJsonBody

beforeEach(() => {
originalResponseTextBody = 'This is the response body'
modifiedResponseTextBody = 'This is the response body with more content';
originalResponseJSONBody = {
a: 'a'
},
modifiedResponseJsonBody = {
a: 'a',
b: 'b',
}
})

function modifyText (data, req, res) {
return (data + ' with more content')
}

function modifyJson (data, req, res) {
data.b = 'b';
return JSON.parse(JSON.stringify(data));
}

function error (data, req, res) {
data.foo.bar.hopefully.fails();
}

function error403 (data, req, res) {
res.status(403).send({ foo: 'bar '})
}

it('should return the munged text result', done => {
const server = express()
.use(mung.send(modifyText))
.get('/', (req, res) => {
res.send(originalResponseTextBody);
});
request(server)
.get('/')
.expect(200)
.expect(res => {
res.text.should.eql(modifiedResponseTextBody);
})
.end(done);
});

it('should return a munged json body ', done => {
const server = express()
.use(mung.send(modifyJson))
.get('/', (req, res) => {
res.send(originalResponseJSONBody);
});
request(server)
.get('/')
.expect(200)
.expect(res => {
res.body.should.eql(modifiedResponseJsonBody);
})
.end(done);
});

it('should not mung an error response (by default)', done => {
const server = express()
.use(mung.send(modifyText))
.get('/', (req, res) => {
res.status(404)
.send(originalResponseTextBody);
});
request(server)
.get('/')
.expect(404)
.expect(res => {
res.text.should.equal(originalResponseTextBody)
res.body.should.deepEqual({});
})
.end(done);
});

it('should mung an error response when told to', done => {
const server = express()
.use(mung.send(modifyText, { mungError: true }))
.get('/', (req, res) => {
res.status(404)
.send(originalResponseTextBody);
});
request(server)
.get('/')
.expect(404)
.expect(res => {
res.text.should.eql(modifiedResponseTextBody);
})
.end(done);
});

it('should abort if a response is sent', done => {
const server = express()
.use(mung.send(error403))
.get('/', (req, res) => {
res.status(200)
.send(originalResponseTextBody)
});
request(server)
.get('/')
.expect(403)
.end(done);
});

it('should 500 on a synchronous exception', done => {
const server = express()
.use(mung.send(error))
.get('/', (req, res) => {
res.status(200)
.send(originalResponseTextBody);
});
request(server)
.get('/')
.expect(500)
.end(done);
});

it('should 500 on an asynchronous exception', done => {
const server = express()
.use(mung.send(error))
.get('/', (req, res) => {
process.nextTick(() => {
res.status(200).send(originalResponseTextBody);
});
});
request(server)
.get('/')
.expect(500)
.end(done);
});
})
Loading