diff --git a/src/request-pipeline/destination-request/index.ts b/src/request-pipeline/destination-request/index.ts index d065a5b0b..8386b63f5 100644 --- a/src/request-pipeline/destination-request/index.ts +++ b/src/request-pipeline/destination-request/index.ts @@ -49,13 +49,24 @@ export default class DestinationRequest extends EventEmitter implements Destinat this._send(); } + private static _isHttp2ProtocolError (err: Error) { + return err['code'] === 'ERR_HTTP2_STREAM_ERROR' && err.message.includes('NGHTTP2_PROTOCOL_ERROR'); + } + private _sendRealThroughHttp2 (session: ClientHttp2Session) { const reqHeaders = formatRequestHttp2Headers(this.opts); const endStream = !this.opts.body.length; const stream = session.request(reqHeaders, { endStream }); stream.setTimeout(this.timeout, () => this._onTimeout()); - stream.on('error', (err: Error) => this._onError(err)); + stream.on('error', (err: Error) => { + if (DestinationRequest._isHttp2ProtocolError(err)) { + session.destroy(); + this._sendReal(); + } + else + this._onError(err); + }); stream.on('response', headers => { const http2res = createResponseLike(stream, headers); diff --git a/test/server/proxy/http2-client-test.js b/test/server/proxy/http2-client-test.js index 6a386847b..3e97fe17f 100644 --- a/test/server/proxy/http2-client-test.js +++ b/test/server/proxy/http2-client-test.js @@ -3,7 +3,8 @@ const { HTTP2_HEADER_STATUS } = require('http2').constants; const { expect } = require('chai'); const request = require('request-promise-native'); const logger = require('../../../lib/utils/logger'); -const { clearSessionsCache } = require('../../../lib/request-pipeline/destination-request/http2'); +const { noop } = require('lodash'); +const http2Utils = require('../../../lib/request-pipeline/destination-request/http2'); const { CROSS_DOMAIN_SERVER_PORT } = require('../common/constants'); const { @@ -39,7 +40,6 @@ describe('https proxy', () => { } before(() => { - const crossDomainDestinationServer = createDestinationServer(CROSS_DOMAIN_SERVER_PORT, true); const { app: httpsApp } = crossDomainDestinationServer; @@ -73,7 +73,7 @@ describe('https proxy', () => { restoreLoggerFn(logger.destination, 'onHttp2SessionCreated'); restoreLoggerFn(logger.destination, 'onHttp2Unsupported'); http2Server.close(); - clearSessionsCache(); + http2Utils.clearSessionsCache(); httpsServer.close(); }); @@ -124,4 +124,52 @@ describe('https proxy', () => { expect(logs[1][0]).eql('https://127.0.0.1:2002'); }); }); + + // https://github.com/nodejs/node/issues/37849 + it('Should resend a request through https if http2 stream is emitted a protocol error', () => { + const sessionMock = { + destroyCalled: false, + + request: () => ({ + setTimeout: noop, + write: noop, + + end: function () { + setImmediate(() => this._errorHandler({ + code: 'ERR_HTTP2_STREAM_ERROR', + message: 'Stream closed with error code NGHTTP2_PROTOCOL_ERROR' + })); + }, + + on: function (eventName, fn) { + if (eventName === 'error') + this._errorHandler = fn; + } + }), + + destroy: function () { + this.destroyCalled = true; + } + }; + + session.id = 'sessionId'; + + const storedGetHttp2Session = http2Utils.getHttp2Session; + const proxyUrl = getProxyUrl('https://127.0.0.1:2002/stylesheet'); + + http2Utils.getHttp2Session = () => sessionMock; + + proxy.openSession('https://127.0.0.1:2000/', session); + + return request(proxyUrl, { form: { key: 'value' } }) + .then(body => { + http2Utils.getHttp2Session = storedGetHttp2Session; + + const expected = fs.readFileSync('test/server/data/stylesheet/expected.css').toString(); + + compareCode(body, expected); + expect(logs[0]).eql('onHttp2Stream'); + expect(sessionMock.destroyCalled).to.be.true; + }); + }); });