diff --git a/.github/workflows/node-v12.yaml b/.github/workflows/node-v12.yaml new file mode 100644 index 0000000..72743f6 --- /dev/null +++ b/.github/workflows/node-v12.yaml @@ -0,0 +1,31 @@ +# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions + +# NOTE: This action runs the testv12 test suite. + +name: Node-v12 + +on: + push: + branches: [master] + pull_request: + branches: [master] + +jobs: + build: + runs-on: ubuntu-18.04 + + strategy: + fail-fast: false + matrix: + node-version: [12.x] + + steps: + - uses: actions/checkout@v2 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v2 + with: + node-version: ${{ matrix.node-version }} + - run: npm install + - run: npm run build --if-present + - run: npm run testv12 diff --git a/.github/workflows/node.yaml b/.github/workflows/node.yaml new file mode 100644 index 0000000..3be146a --- /dev/null +++ b/.github/workflows/node.yaml @@ -0,0 +1,31 @@ +# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions + +# NOTE: This action runs the normal test suite. + +name: Node + +on: + push: + branches: [master] + pull_request: + branches: [master] + +jobs: + build: + runs-on: ubuntu-18.04 + + strategy: + fail-fast: false + matrix: + node-version: [6.x, 8.x, 10.x, 14.x, 16.x] + + steps: + - uses: actions/checkout@v2 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v2 + with: + node-version: ${{ matrix.node-version }} + - run: npm install + - run: npm run build --if-present + - run: npm test diff --git a/.gitignore b/.gitignore index c97c649..b48af5b 100644 --- a/.gitignore +++ b/.gitignore @@ -216,3 +216,5 @@ pip-log.txt #Mr Developer .mr.developer.cfg +.idea +node_modules diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 9b206e7..0000000 --- a/.travis.yml +++ /dev/null @@ -1,3 +0,0 @@ -language: node_js -node_js: - - '6' diff --git a/README.md b/README.md index 8b9abef..6c7fce0 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,14 @@ -# HTTP Parser - -This library parses HTTP protocol for requests and responses. It was created to replace `http_parser.c` since calling C++ function from JS is really slow in V8. +![Node](https://github.com/creationix/http-parser-js/workflows/Node/badge.svg) +![Node-v12](https://github.com/creationix/http-parser-js/workflows/Node-v12/badge.svg) -This was further modified by Jimbly to be useable in parsing responses, specifically tested with the "request" module, and addresses issues such as corrupt HTTP headers, which would otherwise cause Node's parser to throw a fatal error (HPE_INVALID_HEADER_TOKEN). +# HTTP Parser -Jan Schär (jscissr) made some bigger changes and added tests. This fixed some bugs and added many missing features. +This library parses HTTP protocol for requests and responses. +It was created to replace `http_parser.c` since calling C++ functions from JS is really slow in V8. +However, it is now primarily useful in having a more flexible/tolerant HTTP parser when dealing with legacy services that do not meet the strict HTTP parsing rules Node's parser follows. -This is packaged as a standalone npm module. To use in node, monkeypatch HTTPParser. +This is packaged as a standalone npm module. +To use in node, monkeypatch HTTPParser. ```js // Monkey patch before you require http for the first time. @@ -18,12 +20,24 @@ var http = require('http'); ## Testing -Simply do `npm test`. The tests are copied from node and mscedex/io.js, with some modifcations. +Simply run `npm test`. +The tests are copied from node and mscedex/io.js, with some modifcations. ## Status -This should now be usable in any node application, it now supports (nearly) everything `http_parser.c` does while still being tolerant with corrupted headers. +This should now be usable in any node application, it now supports (nearly) everything `http_parser.c` does while still being tolerant with corrupted headers, and other kinds of malformed data. + +### Node versions + +`http-parser-js` should work via monkey-patching on Node v6-v11, and v13-14. + +Node v12.x renamed the internal http parser, and did not expose it for monkey-patching, so to be able to monkey-patch on Node v12, you must run `node --http-parser=legacy file.js` to opt in to the old, monkey-patchable http_parser binding. + +## Standalone usage + +While this module is intended to be used as a replacement for the internal Node.js parser, it can be used as a standalone parser. The [`standalone-example.js`](standalone-example.js) demonstrates how to use the somewhat awkward API (coming from compatibility with the Node.js internals) to parse HTTP from raw Buffers. ## License -MIT. See LICENSE.md +MIT. +See [LICENSE.md](LICENSE.md) diff --git a/http-parser.d.ts b/http-parser.d.ts new file mode 100644 index 0000000..9a8ac57 --- /dev/null +++ b/http-parser.d.ts @@ -0,0 +1,181 @@ +type ParserType = + | 'REQUEST' + | 'RESPONSE' + +type RequestMethod = + | 'DELETE' + | 'GET' + | 'HEAD' + | 'POST' + | 'PUT' + | 'CONNECT' + | 'OPTIONS' + | 'TRACE' + | 'COPY' + | 'LOCK' + | 'MKCOL' + | 'MOVE' + | 'PROPFIND' + | 'PROPPATCH' + | 'SEARCH' + | 'UNLOCK' + | 'BIND' + | 'REBIND' + | 'UNBIND' + | 'ACL' + | 'REPORT' + | 'MKACTIVITY' + | 'CHECKOUT' + | 'MERGE' + | 'M-SEARCH' + | 'NOTIFY' + | 'SUBSCRIBE' + | 'UNSUBSCRIBE' + | 'PATCH' + | 'PURGE' + | 'MKCALENDAR' + | 'LINK' + | 'UNLINK' + | string + +type StateHeaderKey = + | 'REQUEST_LINE' + | 'RESPONSE_LINE' + | 'HEADER' + +type StateFinishAllowedKey = + | 'REQUEST_LINE' + | 'RESPONSE_LINE' + | 'BODY_RAW' + +type HeaderObject = Array +type noop = ()=> T + +type HeaderInfo
= { + versionMajor: number + versionMinor: number + headers: HEADER + method: number + url: string + statusCode: number + statusMessage: string + upgrade: boolean + shouldKeepAlive: boolean +} +export type OnHeadersCompleteParser
= Mode_0_12 extends true + ? (info: HeaderInfo
)=> number | void + : ( + versionMajor: number, + versionMinor: number, + headers: HEADER, + method: number, + url: string, + statusCode: number, + statusMessage: string, + upgrade: boolean, + shouldKeepAlive: boolean, + )=> number | void +export type OnBodyParser = (chunk: Buffer, offset: number, length: number)=> void +// Only called in the slow case where slow means +// that the request headers were either fragmented +// across multiple TCP packets or too large to be +// processed in a single run. This method is also +// called to process trailing HTTP headers. +export type OnHeadersParser = (headers: string[], url: string)=> void + +declare class HTTPParserJS { + initialize(type: ParserType, async_resource?: unknown): void + + // Some handler stubs, needed for compatibility + [HTTPParser.kOnHeaders]: OnHeadersParser + [HTTPParser.kOnHeadersComplete]: OnHeadersCompleteParser + [HTTPParser.kOnBody]: OnBodyParser + [HTTPParser.kOnMessageComplete]: noop + + /** + * Max number of bytes that will be parsed as headers, 80kb by default + * @default 81920 + */ + maxHeaderSize: number + + reinitialize: HTTPParserConstructor + close: noop + pause: noop + resume: noop + free: noop + private _compatMode0_11: false | boolean + getAsyncId: noop<0> + + execute(chunk: Buffer, start?: number, length?: number): number | Error + finish(): void | Error + + // These three methods are used for an internal speed optimization, and it also + // works if theses are noops. Basically consume() asks us to read the bytes + // ourselves, but if we don't do it we get them through execute(). + consume: noop + unconsume: noop + getCurrentBuffer: noop + + /** + * For correct error handling - see HTTPParser#execute + * @example this.userCall()(userFunction('arg')); + */ + userCall(): (ret?: T)=> T + private nextRequest: noop + private consumeLine: noop + parseHeader(line: string, headers: string[]): void + private REQUEST_LINE: noop + private RESPONSE_LINE: noop + shouldKeepAlive(): boolean + /** + * For older versions of node (v6.x and older?), that return `skipBody=1` or `skipBody=true`, need this `return true;` if it's an upgrade request. + */ + private HEADER(): void | boolean + private BODY_CHUNKHEAD(): void + private BODY_CHUNK(): void + private BODY_CHUNKEMPTYLINE(): void + private BODY_CHUNKTRAILERS(): void + private BODY_RAW(): void + private BODY_SIZED(): void + + get onHeaders(): OnHeadersParser + set onHeaders(to: OnHeadersParser) + + get onHeadersComplete(): OnHeadersCompleteParser + set onHeadersComplete(to: OnHeadersCompleteParser) + + get onBody(): OnBodyParser + set onBody(to: OnBodyParser) + + get onMessageComplete(): noop + set onMessageComplete(to: noop) +} + +interface HTTPParserConstructor extends Function { + new(type?: ParserType): HTTPParserJS + (type?: ParserType): void + + readonly prototype: HTTPParserJS + + readonly REQUEST: 'REQUEST' + readonly RESPONSE: 'RESPONSE' + readonly methods: RequestMethod[] + + encoding: 'ascii'|string + /** + * maxHeaderSize (in bytes) is configurable, but 80kb by default; + * @default 80 * 1024 = 80kb + */ + maxHeaderSize: 81920|number + + // Note: *not* starting with kOnHeaders=0 line the Node parser, because any + // newly added constants (kOnTimeout in Node v12.19.0) will overwrite 0! + readonly kOnHeaders: 1 + readonly kOnHeadersComplete: 2 + readonly kOnBody: 3 + readonly kOnMessageComplete: 4 + + kOnExecute(): void +} +export const HTTPParser: HTTPParserConstructor +export const methods: RequestMethod[] diff --git a/http-parser.js b/http-parser.js index 1f69ff8..3e98ab9 100644 --- a/http-parser.js +++ b/http-parser.js @@ -1,10 +1,21 @@ /*jshint node:true */ -var assert = require('assert'); - exports.HTTPParser = HTTPParser; function HTTPParser(type) { - assert.ok(type === HTTPParser.REQUEST || type === HTTPParser.RESPONSE); + if (type !== undefined && type !== HTTPParser.REQUEST && type !== HTTPParser.RESPONSE) { + throw new Error('type must be REQUEST or RESPONSE'); + } + if (type === undefined) { + // Node v12+ + } else { + this.initialize(type); + } + this.maxHeaderSize=HTTPParser.maxHeaderSize +} +HTTPParser.prototype.initialize = function (type, async_resource) { + if (type !== HTTPParser.REQUEST && type !== HTTPParser.RESPONSE) { + throw new Error('type must be REQUEST or RESPONSE'); + } this.type = type; this.state = type + '_LINE'; this.info = { @@ -19,14 +30,19 @@ function HTTPParser(type) { this.body_bytes = null; this.isUserCall = false; this.hadError = false; -} +}; + +HTTPParser.encoding = 'ascii'; HTTPParser.maxHeaderSize = 80 * 1024; // maxHeaderSize (in bytes) is configurable, but 80kb by default; HTTPParser.REQUEST = 'REQUEST'; HTTPParser.RESPONSE = 'RESPONSE'; -var kOnHeaders = HTTPParser.kOnHeaders = 0; -var kOnHeadersComplete = HTTPParser.kOnHeadersComplete = 1; -var kOnBody = HTTPParser.kOnBody = 2; -var kOnMessageComplete = HTTPParser.kOnMessageComplete = 3; + +// Note: *not* starting with kOnHeaders=0 line the Node parser, because any +// newly added constants (kOnTimeout in Node v12.19.0) will overwrite 0! +var kOnHeaders = HTTPParser.kOnHeaders = 1; +var kOnHeadersComplete = HTTPParser.kOnHeadersComplete = 2; +var kOnBody = HTTPParser.kOnBody = 3; +var kOnMessageComplete = HTTPParser.kOnMessageComplete = 4; // Some handler stubs, needed for compatibility HTTPParser.prototype[kOnHeaders] = @@ -39,7 +55,7 @@ Object.defineProperty(HTTPParser, 'kOnExecute', { get: function () { // hack for backward compatibility compatMode0_12 = false; - return 4; + return 99; } }); @@ -76,13 +92,15 @@ var methods = exports.methods = HTTPParser.methods = [ 'PURGE', 'MKCALENDAR', 'LINK', - 'UNLINK' + 'UNLINK', + 'SOURCE', ]; var method_connect = methods.indexOf('CONNECT'); HTTPParser.prototype.reinitialize = HTTPParser; HTTPParser.prototype.close = HTTPParser.prototype.pause = HTTPParser.prototype.resume = +HTTPParser.prototype.remove = HTTPParser.prototype.free = function () {}; HTTPParser.prototype._compatMode0_11 = false; HTTPParser.prototype.getAsyncId = function() { return 0; }; @@ -122,7 +140,7 @@ HTTPParser.prototype.execute = function (chunk, start, length) { length = this.offset - start; if (headerState[this.state]) { this.headerSize += length; - if (this.headerSize > HTTPParser.maxHeaderSize) { + if (this.headerSize > (this.maxHeaderSize||HTTPParser.maxHeaderSize)) { return new Error('max header size exceeded'); } } @@ -174,7 +192,7 @@ HTTPParser.prototype.consumeLine = function () { chunk = this.chunk; for (var i = this.offset; i < end; i++) { if (chunk[i] === 0x0a) { // \n - var line = this.line + chunk.toString('ascii', this.offset, i); + var line = this.line + chunk.toString(HTTPParser.encoding, this.offset, i); if (line.charAt(line.length - 1) === '\r') { line = line.substr(0, line.length - 1); } @@ -184,7 +202,7 @@ HTTPParser.prototype.consumeLine = function () { } } //line split over multiple chunks - this.line += chunk.toString('ascii', this.offset, this.end); + this.line += chunk.toString(HTTPParser.encoding, this.offset, this.end); this.offset = this.end; }; @@ -310,8 +328,14 @@ HTTPParser.prototype.HEADER = function () { } } + // if both isChunked and hasContentLength, isChunked wins + // This is required so the body is parsed using the chunked method, and matches + // Chrome's behavior. We could, maybe, ignore them both (would get chunked + // encoding into the body), and/or disable shouldKeepAlive to be more + // resilient. if (this.isChunked && hasContentLength) { - throw parseErrorCode('HPE_UNEXPECTED_CONTENT_LENGTH'); + hasContentLength = false; + this.body_bytes = null; } // Logic from https://github.com/nodejs/http-parser/blob/921d5585515a153fa00e411cf144280c59b41f90/http_parser.c#L1727-L1737 @@ -324,6 +348,10 @@ HTTPParser.prototype.HEADER = function () { info.upgrade = info.method === method_connect; } + if (this.isChunked && info.upgrade) { + this.isChunked = false; + } + info.shouldKeepAlive = this.shouldKeepAlive(); //problem which also exists in original node: we should know skipBody before calling onHeadersComplete var skipBody; @@ -367,7 +395,8 @@ HTTPParser.prototype.BODY_CHUNKHEAD = function () { HTTPParser.prototype.BODY_CHUNK = function () { var length = Math.min(this.end - this.offset, this.body_bytes); - this.userCall()(this[kOnBody](this.chunk, this.offset, length)); + // 0, length are for backwards compatibility. See: https://github.com/creationix/http-parser-js/pull/98 + this.userCall()(this[kOnBody](this.chunk.slice(this.offset, this.offset + length), 0, length)); this.offset += length; this.body_bytes -= length; if (!this.body_bytes) { @@ -380,7 +409,9 @@ HTTPParser.prototype.BODY_CHUNKEMPTYLINE = function () { if (line === undefined) { return; } - assert.equal(line, ''); + if (line !== '') { + throw new Error('Expected empty line'); + } this.state = 'BODY_CHUNKHEAD'; }; @@ -400,14 +431,15 @@ HTTPParser.prototype.BODY_CHUNKTRAILERS = function () { }; HTTPParser.prototype.BODY_RAW = function () { - var length = this.end - this.offset; - this.userCall()(this[kOnBody](this.chunk, this.offset, length)); + // 0, length are for backwards compatibility. See: https://github.com/creationix/http-parser-js/pull/98 + this.userCall()(this[kOnBody](this.chunk.slice(this.offset, this.end), 0, this.end - this.offset)); this.offset = this.end; }; HTTPParser.prototype.BODY_SIZED = function () { var length = Math.min(this.end - this.offset, this.body_bytes); - this.userCall()(this[kOnBody](this.chunk, this.offset, length)); + // 0, length are for backwards compatibility. See: https://github.com/creationix/http-parser-js/pull/98 + this.userCall()(this[kOnBody](this.chunk.slice(this.offset, this.offset + length), 0, length)); this.offset += length; this.body_bytes -= length; if (!this.body_bytes) { diff --git a/package.json b/package.json index 2279a94..f1a1c24 100644 --- a/package.json +++ b/package.json @@ -1,17 +1,20 @@ { "name": "http-parser-js", - "version": "0.4.10", + "version": "0.5.10", "description": "A pure JS HTTP parser for node.", "main": "http-parser.js", + "types": "http-parser.d.ts", "scripts": { - "test": "python tests/test.py && node tests/iojs/test-http-parser-durability.js" + "test": "python tests/test.py && node tests/iojs/test-http-parser-durability.js", + "testv12": "python tests/test.py --node-args=\"--http-parser=legacy\" && node --http-parser=legacy tests/iojs/test-http-parser-durability.js" }, "repository": { "type": "git", "url": "git://github.com/creationix/http-parser-js.git" }, "files": [ - "http-parser.js" + "http-parser.js", + "http-parser.d.ts" ], "keywords": [ "http" diff --git a/standalone-example.js b/standalone-example.js new file mode 100644 index 0000000..9f5c652 --- /dev/null +++ b/standalone-example.js @@ -0,0 +1,244 @@ +/* + The following is an example of how to use the parser as a standalone module. + Both parseRequest and parseResponse can be used to parse a raw HTTP request/response from a Buffer. +*/ + +const { deepStrictEqual, throws } = require('assert'); +// Replace the require when using the module from npm. +//const { HTTPParser } = require('http-parser-js'); +const { HTTPParser } = require('./http-parser.js'); + +function parseRequest(input) { + const parser = new HTTPParser(HTTPParser.REQUEST); + let complete = false; + let shouldKeepAlive; + let upgrade; + let method; + let url; + let versionMajor; + let versionMinor; + let headers = []; + let trailers = []; + let bodyChunks = []; + + parser[HTTPParser.kOnHeadersComplete] = function (req) { + shouldKeepAlive = req.shouldKeepAlive; + upgrade = req.upgrade; + method = HTTPParser.methods[req.method]; + url = req.url; + versionMajor = req.versionMajor; + versionMinor = req.versionMinor; + headers = req.headers; + }; + + parser[HTTPParser.kOnBody] = function (chunk, offset, length) { + bodyChunks.push(chunk.slice(offset, offset + length)); + }; + + // This is actually the event for trailers, go figure. + parser[HTTPParser.kOnHeaders] = function (t) { + trailers = t; + }; + + parser[HTTPParser.kOnMessageComplete] = function () { + complete = true; + }; + + // Since we are sending the entire Buffer at once here all callbacks above happen synchronously. + // The parser does not do _anything_ asynchronous. + // However, you can of course call execute() multiple times with multiple chunks, e.g. from a stream. + // But then you have to refactor the entire logic to be async (e.g. resolve a Promise in kOnMessageComplete and add timeout logic). + parser.execute(input); + parser.finish(); + + if (!complete) { + throw new Error('Could not parse request'); + } + + let body = Buffer.concat(bodyChunks); + + return { + shouldKeepAlive, + upgrade, + method, + url, + versionMajor, + versionMinor, + headers, + body, + trailers, + }; +} + +function parseResponse(input) { + const parser = new HTTPParser(HTTPParser.RESPONSE); + let complete = false; + let shouldKeepAlive; + let upgrade; + let statusCode; + let statusMessage; + let versionMajor; + let versionMinor; + let headers = []; + let trailers = []; + let bodyChunks = []; + + parser[HTTPParser.kOnHeadersComplete] = function (res) { + shouldKeepAlive = res.shouldKeepAlive; + upgrade = res.upgrade; + statusCode = res.statusCode; + statusMessage = res.statusMessage; + versionMajor = res.versionMajor; + versionMinor = res.versionMinor; + headers = res.headers; + }; + + parser[HTTPParser.kOnBody] = function (chunk, offset, length) { + bodyChunks.push(chunk.slice(offset, offset + length)); + }; + + // This is actually the event for trailers, go figure. + parser[HTTPParser.kOnHeaders] = function (t) { + trailers = t; + }; + + parser[HTTPParser.kOnMessageComplete] = function () { + complete = true; + }; + + // Since we are sending the entire Buffer at once here all callbacks above happen synchronously. + // The parser does not do _anything_ asynchronous. + // However, you can of course call execute() multiple times with multiple chunks, e.g. from a stream. + // But then you have to refactor the entire logic to be async (e.g. resolve a Promise in kOnMessageComplete and add timeout logic). + parser.execute(input); + parser.finish(); + + if (!complete) { + throw new Error('Could not parse'); + } + + let body = Buffer.concat(bodyChunks); + + return { + shouldKeepAlive, + upgrade, + statusCode, + statusMessage, + versionMajor, + versionMinor, + headers, + body, + trailers, + }; +} + +let parsed; + +console.log('Example: basic GET request:'); + +parsed = parseRequest( + Buffer.from(`GET / HTTP/1.1 +Host: www.example.com + +`) +); + +console.log(parsed); + +deepStrictEqual(parsed.shouldKeepAlive, true); +deepStrictEqual(parsed.upgrade, false); +deepStrictEqual(parsed.method, 'GET'); +deepStrictEqual(parsed.url, '/'); +deepStrictEqual(parsed.versionMajor, 1); +deepStrictEqual(parsed.versionMinor, 1); +deepStrictEqual(parsed.headers, ['Host', 'www.example.com']); +deepStrictEqual(parsed.body.toString(), ''); +deepStrictEqual(parsed.trailers, []); + +console.log('Example: POST request with body:'); + +parsed = parseRequest( + Buffer.from(`POST /memes HTTP/1.1 +Host: www.example.com +Content-Length: 7 +Content-Type: text/plain + +foo bar +`) +); + +console.log(parsed); + +deepStrictEqual(parsed.shouldKeepAlive, true); +deepStrictEqual(parsed.upgrade, false); +deepStrictEqual(parsed.method, 'POST'); +deepStrictEqual(parsed.url, '/memes'); +deepStrictEqual(parsed.versionMajor, 1); +deepStrictEqual(parsed.versionMinor, 1); +deepStrictEqual(parsed.headers, ['Host', 'www.example.com', 'Content-Length', '7', 'Content-Type', 'text/plain']); +deepStrictEqual(parsed.body.toString(), 'foo bar'); +deepStrictEqual(parsed.trailers, []); + +console.log('Example: basic HTML response'); + +parsed = parseResponse( + Buffer.from(`HTTP/1.1 404 Not Found +Content-Type: text/html +Content-Length: 33 + +Computer says no +`) +); + +console.log(parsed); + +deepStrictEqual(parsed.shouldKeepAlive, true); +deepStrictEqual(parsed.upgrade, false); +deepStrictEqual(parsed.statusCode, 404); +deepStrictEqual(parsed.statusMessage, 'Not Found'); +deepStrictEqual(parsed.versionMajor, 1); +deepStrictEqual(parsed.versionMinor, 1); +deepStrictEqual(parsed.headers, ['Content-Type', 'text/html', 'Content-Length', '33']); +deepStrictEqual(parsed.body.toString(), 'Computer says no'); +deepStrictEqual(parsed.trailers, []); + +console.log('Example: chunked response with trailers'); + +parsed = parseResponse( + Buffer.from(`HTTP/1.1 200 OK +Content-Type: text/plain +Transfer-Encoding: chunked +Trailer: Expires + +7 +Mozilla +9 +Developer +7 +Network +0 +Expires: Wed, 21 Oct 2015 07:28:00 GMT + +`) +); + +console.log(parsed); + +deepStrictEqual(parsed.shouldKeepAlive, true); +deepStrictEqual(parsed.upgrade, false); +deepStrictEqual(parsed.statusCode, 200); +deepStrictEqual(parsed.statusMessage, 'OK'); +deepStrictEqual(parsed.versionMajor, 1); +deepStrictEqual(parsed.versionMinor, 1); +deepStrictEqual(parsed.headers, ['Content-Type', 'text/plain', 'Transfer-Encoding', 'chunked', 'Trailer', 'Expires']); +deepStrictEqual(parsed.body.toString(), 'MozillaDeveloperNetwork'); +deepStrictEqual(parsed.trailers, ['Expires', 'Wed, 21 Oct 2015 07:28:00 GMT']); + +throws(function () { + parseResponse( + Buffer.from(`HTTP/1.1 200 OK +Content-Length: 1 + +`) + ); +}); diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..b1acd04 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,23 @@ +## Tests that currently fail on Node v13.0.1: + +['chunked with (arguably wrong) content length'](https://github.com/creationix/http-parser-js/blob/master/tests/iojs/test-http-parser-durability.js#L1306-L1345) +* Real case from proxy servers: Should ignore `content-length` if both it and `encoding: chunked` are specified + +['duplicate content-length'](https://github.com/creationix/http-parser-js/blob/master/tests/iojs/test-http-parser-durability.js#L220-L245) +* Real case: Allow duplicate (but identical) `content-length` headers + +[parallel/test-http-client-max-header-size_increased](https://github.com/creationix/http-parser-js/tree/master/tests/parallel/test-http-client-max-header-size_increased) +* Test from actual user issue: Extra large headers (configurable max setable in `http-parser-js`) + +[parallel/test-http-max-headers-count](https://github.com/creationix/http-parser-js/tree/master/tests/parallel/test-http-max-headers-count) +* Test from old Node version: Exceeding Node v12's new 8KB header size limit + +['200 malformed header'](https://github.com/creationix/http-parser-js/blob/master/tests/iojs/test-http-parser-durability.js#L1276-L1305) +* Getting blank headers on malformed headers (probably fine, used to crash Node) + + +Whitespace/trimming differences (probably fine): all in https://github.com/creationix/http-parser-js/blob/master/tests/iojs/test-http-parser-durability.js +* 'line folding in header value with CRLF' +* 'line folding in header value with LF' +* 'multiple connection header values with folding and lws and CRLF' +* 'google 301' diff --git a/tests/bench.js b/tests/bench.js index c120567..46ec81b 100644 --- a/tests/bench.js +++ b/tests/bench.js @@ -2,7 +2,7 @@ var HTTPParser = require('../http-parser.js').HTTPParser; var n = 40000; -var request = new Buffer([ +var request = Buffer.from([ 'GET /favicon.ico HTTP/1.1', 'Host: 0.0.0.0=5000', 'User-Agent: Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9) ' @@ -15,7 +15,7 @@ var request = new Buffer([ 'Connection: keep-alive', '', '' ].join('\r\n')); -var response = new Buffer([ +var response = Buffer.from([ 'HTTP/1.1 301 Moved Permanently', 'Location: http://www.google.com/', 'Content-Type: text/html; charset=UTF-8', diff --git a/tests/common.js b/tests/common.js index c6fcad9..0af3e1a 100644 --- a/tests/common.js +++ b/tests/common.js @@ -293,6 +293,10 @@ if (global.gc) { knownGlobals.push(global.gc); } +if (global.performance) { + knownGlobals.push(global.performance); +} + if (global.DTRACE_HTTP_SERVER_RESPONSE) { knownGlobals.push(DTRACE_HTTP_SERVER_RESPONSE); knownGlobals.push(DTRACE_HTTP_SERVER_REQUEST); @@ -343,6 +347,10 @@ if (global.Symbol) { knownGlobals.push(Symbol); } +if (global.queueMicrotask) { + knownGlobals.push(queueMicrotask); +} + function leakedGlobals() { var leaked = []; diff --git a/tests/iojs/test-http-parser-durability.js b/tests/iojs/test-http-parser-durability.js index 9c7f91d..a0749d5 100644 --- a/tests/iojs/test-http-parser-durability.js +++ b/tests/iojs/test-http-parser-durability.js @@ -217,6 +217,32 @@ var cases = [ ], body: 'HELLO' }, + { + name: 'duplicate content-length', + type: REQUEST, + raw: [ + 'GET /get_duplicate_content_length HTTP/1.0', + 'Content-Length: 5', + 'Content-Length: 5', + '', + 'HELLO' + ].join(CRLF), + shouldKeepAlive: false, + msgCompleteOnEOF: false, + httpMajor: 1, + httpMinor: 0, + method: 'GET', + url: '/get_duplicate_content_length', + statusCode: null, + statusText: null, + headers: [ + 'Content-Length', + '5', + 'Content-Length', + '5', + ], + body: 'HELLO' + }, { name: 'post identity body world', type: REQUEST, @@ -739,7 +765,7 @@ var cases = [ name: 'utf-8 path request', type: REQUEST, raw: [ - new Buffer('GET /δ¶/δt/pope?q=1#narf HTTP/1.1', 'utf8') + Buffer.from('GET /δ¶/δt/pope?q=1#narf HTTP/1.1', 'utf8') .toString('binary'), 'Host: github.com', '', '' @@ -749,7 +775,7 @@ var cases = [ httpMajor: 1, httpMinor: 1, method: 'GET', - url: new Buffer('/δ¶/δt/pope?q=1#narf', 'utf8').toString('binary'), + url: Buffer.from('/δ¶/δt/pope?q=1#narf', 'utf8').toString('binary'), statusCode: null, statusText: null, headers: [ @@ -1052,6 +1078,39 @@ var cases = [ body: undefined }, // RESPONSES ================================================================= + { + name: 'chunked upgrade response', + type: RESPONSE, + raw: [ + 'HTTP/1.1 101 Switching Protocols', + 'Connection: upgrade', + 'Upgrade: websocket', + 'Sec-WebSocket-Accept: QV3I5XUXU2CdhtjixE7QCkCcMZM=', + 'Transfer-Encoding: chunked', + '', + 'hello' + ].join(CRLF), + shouldKeepAlive: true, + msgCompleteOnEOF: false, + httpMajor: 1, + httpMinor: 1, + method: null, + url: null, + statusCode: 101, + statusText: 'Switching Protocols', + headers: [ + 'Connection', + 'upgrade', + 'Upgrade', + 'websocket', + 'Sec-WebSocket-Accept', + 'QV3I5XUXU2CdhtjixE7QCkCcMZM=', + 'Transfer-Encoding', + 'chunked', + ], + upgrade: 'hello', + body: undefined + }, { name: 'google 301', type: RESPONSE, @@ -1243,6 +1302,77 @@ var cases = [ ], body: 'This is the data in the first chunk\r\n' }, + { + name: 'chunked with (arguably wrong) content length', + type: RESPONSE, + raw: [ + 'HTTP/1.1 200 OK', + 'Transfer-Encoding: chunked', + 'Content-Length: 13', + 'Content-Type: application/download', + 'Content-Disposition: attachment;filename=test.txt', + 'Connection: keep-alive', + '', + '7', + 'thunk1\n', + '6', + 'thunk2', + '0', + '', '' + ].join(CRLF), + shouldKeepAlive: true, + msgCompleteOnEOF: false, + httpMajor: 1, + httpMinor: 1, + method: null, + url: null, + statusCode: 200, + statusText: 'OK', + headers: [ + 'Transfer-Encoding', + 'chunked', + 'Content-Length', + '13', + 'Content-Type', + 'application/download', + 'Content-Disposition', + 'attachment;filename=test.txt', + 'Connection', + 'keep-alive', + ], + body: 'thunk1\nthunk2' + }, + // Test below does not work, not in Chrome either, but could be detected, + // not sure if it ever actually happens though. + // { + // name: 'incorrectly flagged as chunked with content length', + // type: RESPONSE, + // raw: [ + // 'HTTP/1.1 200 OK', + // 'Transfer-Encoding: chunked', + // 'Content-Length: 13', + // 'Connection: keep-alive', + // '', + // 'thunk1\nthunk2', + // ].join(CRLF), + // shouldKeepAlive: true, + // msgCompleteOnEOF: false, + // httpMajor: 1, + // httpMinor: 1, + // method: null, + // url: null, + // statusCode: 200, + // statusText: 'OK', + // headers: [ + // 'Transfer-Encoding', + // 'chunked', + // 'Content-Length', + // '13', + // 'Connection', + // 'keep-alive', + // ], + // body: 'thunk1\nthunk2' + // }, { name: 'underscore header key', type: RESPONSE, @@ -1650,7 +1780,7 @@ process.setMaxListeners(0); // Test predefined requests/responses cases.forEach(function(testCase) { var parser = new HTTPParser(testCase.type); - var input = new Buffer(testCase.raw, 'binary'); + var input = Buffer.from(testCase.raw, 'binary'); var reqEvents = ['onHeaders']; var completed = false; var message = {}; @@ -1755,7 +1885,7 @@ cases.forEach(function(testCase) { var ret; parser.onHeaders = parser.onBody = parser.onMessageComplete = function() {}; - ret = parser.execute(new Buffer(input)); + ret = parser.execute(Buffer.from(input)); assert.strictEqual(ret, Buffer.byteLength(input)); })(); @@ -1766,10 +1896,10 @@ cases.forEach(function(testCase) { var ret; parser.onHeaders = parser.onBody = parser.onMessageComplete = function() {}; - ret = parser.execute(new Buffer(input)); + ret = parser.execute(Buffer.from(input)); assert.strictEqual(ret, Buffer.byteLength(input)); - input = new Buffer('header-key: header-value\r\n'); + input = Buffer.from('header-key: header-value\r\n'); for (var i = 0; i < 10000; ++i) { ret = parser.execute(input); if (typeof ret !== 'number') { @@ -1790,11 +1920,11 @@ cases.forEach(function(testCase) { type === REQUEST ? 'POST / HTTP/1.0' : 'HTTP/1.0 200 OK', length ); - var input2 = new Buffer('a'); + var input2 = Buffer.from('a'); var ret; parser.onHeaders = parser.onBody = parser.onMessageComplete = function() {}; - ret = parser.execute(new Buffer(input)); + ret = parser.execute(Buffer.from(input)); assert.strictEqual(ret, Buffer.byteLength(input)); for (var i = 0; i < length; ++i) { @@ -1802,7 +1932,7 @@ cases.forEach(function(testCase) { assert.strictEqual(ret, 1); } - ret = parser.execute(new Buffer(input)); + ret = parser.execute(Buffer.from(input)); assert.strictEqual(ret, Buffer.byteLength(input)); }); }); @@ -1815,7 +1945,7 @@ cases.forEach(function(testCase) { var ret; parser.onHeaders = parser.onBody = parser.onMessageComplete = function() {}; - ret = parser.execute(new Buffer(input)); + ret = parser.execute(Buffer.from(input)); if (i === 0) assert.strictEqual(ret, Buffer.byteLength(input)); else { @@ -1834,7 +1964,7 @@ cases.forEach(function(testCase) { var ret; parser.onHeaders = parser.onBody = parser.onMessageComplete = function() {}; - ret = parser.execute(new Buffer(input)); + ret = parser.execute(Buffer.from(input)); if (i === 0) assert.strictEqual(ret, Buffer.byteLength(input)); else { @@ -1951,7 +2081,7 @@ cases.forEach(function(testCase) { for (var i = 0; i < l; i += chunk) { var toread = Math.min(l - i, chunk); ret = parser.execute( - new Buffer(expected.raw.slice(i, i + toread), 'binary') + Buffer.from(expected.raw.slice(i, i + toread), 'binary') ); assert.strictEqual(ret, toread); } @@ -2009,7 +2139,7 @@ console.log('responses okay'); var ret; parser.onHeaders = parser.onBody = parser.onMessageComplete = function() {}; - ret = parser.execute(new Buffer(input)); + ret = parser.execute(Buffer.from(input)); assert.strictEqual(typeof ret !== 'number', true); //assert.strictEqual(/Malformed request line/i.test(ret.message), true); })(); @@ -2022,7 +2152,7 @@ console.log('responses okay'); var ret; parser.onHeaders = parser.onBody = parser.onMessageComplete = function() {}; - ret = parser.execute(new Buffer(input)); + ret = parser.execute(Buffer.from(input)); assert.strictEqual(ret, input.length); })(); @@ -2033,7 +2163,7 @@ console.log('responses okay'); var ret; parser.onHeaders = parser.onBody = parser.onMessageComplete = function() {}; - ret = parser.execute(new Buffer(input)); + ret = parser.execute(Buffer.from(input)); assert.strictEqual(typeof ret !== 'number', true); //assert.strictEqual(/Malformed header line/i.test(ret.message), true); })();*/ @@ -2080,7 +2210,7 @@ console.log('responses okay'); var ret; parser.onHeaders = parser.onBody = parser.onMessageComplete = function() {}; - ret = parser.execute(new Buffer(input)); + ret = parser.execute(Buffer.from(input)); assert.strictEqual(ret, input.length); })(); @@ -2209,7 +2339,7 @@ function testScan(case1, case2, case3) { hasUpgrade = false; nb = 0; - ret = parser.execute(new Buffer(total.slice(0, i), 'binary')); + ret = parser.execute(Buffer.from(total.slice(0, i), 'binary')); assert.strictEqual(typeof ret === 'number', true); nb += ret; @@ -2223,7 +2353,7 @@ function testScan(case1, case2, case3) { if (!hasUpgrade) { assert.strictEqual(nb, i); - ret = parser.execute(new Buffer(total.slice(i, j), 'binary')); + ret = parser.execute(Buffer.from(total.slice(i, j), 'binary')); assert.strictEqual(typeof ret === 'number', true); nb += ret; @@ -2237,7 +2367,7 @@ function testScan(case1, case2, case3) { if (!hasUpgrade) { assert.strictEqual(nb, i + (j - i)); - ret = parser.execute(new Buffer(total.slice(j), 'binary')); + ret = parser.execute(Buffer.from(total.slice(j), 'binary')); assert.strictEqual(typeof ret === 'number', true); nb += ret; @@ -2328,7 +2458,7 @@ function testMultiple3(case1, case2, case3) { message = {}; }; - ret = parser.execute(new Buffer(total, 'binary')); + ret = parser.execute(Buffer.from(total, 'binary')); assert.strictEqual(parser.finish(), undefined); assert.strictEqual(messages.length, messageCount); @@ -2391,7 +2521,7 @@ function createLargeChunkedMessage(bodySizeKB, rawHeaders) { var wrote = 0; var headerslen = rawHeaders.length; var bufsize = headerslen + (5 + 1024 + 2) * bodySizeKB + 5; - var buf = new Buffer(bufsize); + var buf = Buffer.alloc(bufsize); buf.write(rawHeaders, wrote, headerslen, 'binary'); wrote += headerslen; diff --git a/tests/parallel/test-http-agent-destroyed-socket.js b/tests/parallel/test-http-agent-destroyed-socket.js index 34772f7..b6a255f 100644 --- a/tests/parallel/test-http-agent-destroyed-socket.js +++ b/tests/parallel/test-http-agent-destroyed-socket.js @@ -29,6 +29,17 @@ var server = http.createServer(function(req, res) { console.log('request1 socket closed'); }); response.pipe(process.stdout); + response.socket.once('close', function() { + // assert request2 was removed from the queue + assert(!agent.requests[key]); + console.log("waiting for request2.onSocket's nextTick"); + process.nextTick(function() { + // assert that the same socket was not assigned to request2, + // since it was destroyed. + assert(request1.socket !== request2.socket); + assert(!request2.socket.destroyed, 'the socket is destroyed'); + }); + }); response.on('end', function() { console.log('response1 done'); ///////////////////////////////// @@ -44,17 +55,6 @@ var server = http.createServer(function(req, res) { // is triggered. request1.socket.destroy(); - response.once('close', function() { - // assert request2 was removed from the queue - assert(!agent.requests[key]); - console.log("waiting for request2.onSocket's nextTick"); - process.nextTick(function() { - // assert that the same socket was not assigned to request2, - // since it was destroyed. - assert(request1.socket !== request2.socket); - assert(!request2.socket.destroyed, 'the socket is destroyed'); - }); - }); }); }); diff --git a/tests/parallel/test-http-blank-header.js b/tests/parallel/test-http-blank-header.js index 3a804d4..9c3e2a4 100644 --- a/tests/parallel/test-http-blank-header.js +++ b/tests/parallel/test-http-blank-header.js @@ -29,6 +29,9 @@ server.listen(0, function() { '\r\n\r\nhello world\r\n' //need \r\n at end for http-parser-js to fail ); }); + + c.on('data', function () { + }); c.on('end', function() { c.end(); diff --git a/tests/parallel/test-http-chunked-2.js b/tests/parallel/test-http-chunked-2.js new file mode 100644 index 0000000..2175870 --- /dev/null +++ b/tests/parallel/test-http-chunked-2.js @@ -0,0 +1,45 @@ +'use strict'; + +require('../common'); +var assert = require('assert'); +var http = require('http'); +var url = require('url'); + +var responses_sent = 0; +var responses_recvd = 0; +var body0 = ''; +var body1 = ''; + +var server = http.Server(function(req, res) { + this.close(); + req.on('end', function() { + res.writeHead(200, {'Content-Type': 'text/plain', 'Transfer-Encoding': 'chunked'}); + res.write('some'); + res.write('chunked'); + res.write('data'); + res.end(); + }); + req.resume(); +}); +server.listen(0); + +server.on('listening', function() { + var agent = new http.Agent({ port: this.address().port, maxSockets: 1 }); + http.get({ + port: this.address().port, + path: '/hello', + headers: {'Accept': '*/*', 'Foo': 'bar'}, + agent: agent + }, function(res) { + assert.equal(200, res.statusCode); + res.setEncoding('utf8'); + res.on('data', function(chunk) { body0 += chunk; }); + res.on('end', function () { + console.error('Got /hello response', body0); + }); + }); +}); + +process.on('exit', function() { + assert.equal('somechunkeddata', body0); +}); diff --git a/tests/parallel/test-http-client-agent.js b/tests/parallel/test-http-client-agent.js index 36ed24e..3434a0c 100644 --- a/tests/parallel/test-http-client-agent.js +++ b/tests/parallel/test-http-client-agent.js @@ -36,8 +36,8 @@ function request(i) { if (count < max) { assert.equal(http.globalAgent.sockets[name].indexOf(socket), -1); } else { - assert(!http.globalAgent.sockets.hasOwnProperty(name)); - assert(!http.globalAgent.requests.hasOwnProperty(name)); + assert(!http.globalAgent.sockets[name]); + assert(!http.globalAgent.requests[name]); server.close(); } }); diff --git a/tests/parallel/test-http-client-max-header-size_increased.js b/tests/parallel/test-http-client-max-header-size_increased.js index a60d423..537316a 100644 --- a/tests/parallel/test-http-client-max-header-size_increased.js +++ b/tests/parallel/test-http-client-max-header-size_increased.js @@ -2,7 +2,7 @@ require('../common'); // set max header size -require('../../../http-parser-js').HTTPParser.maxHeaderSize = 1024 * 1024; // 1MB instead of 80kb +process.binding('http_parser').HTTPParser.maxHeaderSize = 1024 * 1024; // 1MB instead of 80kb var assert = require('assert'); var http = require('http'); diff --git a/tests/parallel/test-http-client-reject-chunked-with-content-length.js b/tests/parallel/test-http-client-reject-chunked-with-content-length.js deleted file mode 100644 index 324ba00..0000000 --- a/tests/parallel/test-http-client-reject-chunked-with-content-length.js +++ /dev/null @@ -1,28 +0,0 @@ -'use strict'; - -const common = require('../common'); -const http = require('http'); -const net = require('net'); -const assert = require('assert'); - -const reqstr = 'HTTP/1.1 200 OK\r\n' + - 'Content-Length: 1\r\n' + - 'Transfer-Encoding: chunked\r\n\r\n'; - -const server = net.createServer((socket) => { - socket.write(reqstr); -}); - -server.listen(0, () => { - // The callback should not be called because the server is sending - // both a Content-Length header and a Transfer-Encoding: chunked - // header, which is a violation of the HTTP spec. - const req = http.get({port: server.address().port}, (res) => { - assert.fail(null, null, 'callback should not be called'); - }); - req.on('error', common.mustCall((err) => { - assert(/^Parse Error/.test(err.message)); - assert.equal(err.code, 'HPE_UNEXPECTED_CONTENT_LENGTH'); - server.close(); - })); -}); diff --git a/tests/parallel/test-http-connect-req-res.js b/tests/parallel/test-http-connect-req-res.js index c6b545b..ab43b2b 100644 --- a/tests/parallel/test-http-connect-req-res.js +++ b/tests/parallel/test-http-connect-req-res.js @@ -44,8 +44,8 @@ server.listen(0, common.mustCall(function() { // Make sure this request got removed from the pool. const name = 'localhost:' + server.address().port; - assert(!http.globalAgent.sockets.hasOwnProperty(name)); - assert(!http.globalAgent.requests.hasOwnProperty(name)); + assert(!http.globalAgent.sockets[name]); + assert(!http.globalAgent.requests[name]); // Make sure this socket has detached. assert(!socket.ondata); diff --git a/tests/parallel/test-http-connect.js b/tests/parallel/test-http-connect.js index b0efca6..54551ea 100644 --- a/tests/parallel/test-http-connect.js +++ b/tests/parallel/test-http-connect.js @@ -45,8 +45,8 @@ server.listen(0, function() { // Make sure this request got removed from the pool. var name = 'localhost:' + server.address().port; - assert(!http.globalAgent.sockets.hasOwnProperty(name)); - assert(!http.globalAgent.requests.hasOwnProperty(name)); + assert(!http.globalAgent.sockets[name]); + assert(!http.globalAgent.requests[name]); // Make sure this socket has detached. assert(!socket.ondata); diff --git a/tests/parallel/test-http-destroyed-socket-write2.js b/tests/parallel/test-http-destroyed-socket-write2.js index 7d2f235..df03a32 100644 --- a/tests/parallel/test-http-destroyed-socket-write2.js +++ b/tests/parallel/test-http-destroyed-socket-write2.js @@ -47,8 +47,12 @@ server.listen(0, function() { break; } - assert.equal(req.output.length, 0); - assert.equal(req.outputEncodings.length, 0); + if (req.outputData) { // Node v12+ + assert.strictEqual(req.outputData.length, 0); + } else { + assert.equal(req.output.length, 0); + assert.equal(req.outputEncodings.length, 0); + } server.close(); })); diff --git a/tests/parallel/test-http-flush.js b/tests/parallel/test-http-flush.js deleted file mode 100644 index 8b3dacb..0000000 --- a/tests/parallel/test-http-flush.js +++ /dev/null @@ -1,16 +0,0 @@ -'use strict'; -require('../common'); -var http = require('http'); - -http.createServer(function(req, res) { - res.end('ok'); - this.close(); -}).listen(0, '127.0.0.1', function() { - var req = http.request({ - method: 'POST', - host: '127.0.0.1', - port: this.address().port, - }); - req.flush(); // Flush the request headers. - req.flush(); // Should be idempotent. -}); diff --git a/tests/parallel/test-http-header-unicode.js b/tests/parallel/test-http-header-unicode.js new file mode 100644 index 0000000..b44e330 --- /dev/null +++ b/tests/parallel/test-http-header-unicode.js @@ -0,0 +1,17 @@ +'use strict'; + +const common = require('../common'); +const http = require('http'); +const assert = require('assert'); +const net = require("net"); +process.binding('http_parser').HTTPParser.encoding = 'binary'; + +const server = http.createServer(common.mustCall((req, res) => { + assert.equal(Buffer.from(req.headers.test,'binary').toString('utf8'), "😀"); + res.end('ok'); + server.close(); +})); +server.listen(0, () => { + var socket = net.connect({ port: server.address().port }); + socket.end("GET / HTTP/1.1\r\nHost: some\r\nConnection: close\r\nTest: 😀\r\n\r\n"); +}); diff --git a/tests/parallel/test-http-invalid-urls.js b/tests/parallel/test-http-invalid-urls.js index 678e8ec..1e845e1 100644 --- a/tests/parallel/test-http-invalid-urls.js +++ b/tests/parallel/test-http-invalid-urls.js @@ -11,7 +11,7 @@ function test(host) { [http, https].forEach((module) => { assert.throws(() => module[method](host, () => { throw new Error(`${module}.${method} should not connect to ${host}`); - }), error); + }), `${error} ${host}`); }); }); } diff --git a/tests/parallel/test-http-keep-alive.js b/tests/parallel/test-http-keep-alive.js index d48732e..356c691 100644 --- a/tests/parallel/test-http-keep-alive.js +++ b/tests/parallel/test-http-keep-alive.js @@ -38,7 +38,7 @@ server.listen(0, function() { }, function(response) { response.on('end', function() { assert.equal(agent.sockets[name].length, 1); - assert(!agent.requests.hasOwnProperty(name)); + assert(!agent.requests[name]); server.close(); }); response.resume(); @@ -46,6 +46,6 @@ server.listen(0, function() { }); process.on('exit', function() { - assert(!agent.sockets.hasOwnProperty(name)); - assert(!agent.requests.hasOwnProperty(name)); + assert(!agent.sockets[name]); + assert(!agent.requests[name]); }); diff --git a/tests/parallel/test-http-max-headers-count.js b/tests/parallel/test-http-max-headers-count.js index 2d68ead..d86a008 100644 --- a/tests/parallel/test-http-max-headers-count.js +++ b/tests/parallel/test-http-max-headers-count.js @@ -1,3 +1,5 @@ +// Note: If this test fails, monkey-patching probably failed and you're using +// the default Node parser, which now has a limit of 8KB for header data 'use strict'; require('../common'); var assert = require('assert'); diff --git a/tests/parallel/test-http-request-end-twice.js b/tests/parallel/test-http-request-end-twice.js index fe08ae2..74f9a3f 100644 --- a/tests/parallel/test-http-request-end-twice.js +++ b/tests/parallel/test-http-request-end-twice.js @@ -10,7 +10,11 @@ var server = http.Server(function(req, res) { server.listen(0, function() { var req = http.get({port: this.address().port}, function(res) { res.on('end', function() { - assert.ok(!req.end()); + if (parseInt(process.versions.node) < 10) { + assert.ok(!req.end()); + } else { + assert.equal(req.end(), req); + } server.close(); }); res.resume(); diff --git a/tests/parallel/test-http-res-write-after-end.js b/tests/parallel/test-http-res-write-after-end.js index 8dca7ad..1a64f73 100644 --- a/tests/parallel/test-http-res-write-after-end.js +++ b/tests/parallel/test-http-res-write-after-end.js @@ -14,7 +14,9 @@ var server = http.Server(function(req, res) { res.end(); var r = res.write('This should raise an error.'); - assert.equal(r, true, 'write after end should return true'); + // JE: Disabling this, node v15 changed this to return false, not true, + // cannot reasonably test (must not be important!) + // assert.equal(r, true, 'write after end should return true'); }); server.listen(0, function() { diff --git a/tests/parallel/test-http-server-multiheaders2.js b/tests/parallel/test-http-server-multiheaders2.js index b019df8..dfb9fb8 100644 --- a/tests/parallel/test-http-server-multiheaders2.js +++ b/tests/parallel/test-http-server-multiheaders2.js @@ -54,7 +54,7 @@ var srv = http.createServer(function(req, res) { 'foo', 'header parsed incorrectly: ' + header); }); multipleAllowed.forEach(function(header) { - const sep = (process.version < 'v8.0') ? ', ' : (header.toLowerCase() === 'cookie' ? '; ' : ', '); + const sep = parseInt(process.versions.node) < 8 ? ', ' : (header.toLowerCase() === 'cookie' ? '; ' : ', '); assert.strictEqual(req.headers[header.toLowerCase()], `foo${sep}bar`, `header parsed incorrectly: ${header}`); }); diff --git a/tests/parallel/test-http-server-reject-chunked-with-content-length.js b/tests/parallel/test-http-server-reject-chunked-with-content-length.js deleted file mode 100644 index b3284c3..0000000 --- a/tests/parallel/test-http-server-reject-chunked-with-content-length.js +++ /dev/null @@ -1,31 +0,0 @@ -'use strict'; - -const common = require('../common'); -const http = require('http'); -const net = require('net'); -const assert = require('assert'); - -const reqstr = 'POST / HTTP/1.1\r\n' + - 'Content-Length: 1\r\n' + - 'Transfer-Encoding: chunked\r\n\r\n'; - -const server = http.createServer((req, res) => { - assert.fail(null, null, 'callback should not be invoked'); -}); -server.on('clientError', common.mustCall((err) => { - assert(/^Parse Error/.test(err.message)); - assert.equal(err.code, 'HPE_UNEXPECTED_CONTENT_LENGTH'); - server.close(); -})); -server.listen(0, () => { - const client = net.connect({port: server.address().port}, () => { - client.write(reqstr); - client.end(); - }); - client.on('data', (data) => { - // Should not get to this point because the server should simply - // close the connection without returning any data. - assert.fail(null, null, 'no data should be returned by the server'); - }); - client.on('end', common.mustCall(() => {})); -}); diff --git a/tests/parallel/test-http-unix-socket.js b/tests/parallel/test-http-unix-socket.js index bdac056..b5e328b 100644 --- a/tests/parallel/test-http-unix-socket.js +++ b/tests/parallel/test-http-unix-socket.js @@ -46,7 +46,7 @@ server.listen(common.PIPE, function() { server.close(function(error) { assert.equal(error, undefined); server.close(function(error) { - assert.equal(error && error.message, 'Not running'); + assert.equal(error && error.message, parseInt(process.versions.node) < 10 ? 'Not running' : 'Server is not running.'); }); }); }); diff --git a/tests/parallel/test-http-upgrade-agent.js b/tests/parallel/test-http-upgrade-agent.js index 01074d7..00d57cd 100644 --- a/tests/parallel/test-http-upgrade-agent.js +++ b/tests/parallel/test-http-upgrade-agent.js @@ -62,7 +62,7 @@ srv.listen(0, '127.0.0.1', function() { assert.deepStrictEqual(expectedHeaders, res.headers); // Make sure this request got removed from the pool. - assert(!http.globalAgent.sockets.hasOwnProperty(name)); + assert(!http.globalAgent.sockets[name]); req.on('close', function() { socket.end(); diff --git a/tests/parallel/test-http-upgrade-server.js b/tests/parallel/test-http-upgrade-server.js index ba0533c..cc5d9a6 100644 --- a/tests/parallel/test-http-upgrade-server.js +++ b/tests/parallel/test-http-upgrade-server.js @@ -112,6 +112,9 @@ function test_upgrade_no_listener() { 'Connection: Upgrade\r\n' + '\r\n'); }); + + conn.on('data', function () { + }); conn.on('end', function() { test_upgrade_no_listener_ended = true; diff --git a/tests/parallel/test-http-write-head.js b/tests/parallel/test-http-write-head.js index b7c7b6c..db0dd37 100644 --- a/tests/parallel/test-http-write-head.js +++ b/tests/parallel/test-http-write-head.js @@ -25,7 +25,7 @@ var s = http.createServer(function(req, res) { res.setHeader('foo', undefined); } catch (e) { assert.ok(e instanceof Error); - assert.ok(e.message.indexOf('"value"') != -1); + assert.ok(e.message.indexOf('value') != -1); threw = true; } assert.ok(threw, 'Undefined value should throw'); diff --git a/tests/parallel/test-is-monkeypatched.js b/tests/parallel/test-is-monkeypatched.js new file mode 100644 index 0000000..f34ad99 --- /dev/null +++ b/tests/parallel/test-is-monkeypatched.js @@ -0,0 +1,14 @@ +'use strict'; +require('../common'); + +var assert = require('assert'); +var http_common = require('_http_common'); +var http_parser_js = require('../../http-parser.js') + +if (parseInt(process.versions.node) <= 10) { + // not exported, no easy way to check, but we know it works there +} else { + // on newer Node versions, this is exported + assert(http_common.HTTPParser); + assert.equal(http_common.HTTPParser, http_parser_js.HTTPParser); +} diff --git a/tests/parallel/test-standalone-example.js b/tests/parallel/test-standalone-example.js new file mode 100644 index 0000000..cb8d49f --- /dev/null +++ b/tests/parallel/test-standalone-example.js @@ -0,0 +1 @@ +require('../../standalone-example.js'); \ No newline at end of file