Init
This commit is contained in:
563
node_modules/http2-wrapper/source/client-request.js
generated
vendored
Normal file
563
node_modules/http2-wrapper/source/client-request.js
generated
vendored
Normal file
@ -0,0 +1,563 @@
|
||||
'use strict';
|
||||
// See https://github.com/facebook/jest/issues/2549
|
||||
// eslint-disable-next-line node/prefer-global/url
|
||||
const {URL, urlToHttpOptions} = require('url');
|
||||
const http2 = require('http2');
|
||||
const {Writable} = require('stream');
|
||||
const {Agent, globalAgent} = require('./agent.js');
|
||||
const IncomingMessage = require('./incoming-message.js');
|
||||
const proxyEvents = require('./utils/proxy-events.js');
|
||||
const {
|
||||
ERR_INVALID_ARG_TYPE,
|
||||
ERR_INVALID_PROTOCOL,
|
||||
ERR_HTTP_HEADERS_SENT
|
||||
} = require('./utils/errors.js');
|
||||
const validateHeaderName = require('./utils/validate-header-name.js');
|
||||
const validateHeaderValue = require('./utils/validate-header-value.js');
|
||||
const proxySocketHandler = require('./utils/proxy-socket-handler.js');
|
||||
|
||||
const {
|
||||
HTTP2_HEADER_STATUS,
|
||||
HTTP2_HEADER_METHOD,
|
||||
HTTP2_HEADER_PATH,
|
||||
HTTP2_HEADER_AUTHORITY,
|
||||
HTTP2_METHOD_CONNECT
|
||||
} = http2.constants;
|
||||
|
||||
const kHeaders = Symbol('headers');
|
||||
const kOrigin = Symbol('origin');
|
||||
const kSession = Symbol('session');
|
||||
const kOptions = Symbol('options');
|
||||
const kFlushedHeaders = Symbol('flushedHeaders');
|
||||
const kJobs = Symbol('jobs');
|
||||
const kPendingAgentPromise = Symbol('pendingAgentPromise');
|
||||
|
||||
class ClientRequest extends Writable {
|
||||
constructor(input, options, callback) {
|
||||
super({
|
||||
autoDestroy: false,
|
||||
emitClose: false
|
||||
});
|
||||
|
||||
if (typeof input === 'string') {
|
||||
input = urlToHttpOptions(new URL(input));
|
||||
} else if (input instanceof URL) {
|
||||
input = urlToHttpOptions(input);
|
||||
} else {
|
||||
input = {...input};
|
||||
}
|
||||
|
||||
if (typeof options === 'function' || options === undefined) {
|
||||
// (options, callback)
|
||||
callback = options;
|
||||
options = input;
|
||||
} else {
|
||||
// (input, options, callback)
|
||||
options = Object.assign(input, options);
|
||||
}
|
||||
|
||||
if (options.h2session) {
|
||||
this[kSession] = options.h2session;
|
||||
|
||||
if (this[kSession].destroyed) {
|
||||
throw new Error('The session has been closed already');
|
||||
}
|
||||
|
||||
this.protocol = this[kSession].socket.encrypted ? 'https:' : 'http:';
|
||||
} else if (options.agent === false) {
|
||||
this.agent = new Agent({maxEmptySessions: 0});
|
||||
} else if (typeof options.agent === 'undefined' || options.agent === null) {
|
||||
this.agent = globalAgent;
|
||||
} else if (typeof options.agent.request === 'function') {
|
||||
this.agent = options.agent;
|
||||
} else {
|
||||
throw new ERR_INVALID_ARG_TYPE('options.agent', ['http2wrapper.Agent-like Object', 'undefined', 'false'], options.agent);
|
||||
}
|
||||
|
||||
if (this.agent) {
|
||||
this.protocol = this.agent.protocol;
|
||||
}
|
||||
|
||||
if (options.protocol && options.protocol !== this.protocol) {
|
||||
throw new ERR_INVALID_PROTOCOL(options.protocol, this.protocol);
|
||||
}
|
||||
|
||||
if (!options.port) {
|
||||
options.port = options.defaultPort || (this.agent && this.agent.defaultPort) || 443;
|
||||
}
|
||||
|
||||
options.host = options.hostname || options.host || 'localhost';
|
||||
|
||||
// Unused
|
||||
delete options.hostname;
|
||||
|
||||
const {timeout} = options;
|
||||
options.timeout = undefined;
|
||||
|
||||
this[kHeaders] = Object.create(null);
|
||||
this[kJobs] = [];
|
||||
|
||||
this[kPendingAgentPromise] = undefined;
|
||||
|
||||
this.socket = null;
|
||||
this.connection = null;
|
||||
|
||||
this.method = options.method || 'GET';
|
||||
|
||||
if (!(this.method === 'CONNECT' && (options.path === '/' || options.path === undefined))) {
|
||||
this.path = options.path;
|
||||
}
|
||||
|
||||
this.res = null;
|
||||
this.aborted = false;
|
||||
this.reusedSocket = false;
|
||||
|
||||
const {headers} = options;
|
||||
if (headers) {
|
||||
// eslint-disable-next-line guard-for-in
|
||||
for (const header in headers) {
|
||||
this.setHeader(header, headers[header]);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.auth && !('authorization' in this[kHeaders])) {
|
||||
this[kHeaders].authorization = 'Basic ' + Buffer.from(options.auth).toString('base64');
|
||||
}
|
||||
|
||||
options.session = options.tlsSession;
|
||||
options.path = options.socketPath;
|
||||
|
||||
this[kOptions] = options;
|
||||
|
||||
// Clients that generate HTTP/2 requests directly SHOULD use the :authority pseudo-header field instead of the Host header field.
|
||||
this[kOrigin] = new URL(`${this.protocol}//${options.servername || options.host}:${options.port}`);
|
||||
|
||||
// A socket is being reused
|
||||
const reuseSocket = options._reuseSocket;
|
||||
if (reuseSocket) {
|
||||
options.createConnection = (...args) => {
|
||||
if (reuseSocket.destroyed) {
|
||||
return this.agent.createConnection(...args);
|
||||
}
|
||||
|
||||
return reuseSocket;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line promise/prefer-await-to-then
|
||||
this.agent.getSession(this[kOrigin], this[kOptions]).catch(() => {});
|
||||
}
|
||||
|
||||
if (timeout) {
|
||||
this.setTimeout(timeout);
|
||||
}
|
||||
|
||||
if (callback) {
|
||||
this.once('response', callback);
|
||||
}
|
||||
|
||||
this[kFlushedHeaders] = false;
|
||||
}
|
||||
|
||||
get method() {
|
||||
return this[kHeaders][HTTP2_HEADER_METHOD];
|
||||
}
|
||||
|
||||
set method(value) {
|
||||
if (value) {
|
||||
this[kHeaders][HTTP2_HEADER_METHOD] = value.toUpperCase();
|
||||
}
|
||||
}
|
||||
|
||||
get path() {
|
||||
const header = this.method === 'CONNECT' ? HTTP2_HEADER_AUTHORITY : HTTP2_HEADER_PATH;
|
||||
|
||||
return this[kHeaders][header];
|
||||
}
|
||||
|
||||
set path(value) {
|
||||
if (value) {
|
||||
const header = this.method === 'CONNECT' ? HTTP2_HEADER_AUTHORITY : HTTP2_HEADER_PATH;
|
||||
|
||||
this[kHeaders][header] = value;
|
||||
}
|
||||
}
|
||||
|
||||
get host() {
|
||||
return this[kOrigin].hostname;
|
||||
}
|
||||
|
||||
set host(_value) {
|
||||
// Do nothing as this is read only.
|
||||
}
|
||||
|
||||
get _mustNotHaveABody() {
|
||||
return this.method === 'GET' || this.method === 'HEAD' || this.method === 'DELETE';
|
||||
}
|
||||
|
||||
_write(chunk, encoding, callback) {
|
||||
// https://github.com/nodejs/node/blob/654df09ae0c5e17d1b52a900a545f0664d8c7627/lib/internal/http2/util.js#L148-L156
|
||||
if (this._mustNotHaveABody) {
|
||||
callback(new Error('The GET, HEAD and DELETE methods must NOT have a body'));
|
||||
/* istanbul ignore next: Node.js 12 throws directly */
|
||||
return;
|
||||
}
|
||||
|
||||
this.flushHeaders();
|
||||
|
||||
const callWrite = () => this._request.write(chunk, encoding, callback);
|
||||
if (this._request) {
|
||||
callWrite();
|
||||
} else {
|
||||
this[kJobs].push(callWrite);
|
||||
}
|
||||
}
|
||||
|
||||
_final(callback) {
|
||||
this.flushHeaders();
|
||||
|
||||
const callEnd = () => {
|
||||
// For GET, HEAD and DELETE and CONNECT
|
||||
if (this._mustNotHaveABody || this.method === 'CONNECT') {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
this._request.end(callback);
|
||||
};
|
||||
|
||||
if (this._request) {
|
||||
callEnd();
|
||||
} else {
|
||||
this[kJobs].push(callEnd);
|
||||
}
|
||||
}
|
||||
|
||||
abort() {
|
||||
if (this.res && this.res.complete) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.aborted) {
|
||||
process.nextTick(() => this.emit('abort'));
|
||||
}
|
||||
|
||||
this.aborted = true;
|
||||
|
||||
this.destroy();
|
||||
}
|
||||
|
||||
async _destroy(error, callback) {
|
||||
if (this.res) {
|
||||
this.res._dump();
|
||||
}
|
||||
|
||||
if (this._request) {
|
||||
this._request.destroy();
|
||||
} else {
|
||||
process.nextTick(() => {
|
||||
this.emit('close');
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
await this[kPendingAgentPromise];
|
||||
} catch (internalError) {
|
||||
if (this.aborted) {
|
||||
error = internalError;
|
||||
}
|
||||
}
|
||||
|
||||
callback(error);
|
||||
}
|
||||
|
||||
async flushHeaders() {
|
||||
if (this[kFlushedHeaders] || this.destroyed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this[kFlushedHeaders] = true;
|
||||
|
||||
const isConnectMethod = this.method === HTTP2_METHOD_CONNECT;
|
||||
|
||||
// The real magic is here
|
||||
const onStream = stream => {
|
||||
this._request = stream;
|
||||
|
||||
if (this.destroyed) {
|
||||
stream.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
// Forwards `timeout`, `continue`, `close` and `error` events to this instance.
|
||||
if (!isConnectMethod) {
|
||||
// TODO: Should we proxy `close` here?
|
||||
proxyEvents(stream, this, ['timeout', 'continue']);
|
||||
}
|
||||
|
||||
stream.once('error', error => {
|
||||
this.destroy(error);
|
||||
});
|
||||
|
||||
stream.once('aborted', () => {
|
||||
const {res} = this;
|
||||
if (res) {
|
||||
res.aborted = true;
|
||||
res.emit('aborted');
|
||||
res.destroy();
|
||||
} else {
|
||||
this.destroy(new Error('The server aborted the HTTP/2 stream'));
|
||||
}
|
||||
});
|
||||
|
||||
const onResponse = (headers, flags, rawHeaders) => {
|
||||
// If we were to emit raw request stream, it would be as fast as the native approach.
|
||||
// Note that wrapping the raw stream in a Proxy instance won't improve the performance (already tested it).
|
||||
const response = new IncomingMessage(this.socket, stream.readableHighWaterMark);
|
||||
this.res = response;
|
||||
|
||||
// Undocumented, but it is used by `cacheable-request`
|
||||
response.url = `${this[kOrigin].origin}${this.path}`;
|
||||
|
||||
response.req = this;
|
||||
response.statusCode = headers[HTTP2_HEADER_STATUS];
|
||||
response.headers = headers;
|
||||
response.rawHeaders = rawHeaders;
|
||||
|
||||
response.once('end', () => {
|
||||
response.complete = true;
|
||||
|
||||
// Has no effect, just be consistent with the Node.js behavior
|
||||
response.socket = null;
|
||||
response.connection = null;
|
||||
});
|
||||
|
||||
if (isConnectMethod) {
|
||||
response.upgrade = true;
|
||||
|
||||
// The HTTP1 API says the socket is detached here,
|
||||
// but we can't do that so we pass the original HTTP2 request.
|
||||
if (this.emit('connect', response, stream, Buffer.alloc(0))) {
|
||||
this.emit('close');
|
||||
} else {
|
||||
// No listeners attached, destroy the original request.
|
||||
stream.destroy();
|
||||
}
|
||||
} else {
|
||||
// Forwards data
|
||||
stream.on('data', chunk => {
|
||||
if (!response._dumped && !response.push(chunk)) {
|
||||
stream.pause();
|
||||
}
|
||||
});
|
||||
|
||||
stream.once('end', () => {
|
||||
if (!this.aborted) {
|
||||
response.push(null);
|
||||
}
|
||||
});
|
||||
|
||||
if (!this.emit('response', response)) {
|
||||
// No listeners attached, dump the response.
|
||||
response._dump();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// This event tells we are ready to listen for the data.
|
||||
stream.once('response', onResponse);
|
||||
|
||||
// Emits `information` event
|
||||
stream.once('headers', headers => this.emit('information', {statusCode: headers[HTTP2_HEADER_STATUS]}));
|
||||
|
||||
stream.once('trailers', (trailers, flags, rawTrailers) => {
|
||||
const {res} = this;
|
||||
|
||||
// https://github.com/nodejs/node/issues/41251
|
||||
if (res === null) {
|
||||
onResponse(trailers, flags, rawTrailers);
|
||||
return;
|
||||
}
|
||||
|
||||
// Assigns trailers to the response object.
|
||||
res.trailers = trailers;
|
||||
res.rawTrailers = rawTrailers;
|
||||
});
|
||||
|
||||
stream.once('close', () => {
|
||||
const {aborted, res} = this;
|
||||
if (res) {
|
||||
if (aborted) {
|
||||
res.aborted = true;
|
||||
res.emit('aborted');
|
||||
res.destroy();
|
||||
}
|
||||
|
||||
const finish = () => {
|
||||
res.emit('close');
|
||||
|
||||
this.destroy();
|
||||
this.emit('close');
|
||||
};
|
||||
|
||||
if (res.readable) {
|
||||
res.once('end', finish);
|
||||
} else {
|
||||
finish();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.destroyed) {
|
||||
this.destroy(new Error('The HTTP/2 stream has been early terminated'));
|
||||
this.emit('close');
|
||||
return;
|
||||
}
|
||||
|
||||
this.destroy();
|
||||
this.emit('close');
|
||||
});
|
||||
|
||||
this.socket = new Proxy(stream, proxySocketHandler);
|
||||
|
||||
for (const job of this[kJobs]) {
|
||||
job();
|
||||
}
|
||||
|
||||
this[kJobs].length = 0;
|
||||
|
||||
this.emit('socket', this.socket);
|
||||
};
|
||||
|
||||
if (!(HTTP2_HEADER_AUTHORITY in this[kHeaders]) && !isConnectMethod) {
|
||||
this[kHeaders][HTTP2_HEADER_AUTHORITY] = this[kOrigin].host;
|
||||
}
|
||||
|
||||
// Makes a HTTP2 request
|
||||
if (this[kSession]) {
|
||||
try {
|
||||
onStream(this[kSession].request(this[kHeaders]));
|
||||
} catch (error) {
|
||||
this.destroy(error);
|
||||
}
|
||||
} else {
|
||||
this.reusedSocket = true;
|
||||
|
||||
try {
|
||||
const promise = this.agent.request(this[kOrigin], this[kOptions], this[kHeaders]);
|
||||
this[kPendingAgentPromise] = promise;
|
||||
|
||||
onStream(await promise);
|
||||
|
||||
this[kPendingAgentPromise] = false;
|
||||
} catch (error) {
|
||||
this[kPendingAgentPromise] = false;
|
||||
|
||||
this.destroy(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get connection() {
|
||||
return this.socket;
|
||||
}
|
||||
|
||||
set connection(value) {
|
||||
this.socket = value;
|
||||
}
|
||||
|
||||
getHeaderNames() {
|
||||
return Object.keys(this[kHeaders]);
|
||||
}
|
||||
|
||||
hasHeader(name) {
|
||||
if (typeof name !== 'string') {
|
||||
throw new ERR_INVALID_ARG_TYPE('name', 'string', name);
|
||||
}
|
||||
|
||||
return Boolean(this[kHeaders][name.toLowerCase()]);
|
||||
}
|
||||
|
||||
getHeader(name) {
|
||||
if (typeof name !== 'string') {
|
||||
throw new ERR_INVALID_ARG_TYPE('name', 'string', name);
|
||||
}
|
||||
|
||||
return this[kHeaders][name.toLowerCase()];
|
||||
}
|
||||
|
||||
get headersSent() {
|
||||
return this[kFlushedHeaders];
|
||||
}
|
||||
|
||||
removeHeader(name) {
|
||||
if (typeof name !== 'string') {
|
||||
throw new ERR_INVALID_ARG_TYPE('name', 'string', name);
|
||||
}
|
||||
|
||||
if (this.headersSent) {
|
||||
throw new ERR_HTTP_HEADERS_SENT('remove');
|
||||
}
|
||||
|
||||
delete this[kHeaders][name.toLowerCase()];
|
||||
}
|
||||
|
||||
setHeader(name, value) {
|
||||
if (this.headersSent) {
|
||||
throw new ERR_HTTP_HEADERS_SENT('set');
|
||||
}
|
||||
|
||||
validateHeaderName(name);
|
||||
validateHeaderValue(name, value);
|
||||
|
||||
const lowercased = name.toLowerCase();
|
||||
|
||||
if (lowercased === 'connection') {
|
||||
if (value.toLowerCase() === 'keep-alive') {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error(`Invalid 'connection' header: ${value}`);
|
||||
}
|
||||
|
||||
if (lowercased === 'host' && this.method === 'CONNECT') {
|
||||
this[kHeaders][HTTP2_HEADER_AUTHORITY] = value;
|
||||
} else {
|
||||
this[kHeaders][lowercased] = value;
|
||||
}
|
||||
}
|
||||
|
||||
setNoDelay() {
|
||||
// HTTP2 sockets cannot be malformed, do nothing.
|
||||
}
|
||||
|
||||
setSocketKeepAlive() {
|
||||
// HTTP2 sockets cannot be malformed, do nothing.
|
||||
}
|
||||
|
||||
setTimeout(ms, callback) {
|
||||
const applyTimeout = () => this._request.setTimeout(ms, callback);
|
||||
|
||||
if (this._request) {
|
||||
applyTimeout();
|
||||
} else {
|
||||
this[kJobs].push(applyTimeout);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
get maxHeadersCount() {
|
||||
if (!this.destroyed && this._request) {
|
||||
return this._request.session.localSettings.maxHeaderListSize;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
set maxHeadersCount(_value) {
|
||||
// Updating HTTP2 settings would affect all requests, do nothing.
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ClientRequest;
|
||||
Reference in New Issue
Block a user