Skip to content

Bind AsyncResource on busboy close event to preserve async context #1358

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

Open
wants to merge 2 commits into
base: main
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
12 changes: 10 additions & 2 deletions lib/make-middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ var Counter = require('./counter')
var MulterError = require('./multer-error')
var FileAppender = require('./file-appender')
var removeUploadedFiles = require('./remove-uploaded-files')
var { AsyncResource } = require('async_hooks')

function drainStream (stream) {
stream.on('readable', () => {
Expand Down Expand Up @@ -181,13 +182,20 @@ function makeMiddleware (setup) {
busboy.on('partsLimit', function () { abortWithCode('LIMIT_PART_COUNT') })
busboy.on('filesLimit', function () { abortWithCode('LIMIT_FILE_COUNT') })
busboy.on('fieldsLimit', function () { abortWithCode('LIMIT_FIELD_COUNT') })
busboy.on('close', function () {
busboy.on('close', bindToAsyncResource(function () {
readFinished = true
indicateDone()
})
}))

req.pipe(busboy)
}
}
function bindToAsyncResource(fn) {
if (typeof AsyncResource?.bind === "function") {
return AsyncResource.bind(fn);
}

return fn;
}

module.exports = makeMiddleware
66 changes: 66 additions & 0 deletions test/async-context.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/* eslint-env mocha */

var assert = require('assert')
var express = require('express')
var FormData = require('form-data')
var concat = require('concat-stream')
var multer = require('..')

var asyncHooks = require('async_hooks')

var port = 34280

describe('Async context preservation', function () {
var app

before(function (done) {
app = express()
app.listen(port, done)
})

function submitForm (form, path, cb) {
var req = form.submit('http://localhost:' + port + path)

req.on('error', cb)
req.on('response', function (res) {
res.on('error', cb)
res.pipe(concat({ encoding: 'buffer' }, function (body) {
cb(null, res, body)
}))
})
}

it('should preserve AsyncLocalStorage context across multer processing', function (done) {
if (!asyncHooks || typeof asyncHooks.AsyncResource !== 'function' || typeof asyncHooks.AsyncResource.bind !== 'function') {
this.skip()
}

if (typeof asyncHooks.AsyncLocalStorage !== 'function') {
this.skip()
}

var AsyncLocalStorage = asyncHooks.AsyncLocalStorage
var als = new AsyncLocalStorage()

var upload = multer()
var form = new FormData()

form.append('field', 'value')

app.post('/ctx', function (req, res, next) {
als.run({ requestId: 'abc-123' }, function () { next() })
}, upload.none(), function (req, res) {
var store = als.getStore()
var requestId = store && store.requestId
res.status(200).end(requestId || 'NO_CONTEXT')
})

submitForm(form, '/ctx', function (err, res, body) {
assert.ifError(err)
assert.strictEqual(res.statusCode, 200)
assert.strictEqual(body.toString(), 'abc-123')
done()
})
})
})