-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Support http2. #1399
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
Support http2. #1399
Changes from 5 commits
2b088d5
32056e5
e27e70a
175671a
be22f74
d4867a8
0304d41
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,4 +8,5 @@ test.js | |
| components | ||
| test/node/fixtures/tmp.json | ||
| .idea | ||
| superagent.js | ||
| superagent.js | ||
| package-lock.json | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,186 @@ | ||
| 'use strict'; | ||
|
|
||
| const http2 = require('http2'); | ||
| const Stream = require('stream'); | ||
| const util = require('util'); | ||
| const net = require('net'); | ||
| const tls = require('tls'); | ||
| const parse = require('url').parse; | ||
|
|
||
| const { | ||
| HTTP2_HEADER_PATH, | ||
| HTTP2_HEADER_STATUS, | ||
| HTTP2_HEADER_METHOD, | ||
| HTTP2_HEADER_AUTHORITY, | ||
| HTTP2_HEADER_HOST, | ||
| HTTP2_HEADER_SET_COOKIE, | ||
| NGHTTP2_CANCEL, | ||
| } = http2.constants; | ||
|
|
||
|
|
||
| function setProtocol(protocol) { | ||
| return { | ||
| request: function (options) { | ||
| return new Request(protocol, options); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| function Request(protocol, options) { | ||
| Stream.call(this); | ||
| const defaultPort = protocol === 'https' ? 443 : 80; | ||
| const defaultHost = 'localhost' | ||
| const port = options.port || defaultPort; | ||
| const host = options.host || defaultHost; | ||
|
|
||
| delete options.port | ||
| delete options.host | ||
|
|
||
| this.method = options.method; | ||
| this.path = options.path; | ||
| this.protocol = protocol; | ||
| this.host = host; | ||
|
|
||
| delete options.method | ||
| delete options.path | ||
|
|
||
| const sessionOptions = Object.assign({}, options); | ||
| if (options.socketPath) { | ||
| sessionOptions.socketPath = options.socketPath; | ||
| sessionOptions.createConnection = this.createUnixConnection.bind(this); | ||
| } | ||
|
|
||
| this._headers = {}; | ||
|
|
||
| const session = http2.connect(`${protocol}://${host}:${port}`, sessionOptions); | ||
| this.setHeader('host', `${host}:${port}`) | ||
|
|
||
| session.on('error', (err) => this.emit('error', err)); | ||
|
|
||
| this.session = session; | ||
| } | ||
|
|
||
| /** | ||
| * Inherit from `Stream` (which inherits from `EventEmitter`). | ||
| */ | ||
| util.inherits(Request, Stream); | ||
|
|
||
| Request.prototype.createUnixConnection = function (authority, options) { | ||
| switch (this.protocol) { | ||
| case 'http': | ||
| return net.connect(options.socketPath); | ||
| case 'https': | ||
| options.ALPNProtocols = ['h2']; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. AFAIK browsers also send http/1 here. Do we support a fallback?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Current implement does not support fall back. I can implement fallback, I plan to take fallback after this PR. Or Should I integrate with this PR?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see. If there's no fallback, then that's OK. |
||
| options.servername = this.host; | ||
| options.allowHalfOpen = true; | ||
| return tls.connect(options.socketPath, options); | ||
| default: | ||
| throw new Error('Unsupported protocol', this.protocol); | ||
| } | ||
| } | ||
|
|
||
| Request.prototype.setNoDelay = function (bool) { | ||
| this.session.socket.setNoDelay(bool); | ||
| } | ||
|
|
||
| Request.prototype.getFrame = function () { | ||
| if (this.frame) { | ||
| return this.frame; | ||
| } | ||
|
|
||
| const method = { | ||
| [HTTP2_HEADER_PATH]: this.path, | ||
| [HTTP2_HEADER_METHOD]: this.method, | ||
| } | ||
|
|
||
| let headers = this.mapToHttp2Header(this._headers); | ||
|
|
||
| headers = Object.assign(headers, method); | ||
|
|
||
| const frame = this.session.request(headers); | ||
| frame.once('response', (headers, flags) => { | ||
| headers = this.mapToHttpHeader(headers); | ||
| frame.headers = headers; | ||
| frame.status = frame.statusCode = headers[HTTP2_HEADER_STATUS]; | ||
| this.emit('response', frame); | ||
| }); | ||
|
|
||
| this._headerSent = true; | ||
|
|
||
| frame.once('drain', () => this.emit('drain')); | ||
| frame.on('error', (err) => this.emit('error', err)); | ||
| frame.on('close', () => this.session.close()); | ||
|
|
||
| this.frame = frame; | ||
| return frame; | ||
| } | ||
|
|
||
| Request.prototype.mapToHttpHeader = function (headers) { | ||
| const keys = Object.keys(headers); | ||
| const http2Headers = {}; | ||
| for (var i = 0; i < keys.length; i++) { | ||
| let key = keys[i]; | ||
| let value = headers[key]; | ||
| key = key.toLowerCase(); | ||
| switch (key) { | ||
| case HTTP2_HEADER_SET_COOKIE: | ||
| value = Array.isArray(value) ? value : [value]; | ||
| break; | ||
| default: | ||
| break; | ||
| } | ||
| http2Headers[key] = value; | ||
| } | ||
| return http2Headers; | ||
| } | ||
|
|
||
| Request.prototype.mapToHttp2Header = function (headers) { | ||
| const keys = Object.keys(headers); | ||
| const http2Headers = {}; | ||
| for (var i = 0; i < keys.length; i++) { | ||
| let key = keys[i]; | ||
| let value = headers[key]; | ||
| key = key.toLowerCase(); | ||
| switch (key) { | ||
| case HTTP2_HEADER_HOST: | ||
| key = HTTP2_HEADER_AUTHORITY; | ||
| value = value.startsWith('http') ? parse(value).host : value; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is such conditional parsing needed?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is add for compatibility with Http/1. HTTP2_HEADER_AUTHORITY(alternative Host in http/2) only support domain string, otherwise it raise error. However, I met Host header with schema(like Host: http://example.com) in some express tests.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So it would be better to improve the check, otherwise host like /^https?:\/\//.test(value) |
||
| break; | ||
| default: | ||
| break; | ||
| } | ||
| http2Headers[key] = value; | ||
| } | ||
| return http2Headers; | ||
| } | ||
|
|
||
| Request.prototype.setHeader = function (name, value) { | ||
| this._headers[name.toLowerCase()] = value; | ||
| } | ||
|
|
||
| Request.prototype.getHeader = function (name) { | ||
| return this._headers[name.toLowerCase()]; | ||
| } | ||
|
|
||
| Request.prototype.write = function (data, encoding) { | ||
| const frame = this.getFrame(); | ||
| return frame.write(data, encoding); | ||
| }; | ||
|
|
||
| Request.prototype.pipe = function (stream, options) { | ||
| const frame = this.getFrame(); | ||
| return frame.pipe(stream, options); | ||
| } | ||
|
|
||
| Request.prototype.end = function (data) { | ||
| const frame = this.getFrame(); | ||
| frame.end(data); | ||
| } | ||
|
|
||
| Request.prototype.abort = function (data) { | ||
| const frame = this.getFrame(); | ||
| frame.close(NGHTTP2_CANCEL); | ||
| this.session.close(); | ||
| } | ||
|
|
||
| exports.setProtocol = setProtocol; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it necessary here? In normal implementation we allow
options.hostto be different than host header, because it's useful to dorequest.get('http://[ip address]').host('example.com')to force request to connect to a particular IP address.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think current implementation supports
request.get('http://[ip address]').host('example.com'). I will check later 👍 .There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still not send header here and we can change
Hostheader bysetmethod. I add test for this.