diff --git a/HISTORY.md b/HISTORY.md index e92e357..bb576c7 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,7 +1,7 @@ Version history =============== -### 4.0.3 (2018-07-18) +### 4.0.3 (2018-07-18) ### * Update http.js to add support on request for `retry-after` header on `503|429|302` status code. @@ -11,14 +11,14 @@ Version history [Retry-After](https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.37) If a Retry-After header ([RFC2616]) is present in the response, the client SHOULD<6> retry the request after waiting the number of seconds indicated by the Retry-After header. Any such value represents an estimate of when the server is expected to be able to process the request. -### 4.0.1 (2017-10-01) +### 4.0.1 (2017-10-01) ### * Bugfixes from pull requests - Fails to load in IE11 due to timers.setImmediate call - Fix Node 5+ support #4 bug - update object-keys module for Object.keys shim -### 4.0.0 (2017-08-23) +### 4.0.0 (2017-08-23) ### * Rename package from 'http2' to 'http2.js'. * Fork from abandoned https://github.com/molnarg/node-http2 to https://github.com/kaazing/http2.js and change version to '4.0.0' to avoid confusion. @@ -32,6 +32,9 @@ Version history - Typo: finshed (https://github.com/molnarg/node-http2/pull/199) - JSHINT all source (https://github.com/dpwspoon/node-http2/pull/4) - Changed API to be fully pluggable for any transport or server (https://github.com/dpwspoon/node-http2/pull/2) + +### 3.3.8 (2018-02-15) ### +* Fix an issue with HTTP trailers and END_STREAM. ### 3.3.6 (2016-09-16) ### * We were not appropriately sending HPACK context updates when receiving SETTINGS_HEADER_TABLE_SIZE. This release fixes that bug. diff --git a/README.md b/README.md index b0a99b2..753fb52 100644 --- a/README.md +++ b/README.md @@ -121,7 +121,7 @@ point to understand the code. ### Test coverage ### To generate a code coverage report, run `npm test --coverage` (which runs very slowly, be patient). -Code coverage summary as of version 4.0.2: +Code coverage summary as of version 4.0.3: ``` Statements : 89.41% ( 2017/2256 ) Branches : 79.33% ( 852/1074 ) diff --git a/lib/http.js b/lib/http.js index fb4b849..7bbe5ec 100644 --- a/lib/http.js +++ b/lib/http.js @@ -402,7 +402,7 @@ OutgoingMessage.prototype._finish = function _finish() { if (this.request) { this.request.addTrailers(this._trailers); } else { - this.stream.headers(this._trailers); + this.stream.trailers(this._trailers); } } this.finished = true; diff --git a/lib/protocol/connection.js b/lib/protocol/connection.js index 15db759..5f5b687 100644 --- a/lib/protocol/connection.js +++ b/lib/protocol/connection.js @@ -126,7 +126,7 @@ Connection.prototype._initializeStreamManagement = function _initializeStreamMan Connection.prototype._writeControlFrame = function _writeControlFrame(frame) { if ((frame.type === 'SETTINGS') || (frame.type === 'PING') || (frame.type === 'GOAWAY') || (frame.type === 'WINDOW_UPDATE') || - (frame.type === 'ALTSVC')) { + (frame.type === 'ALTSVC') || (frame.type == 'ORIGIN')) { this._log.debug({ frame: frame }, 'Receiving connection level frame'); this.emit(frame.type, frame); } else { @@ -600,6 +600,17 @@ Connection.prototype._receivePing = function _receivePing(frame) { } }; +Connection.prototype.originFrame = function originFrame(originList) { + this._log.debug(originList, 'emitting origin frame'); + + this.push({ + type: 'ORIGIN', + flags: {}, + stream: 0, + originList : originList, + }); +}; + // Terminating the connection Connection.prototype.close = function close(error) { if (this._closed) { diff --git a/lib/protocol/flow.js b/lib/protocol/flow.js index ad4e21e..fc7f071 100644 --- a/lib/protocol/flow.js +++ b/lib/protocol/flow.js @@ -66,7 +66,6 @@ function Flow(flowControlId) { this._queue = []; this._ended = false; this._received = 0; - this._blocked = false; } Flow.prototype = Object.create(Duplex.prototype, { constructor: { value: Flow } }); @@ -159,7 +158,6 @@ Flow.prototype._read = function _read() { // * if there are items in the flow control queue, then let's put them into the output queue (to // the extent it is possible with respect to the window size and output queue feedback) else if (this._window > 0) { - this._blocked = false; this._readableState.sync = true; // to avoid reentrant calls do { var moreNeeded = this._push(this._queue[0]); @@ -175,14 +173,8 @@ Flow.prototype._read = function _read() { } // * otherwise, come back when the flow control window is positive - else if (!this._blocked) { - this._parentPush({ - type: 'BLOCKED', - flags: {}, - stream: this._flowControlId - }); + else { this.once('window_update', this._read); - this._blocked = true; } }; diff --git a/lib/protocol/framer.js b/lib/protocol/framer.js index 1b71ff2..878954c 100644 --- a/lib/protocol/framer.js +++ b/lib/protocol/framer.js @@ -1077,26 +1077,27 @@ Deserializer.ALTSVC = function readAltSvc(buffer, frame) { } }; -// BLOCKED -// ------------------------------------------------------------ -// -// The BLOCKED frame (type=0xB) indicates that the sender is unable to send data -// due to a closed flow control window. -// -// The BLOCKED frame does not define any flags and contains no payload. - -frameTypes[0xB] = 'BLOCKED'; +// frame 0xB was BLOCKED and some versions of chrome will +// throw PROTOCOL_ERROR upon seeing it with non 0 payload -frameFlags.BLOCKED = []; +frameTypes[0xC] = 'ORIGIN'; +frameFlags.ORIGIN = []; +typeSpecificAttributes.ORIGIN = ['originList']; -typeSpecificAttributes.BLOCKED = []; - -Serializer.BLOCKED = function writeBlocked(frame, buffers) { +Serializer.ORIGIN = function writeOrigin(frame, buffers) { + for (var i = 0; i < frame.originList.length; i++) { + var buffer = new Buffer(2); + buffer.writeUInt16BE(frame.originList[i].length, 0); + buffers.push(buffer); + buffers.push(new Buffer(frame.originList[i], 'ascii')); + } }; -Deserializer.BLOCKED = function readBlocked(buffer, frame) { +Deserializer.ORIGIN = function readOrigin(buffer, frame) { + // ignored }; + // [Error Codes](https://tools.ietf.org/html/rfc7540#section-7) // ------------------------------------------------------------ diff --git a/lib/protocol/stream.js b/lib/protocol/stream.js index b3aeb3d..e9fd54a 100644 --- a/lib/protocol/stream.js +++ b/lib/protocol/stream.js @@ -64,6 +64,7 @@ function Stream(log, connection) { this._initializeState(); this.connection = connection; + this.sentEndStream = false; } Stream.prototype = Object.create(Duplex.prototype, { constructor: { value: Stream } }); @@ -108,6 +109,16 @@ Stream.prototype.headers = function headers(headers) { }); }; +Stream.prototype.trailers = function trailers(trailers) { + this.sentEndStream = true; + this._pushUpstream({ + type: 'HEADERS', + flags: {'END_STREAM': true}, + stream: this.id, + headers: trailers + }); +}; + Stream.prototype._onHeaders = function _onHeaders(frame) { if (frame.priority !== undefined) { this.priority(frame.priority, true); @@ -264,7 +275,7 @@ Stream.prototype._writeUpstream = function _writeUpstream(frame) { this._onPriority(frame); } else if (frame.type === 'ALTSVC') { // TODO - } else if (frame.type === 'BLOCKED') { + } else if (frame.type === 'ORIGIN') { // TODO } @@ -374,6 +385,13 @@ Stream.prototype._finishing = function _finishing() { stream: this.id, data: emptyBuffer }; + + if (this.sentEndStream) { + this._log.debug('Already sent END_STREAM, not sending again.'); + return; + } + + this.sentEndStream = true; var lastFrame = this.upstream.getLastQueuedFrame(); if (lastFrame && ((lastFrame.type === 'DATA') || (lastFrame.type === 'HEADERS'))) { this._log.debug({ frame: lastFrame }, 'Marking last frame with END_STREAM flag.'); @@ -445,7 +463,7 @@ Stream.prototype._transition = function transition(sending, frame) { var connectionError; var streamError; - var DATA = false, HEADERS = false, PRIORITY = false, ALTSVC = false, BLOCKED = false; + var DATA = false, HEADERS = false, PRIORITY = false, ALTSVC = false, ORIGIN = false; var RST_STREAM = false, PUSH_PROMISE = false, WINDOW_UPDATE = false; switch(frame.type) { case 'DATA' : DATA = true; break; @@ -455,7 +473,7 @@ Stream.prototype._transition = function transition(sending, frame) { case 'PUSH_PROMISE' : PUSH_PROMISE = true; break; case 'WINDOW_UPDATE': WINDOW_UPDATE = true; break; case 'ALTSVC' : ALTSVC = true; break; - case 'BLOCKED' : BLOCKED = true; break; + case 'ORIGIN' : ORIGIN = true; break; } var previousState = this.state; @@ -515,7 +533,7 @@ Stream.prototype._transition = function transition(sending, frame) { this._setState('CLOSED'); } else if (receiving && HEADERS) { this._setState('HALF_CLOSED_LOCAL'); - } else if (BLOCKED || PRIORITY) { + } else if (PRIORITY || ORIGIN) { /* No state change */ } else { connectionError = 'PROTOCOL_ERROR'; @@ -550,7 +568,7 @@ Stream.prototype._transition = function transition(sending, frame) { case 'HALF_CLOSED_LOCAL': if (RST_STREAM || (receiving && frame.flags.END_STREAM)) { this._setState('CLOSED'); - } else if (BLOCKED || ALTSVC || receiving || PRIORITY || (sending && WINDOW_UPDATE)) { + } else if (ORIGIN || ALTSVC || receiving || PRIORITY || (sending && WINDOW_UPDATE)) { /* No state change */ } else { connectionError = 'PROTOCOL_ERROR'; @@ -570,7 +588,7 @@ Stream.prototype._transition = function transition(sending, frame) { case 'HALF_CLOSED_REMOTE': if (RST_STREAM || (sending && frame.flags.END_STREAM)) { this._setState('CLOSED'); - } else if (BLOCKED || ALTSVC || sending || PRIORITY || (receiving && WINDOW_UPDATE)) { + } else if (ORIGIN || ALTSVC || sending || PRIORITY || (receiving && WINDOW_UPDATE)) { /* No state change */ } else { connectionError = 'PROTOCOL_ERROR'; @@ -602,7 +620,7 @@ Stream.prototype._transition = function transition(sending, frame) { (sending && this._closedWithRst) || (receiving && WINDOW_UPDATE) || (receiving && this._closedByUs && - (this._closedWithRst || RST_STREAM || ALTSVC))) { + (this._closedWithRst || RST_STREAM || ALTSVC || ORIGIN))) { /* No state change */ } else { streamError = 'STREAM_CLOSED';