diff --git a/.travis.yml b/.travis.yml index bf4f58f..ff253a7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -37,3 +37,7 @@ script: - "test -z $(npm -ps ls eslint ) || npm run-script lint" after_script: - "test -e ./coverage/lcov.info && npm install coveralls@2 && cat ./coverage/lcov.info | coveralls" +matrix: + include: + - node_js: "10.11" + env: HTTP2_TEST=1 diff --git a/index.js b/index.js index 4da7301..5e685a7 100644 --- a/index.js +++ b/index.js @@ -84,16 +84,36 @@ function typeis (value, types_) { * or `content-length` headers set. * http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.3 * + * A http/2 request with DataFrame can have no `content-length` header. + * https://httpwg.org/specs/rfc7540.html + * + * A http/2 request without DataFrame send HeaderFrame with end-stream-flag. + * If nodejs gets end-stream-flag, then nodejs ends readable stream. + * https://github.com/nodejs/node/blob/master/lib/internal/http2/core.js#L301 + * * @param {Object} request * @return {Boolean} * @public */ function hasbody (req) { - return req.headers['transfer-encoding'] !== undefined || + return (ishttp2(req) && !req.stream.endAfterHeaders) || + req.headers['transfer-encoding'] !== undefined || !isNaN(req.headers['content-length']) } +/** + * Check if a request is a http2 request. + * + * @param {Object} request + * @return {Boolean} + * @public + */ + +function ishttp2 (req) { + return req.httpVersionMajor === 2 +} + /** * Check if the incoming request contains the "Content-Type" * header field, and it contains any of the give mime `type`s. diff --git a/package.json b/package.json index 5b8e207..32aebcf 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "scripts": { "lint": "eslint --plugin markdown --ext js,md .", "test": "mocha --reporter spec --check-leaks --bail test/", + "test-http2": "HTTP2_TEST=1 mocha --reporter spec --check-leaks --bail test/", "test-cov": "istanbul cover node_modules/mocha/bin/_mocha -- --reporter dot --check-leaks test/", "test-travis": "istanbul cover node_modules/mocha/bin/_mocha --report lcovonly -- --reporter spec --check-leaks test/" }, diff --git a/test/test.js b/test/test.js index 4fe654f..58830d9 100644 --- a/test/test.js +++ b/test/test.js @@ -1,151 +1,207 @@ var assert = require('assert') var typeis = require('..') +var http2 +var Readable + +if (process.env.HTTP2_TEST) { + http2 = require('http2') + Readable = require('stream').Readable +} describe('typeis(req, type)', function () { - it('should ignore params', function () { - var req = createRequest('text/html; charset=utf-8') - assert.equal(typeis(req, ['text/*']), 'text/html') + it('should ignore params', function (done) { + createRequest('text/html; charset=utf-8', function (req) { + assert.equal(typeis(req, ['text/*']), 'text/html') + done() + }) }) - it('should ignore params LWS', function () { - var req = createRequest('text/html ; charset=utf-8') - assert.equal(typeis(req, ['text/*']), 'text/html') + it('should ignore params LWS', function (done) { + createRequest('text/html ; charset=utf-8', function (req) { + assert.equal(typeis(req, ['text/*']), 'text/html') + done() + }) }) - it('should ignore casing', function () { - var req = createRequest('text/HTML') - assert.equal(typeis(req, ['text/*']), 'text/html') + it('should ignore casing', function (done) { + createRequest('text/HTML', function (req) { + assert.equal(typeis(req, ['text/*']), 'text/html') + done() + }) }) - it('should fail invalid type', function () { - var req = createRequest('text/html**') - assert.strictEqual(typeis(req, ['text/*']), false) + it('should fail invalid type', function (done) { + createRequest('text/html**', function (req) { + assert.strictEqual(typeis(req, ['text/*']), false) + done() + }) }) - it('should not match invalid type', function () { - var req = createRequest('text/html') - assert.strictEqual(typeis(req, ['text/html/']), false) - assert.strictEqual(typeis(req, [undefined, null, true, function () {}]), false) + it('should not match invalid type', function (done) { + createRequest('text/html', function (req) { + assert.strictEqual(typeis(req, ['text/html/']), false) + assert.strictEqual(typeis(req, [undefined, null, true, function () {}]), false) + done() + }) }) describe('when no body is given', function () { - it('should return null', function () { - var req = {headers: {}} - - assert.strictEqual(typeis(req), null) - assert.strictEqual(typeis(req, ['image/*']), null) - assert.strictEqual(typeis(req, 'image/*', 'text/*'), null) + it('should return null', function (done) { + createBodylessRequest('', function (req) { + assert.strictEqual(typeis(req), null) + assert.strictEqual(typeis(req, ['image/*']), null) + assert.strictEqual(typeis(req, 'image/*', 'text/*'), null) + done() + }) }) }) describe('when no content type is given', function () { - it('should return false', function () { - var req = createRequest() - assert.strictEqual(typeis(req), false) - assert.strictEqual(typeis(req, ['image/*']), false) - assert.strictEqual(typeis(req, ['text/*', 'image/*']), false) + it('should return false', function (done) { + createRequest('', function (req) { + assert.strictEqual(typeis(req), false) + assert.strictEqual(typeis(req, ['image/*']), false) + assert.strictEqual(typeis(req, ['text/*', 'image/*']), false) + done() + }) }) }) describe('give no types', function () { - it('should return the mime type', function () { - var req = createRequest('image/png') - assert.equal(typeis(req), 'image/png') + it('should return the mime type', function (done) { + createRequest('image/png', function (req) { + assert.equal(typeis(req), 'image/png') + done() + }) }) }) describe('given one type', function () { - it('should return the type or false', function () { - var req = createRequest('image/png') - - assert.equal(typeis(req, ['png']), 'png') - assert.equal(typeis(req, ['.png']), '.png') - assert.equal(typeis(req, ['image/png']), 'image/png') - assert.equal(typeis(req, ['image/*']), 'image/png') - assert.equal(typeis(req, ['*/png']), 'image/png') - - assert.strictEqual(typeis(req, ['jpeg']), false) - assert.strictEqual(typeis(req, ['.jpeg']), false) - assert.strictEqual(typeis(req, ['image/jpeg']), false) - assert.strictEqual(typeis(req, ['text/*']), false) - assert.strictEqual(typeis(req, ['*/jpeg']), false) - - assert.strictEqual(typeis(req, ['bogus']), false) - assert.strictEqual(typeis(req, ['something/bogus*']), false) + it('should return the type or false', function (done) { + createRequest('image/png', function (req) { + assert.equal(typeis(req, ['png']), 'png') + assert.equal(typeis(req, ['.png']), '.png') + assert.equal(typeis(req, ['image/png']), 'image/png') + assert.equal(typeis(req, ['image/*']), 'image/png') + assert.equal(typeis(req, ['*/png']), 'image/png') + + assert.strictEqual(typeis(req, ['jpeg']), false) + assert.strictEqual(typeis(req, ['.jpeg']), false) + assert.strictEqual(typeis(req, ['image/jpeg']), false) + assert.strictEqual(typeis(req, ['text/*']), false) + assert.strictEqual(typeis(req, ['*/jpeg']), false) + + assert.strictEqual(typeis(req, ['bogus']), false) + assert.strictEqual(typeis(req, ['something/bogus*']), false) + done() + }) }) }) describe('given multiple types', function () { - it('should return the first match or false', function () { - var req = createRequest('image/png') - - assert.equal(typeis(req, ['png']), 'png') - assert.equal(typeis(req, '.png'), '.png') - assert.equal(typeis(req, ['text/*', 'image/*']), 'image/png') - assert.equal(typeis(req, ['image/*', 'text/*']), 'image/png') - assert.equal(typeis(req, ['image/*', 'image/png']), 'image/png') - assert.equal(typeis(req, 'image/png', 'image/*'), 'image/png') - - assert.strictEqual(typeis(req, ['jpeg']), false) - assert.strictEqual(typeis(req, ['.jpeg']), false) - assert.strictEqual(typeis(req, ['text/*', 'application/*']), false) - assert.strictEqual(typeis(req, ['text/html', 'text/plain', 'application/json']), false) + it('should return the first match or false', function (done) { + createRequest('image/png', function (req) { + assert.equal(typeis(req, ['png']), 'png') + assert.equal(typeis(req, '.png'), '.png') + assert.equal(typeis(req, ['text/*', 'image/*']), 'image/png') + assert.equal(typeis(req, ['image/*', 'text/*']), 'image/png') + assert.equal(typeis(req, ['image/*', 'image/png']), 'image/png') + assert.equal(typeis(req, 'image/png', 'image/*'), 'image/png') + + assert.strictEqual(typeis(req, ['jpeg']), false) + assert.strictEqual(typeis(req, ['.jpeg']), false) + assert.strictEqual(typeis(req, ['text/*', 'application/*']), false) + assert.strictEqual(typeis(req, ['text/html', 'text/plain', 'application/json']), false) + done() + }) }) }) describe('given +suffix', function () { - it('should match suffix types', function () { - var req = createRequest('application/vnd+json') - - assert.equal(typeis(req, '+json'), 'application/vnd+json') - assert.equal(typeis(req, 'application/vnd+json'), 'application/vnd+json') - assert.equal(typeis(req, 'application/*+json'), 'application/vnd+json') - assert.equal(typeis(req, '*/vnd+json'), 'application/vnd+json') - assert.strictEqual(typeis(req, 'application/json'), false) - assert.strictEqual(typeis(req, 'text/*+json'), false) + it('should match suffix types', function (done) { + createRequest('application/vnd+json', function (req) { + assert.equal(typeis(req, '+json'), 'application/vnd+json') + assert.equal(typeis(req, 'application/vnd+json'), 'application/vnd+json') + assert.equal(typeis(req, 'application/*+json'), 'application/vnd+json') + assert.equal(typeis(req, '*/vnd+json'), 'application/vnd+json') + assert.strictEqual(typeis(req, 'application/json'), false) + assert.strictEqual(typeis(req, 'text/*+json'), false) + done() + }) }) }) describe('given "*/*"', function () { - it('should match any content-type', function () { - assert.equal(typeis(createRequest('text/html'), '*/*'), 'text/html') - assert.equal(typeis(createRequest('text/xml'), '*/*'), 'text/xml') - assert.equal(typeis(createRequest('application/json'), '*/*'), 'application/json') - assert.equal(typeis(createRequest('application/vnd+json'), '*/*'), 'application/vnd+json') + describe('should match any content-type', function () { + it('text/html', function (done) { + createRequest('text/html', function (req) { + assert.equal(typeis(req, '*/*'), 'text/html') + done() + }) + }) + + it('text/xml', function (done) { + createRequest('text/xml', function (req) { + assert.equal(typeis(req, '*/*'), 'text/xml') + done() + }) + }) + + it('application/json', function (done) { + createRequest('application/json', function (req) { + assert.equal(typeis(req, '*/*'), 'application/json') + done() + }) + }) + + it('application/vnd+json', function (done) { + createRequest('application/vnd+json', function (req) { + assert.equal(typeis(req, '*/*'), 'application/vnd+json') + done() + }) + }) }) - it('should not match invalid content-type', function () { - assert.strictEqual(typeis(createRequest('bogus'), '*/*'), false) + it('should not match invalid content-type', function (done) { + createRequest('bogus', function (req) { + assert.strictEqual(typeis(req, '*/*'), false) + done() + }) }) - it('should not match body-less request', function () { - var req = {headers: {'content-type': 'text/html'}} - assert.strictEqual(typeis(req, '*/*'), null) + it('should not match body-less request', function (done) { + createBodylessRequest('text/html', function (req) { + assert.strictEqual(typeis(req, '*/*'), null) + done() + }) }) }) describe('when Content-Type: application/x-www-form-urlencoded', function () { - it('should match "urlencoded"', function () { - var req = createRequest('application/x-www-form-urlencoded') - - assert.equal(typeis(req, ['urlencoded']), 'urlencoded') - assert.equal(typeis(req, ['json', 'urlencoded']), 'urlencoded') - assert.equal(typeis(req, ['urlencoded', 'json']), 'urlencoded') + it('should match "urlencoded"', function (done) { + createRequest('application/x-www-form-urlencoded', function (req) { + assert.equal(typeis(req, ['urlencoded']), 'urlencoded') + assert.equal(typeis(req, ['json', 'urlencoded']), 'urlencoded') + assert.equal(typeis(req, ['urlencoded', 'json']), 'urlencoded') + done() + }) }) }) describe('when Content-Type: multipart/form-data', function () { - it('should match "multipart/*"', function () { - var req = createRequest('multipart/form-data') - - assert.equal(typeis(req, ['multipart/*']), 'multipart/form-data') + it('should match "multipart/*"', function (done) { + createRequest('multipart/form-data', function (req) { + assert.equal(typeis(req, ['multipart/*']), 'multipart/form-data') + done() + }) }) - it('should match "multipart"', function () { - var req = createRequest('multipart/form-data') - - assert.equal(typeis(req, ['multipart']), 'multipart') + it('should match "multipart"', function (done) { + createRequest('multipart/form-data', function (req) { + assert.equal(typeis(req, ['multipart']), 'multipart') + done() + }) }) }) }) @@ -157,9 +213,11 @@ describe('typeis.hasBody(req)', function () { assert.strictEqual(typeis.hasBody(req), true) }) - it('should be true when 0', function () { - var req = {headers: {'content-length': '0'}} - assert.strictEqual(typeis.hasBody(req), true) + it('should be true when 0', function (done) { + createZeroLengthBodyRequest('', function (req) { + assert.strictEqual(typeis.hasBody(req), true) + done() + }) }) it('should be false when bogus', function () { @@ -174,13 +232,180 @@ describe('typeis.hasBody(req)', function () { assert.strictEqual(typeis.hasBody(req), true) }) }) + + if (process.env.HTTP2_TEST) { + describe('http2 request', function () { + it('should not indicate body', function (done) { + createBodylessRequest('', function (req) { + assert.strictEqual(typeis.hasBody(req), false) + done() + }) + }) + + it('should not indicate body after end event occurred', function (done) { + createBodylessRequest('', function (req) { + var data = '' + req.on('data', function (chunk) { + data += chunk + }) + req.on('end', function (chunk) { + process.nextTick(function () { + assert.strictEqual(data, '') + assert.strictEqual(typeis.hasBody(req), false) + done() + }) + }) + }) + }) + + it('should not indicate body while end event occurred', function (done) { + createBodylessRequest('', function (req) { + var data = '' + req.on('data', function (chunk) { + data += chunk + }) + req.on('end', function (chunk) { + assert.strictEqual(data, '') + assert.strictEqual(typeis.hasBody(req), false) + done() + }) + }) + }) + + it('should indicate body', function (done) { + createRequest('', function (req) { + assert.strictEqual(typeis.hasBody(req), true) + done() + }) + }) + + it('should indicate body after end event occurred', function (done) { + createRequest('', function (req) { + var data = '' + req.on('data', function (chunk) { + data += chunk + }) + req.on('end', function (chunk) { + process.nextTick(function () { + assert.strictEqual(data, 'hello') + assert.strictEqual(typeis.hasBody(req), true) + done() + }) + }) + }) + }) + + it('should indicate body while end event occurred', function (done) { + createRequest('', function (req) { + var data = '' + req.on('data', function (chunk) { + data += chunk + }) + req.on('end', function (chunk) { + assert.strictEqual(data, 'hello') + assert.strictEqual(typeis.hasBody(req), true) + done() + }) + }) + }) + + it('should indicate body while data event', function (done) { + createRequest('', function (req) { + req.on('data', function (chunk) { + assert.strictEqual(typeis.hasBody(req), true) + done() + }) + }) + }) + }) + } }) -function createRequest (type) { - return { - headers: { - 'content-type': type || '', - 'transfer-encoding': 'chunked' +function createRequest (type, callback) { + if (process.env.HTTP2_TEST) { + var server = http2.createServer(function (req, res) { + callback(req) + server.close() + }) + + server = server.listen(function () { + var s = new Readable() + s.push('hello') + s.push(null) + + var session = http2.connect('http://localhost:' + server.address().port) + var headers = { + ':path': '/', + ':method': 'post', + 'content-type': type || undefined + } + var request = session.request(headers) + s.pipe(request) + }) + } else { + var req = { + headers: { + 'content-type': type || '', + 'transfer-encoding': 'chunked' + } + } + callback(req) + } +} + +function createBodylessRequest (type, callback) { + if (process.env.HTTP2_TEST) { + var server = http2.createServer(function (req, res) { + callback(req) + server.close() + }) + + server = server.listen(function () { + var session = http2.connect('http://localhost:' + server.address().port) + var headers = { + ':path': '/', + ':method': 'get', + 'content-type': type || '' + } + var option = { + endStream: true + } + session.request(headers, option) + }) + } else { + var req = { + headers: { + 'content-type': type || '' + } + } + callback(req) + } +} + +function createZeroLengthBodyRequest (type, callback) { + if (process.env.HTTP2_TEST) { + var server = http2.createServer(function (req, res) { + callback(req) + server.close() + }) + + server = server.listen(function () { + var session = http2.connect('http://localhost:' + server.address().port) + var headers = { + ':path': '/', + ':method': 'get', + 'content-type': type || '' + } + var request = session.request(headers) + request.end() + }) + } else { + var req = { + headers: { + 'content-type': type || '', + 'content-length': 0 + } } + callback(req) } }