client-request.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563
  1. 'use strict';
  2. // See https://github.com/facebook/jest/issues/2549
  3. // eslint-disable-next-line node/prefer-global/url
  4. const {URL, urlToHttpOptions} = require('url');
  5. const http2 = require('http2');
  6. const {Writable} = require('stream');
  7. const {Agent, globalAgent} = require('./agent.js');
  8. const IncomingMessage = require('./incoming-message.js');
  9. const proxyEvents = require('./utils/proxy-events.js');
  10. const {
  11. ERR_INVALID_ARG_TYPE,
  12. ERR_INVALID_PROTOCOL,
  13. ERR_HTTP_HEADERS_SENT
  14. } = require('./utils/errors.js');
  15. const validateHeaderName = require('./utils/validate-header-name.js');
  16. const validateHeaderValue = require('./utils/validate-header-value.js');
  17. const proxySocketHandler = require('./utils/proxy-socket-handler.js');
  18. const {
  19. HTTP2_HEADER_STATUS,
  20. HTTP2_HEADER_METHOD,
  21. HTTP2_HEADER_PATH,
  22. HTTP2_HEADER_AUTHORITY,
  23. HTTP2_METHOD_CONNECT
  24. } = http2.constants;
  25. const kHeaders = Symbol('headers');
  26. const kOrigin = Symbol('origin');
  27. const kSession = Symbol('session');
  28. const kOptions = Symbol('options');
  29. const kFlushedHeaders = Symbol('flushedHeaders');
  30. const kJobs = Symbol('jobs');
  31. const kPendingAgentPromise = Symbol('pendingAgentPromise');
  32. class ClientRequest extends Writable {
  33. constructor(input, options, callback) {
  34. super({
  35. autoDestroy: false,
  36. emitClose: false
  37. });
  38. if (typeof input === 'string') {
  39. input = urlToHttpOptions(new URL(input));
  40. } else if (input instanceof URL) {
  41. input = urlToHttpOptions(input);
  42. } else {
  43. input = {...input};
  44. }
  45. if (typeof options === 'function' || options === undefined) {
  46. // (options, callback)
  47. callback = options;
  48. options = input;
  49. } else {
  50. // (input, options, callback)
  51. options = Object.assign(input, options);
  52. }
  53. if (options.h2session) {
  54. this[kSession] = options.h2session;
  55. if (this[kSession].destroyed) {
  56. throw new Error('The session has been closed already');
  57. }
  58. this.protocol = this[kSession].socket.encrypted ? 'https:' : 'http:';
  59. } else if (options.agent === false) {
  60. this.agent = new Agent({maxEmptySessions: 0});
  61. } else if (typeof options.agent === 'undefined' || options.agent === null) {
  62. this.agent = globalAgent;
  63. } else if (typeof options.agent.request === 'function') {
  64. this.agent = options.agent;
  65. } else {
  66. throw new ERR_INVALID_ARG_TYPE('options.agent', ['http2wrapper.Agent-like Object', 'undefined', 'false'], options.agent);
  67. }
  68. if (this.agent) {
  69. this.protocol = this.agent.protocol;
  70. }
  71. if (options.protocol && options.protocol !== this.protocol) {
  72. throw new ERR_INVALID_PROTOCOL(options.protocol, this.protocol);
  73. }
  74. if (!options.port) {
  75. options.port = options.defaultPort || (this.agent && this.agent.defaultPort) || 443;
  76. }
  77. options.host = options.hostname || options.host || 'localhost';
  78. // Unused
  79. delete options.hostname;
  80. const {timeout} = options;
  81. options.timeout = undefined;
  82. this[kHeaders] = Object.create(null);
  83. this[kJobs] = [];
  84. this[kPendingAgentPromise] = undefined;
  85. this.socket = null;
  86. this.connection = null;
  87. this.method = options.method || 'GET';
  88. if (!(this.method === 'CONNECT' && (options.path === '/' || options.path === undefined))) {
  89. this.path = options.path;
  90. }
  91. this.res = null;
  92. this.aborted = false;
  93. this.reusedSocket = false;
  94. const {headers} = options;
  95. if (headers) {
  96. // eslint-disable-next-line guard-for-in
  97. for (const header in headers) {
  98. this.setHeader(header, headers[header]);
  99. }
  100. }
  101. if (options.auth && !('authorization' in this[kHeaders])) {
  102. this[kHeaders].authorization = 'Basic ' + Buffer.from(options.auth).toString('base64');
  103. }
  104. options.session = options.tlsSession;
  105. options.path = options.socketPath;
  106. this[kOptions] = options;
  107. // Clients that generate HTTP/2 requests directly SHOULD use the :authority pseudo-header field instead of the Host header field.
  108. this[kOrigin] = new URL(`${this.protocol}//${options.servername || options.host}:${options.port}`);
  109. // A socket is being reused
  110. const reuseSocket = options._reuseSocket;
  111. if (reuseSocket) {
  112. options.createConnection = (...args) => {
  113. if (reuseSocket.destroyed) {
  114. return this.agent.createConnection(...args);
  115. }
  116. return reuseSocket;
  117. };
  118. // eslint-disable-next-line promise/prefer-await-to-then
  119. this.agent.getSession(this[kOrigin], this[kOptions]).catch(() => {});
  120. }
  121. if (timeout) {
  122. this.setTimeout(timeout);
  123. }
  124. if (callback) {
  125. this.once('response', callback);
  126. }
  127. this[kFlushedHeaders] = false;
  128. }
  129. get method() {
  130. return this[kHeaders][HTTP2_HEADER_METHOD];
  131. }
  132. set method(value) {
  133. if (value) {
  134. this[kHeaders][HTTP2_HEADER_METHOD] = value.toUpperCase();
  135. }
  136. }
  137. get path() {
  138. const header = this.method === 'CONNECT' ? HTTP2_HEADER_AUTHORITY : HTTP2_HEADER_PATH;
  139. return this[kHeaders][header];
  140. }
  141. set path(value) {
  142. if (value) {
  143. const header = this.method === 'CONNECT' ? HTTP2_HEADER_AUTHORITY : HTTP2_HEADER_PATH;
  144. this[kHeaders][header] = value;
  145. }
  146. }
  147. get host() {
  148. return this[kOrigin].hostname;
  149. }
  150. set host(_value) {
  151. // Do nothing as this is read only.
  152. }
  153. get _mustNotHaveABody() {
  154. return this.method === 'GET' || this.method === 'HEAD' || this.method === 'DELETE';
  155. }
  156. _write(chunk, encoding, callback) {
  157. // https://github.com/nodejs/node/blob/654df09ae0c5e17d1b52a900a545f0664d8c7627/lib/internal/http2/util.js#L148-L156
  158. if (this._mustNotHaveABody) {
  159. callback(new Error('The GET, HEAD and DELETE methods must NOT have a body'));
  160. /* istanbul ignore next: Node.js 12 throws directly */
  161. return;
  162. }
  163. this.flushHeaders();
  164. const callWrite = () => this._request.write(chunk, encoding, callback);
  165. if (this._request) {
  166. callWrite();
  167. } else {
  168. this[kJobs].push(callWrite);
  169. }
  170. }
  171. _final(callback) {
  172. this.flushHeaders();
  173. const callEnd = () => {
  174. // For GET, HEAD and DELETE and CONNECT
  175. if (this._mustNotHaveABody || this.method === 'CONNECT') {
  176. callback();
  177. return;
  178. }
  179. this._request.end(callback);
  180. };
  181. if (this._request) {
  182. callEnd();
  183. } else {
  184. this[kJobs].push(callEnd);
  185. }
  186. }
  187. abort() {
  188. if (this.res && this.res.complete) {
  189. return;
  190. }
  191. if (!this.aborted) {
  192. process.nextTick(() => this.emit('abort'));
  193. }
  194. this.aborted = true;
  195. this.destroy();
  196. }
  197. async _destroy(error, callback) {
  198. if (this.res) {
  199. this.res._dump();
  200. }
  201. if (this._request) {
  202. this._request.destroy();
  203. } else {
  204. process.nextTick(() => {
  205. this.emit('close');
  206. });
  207. }
  208. try {
  209. await this[kPendingAgentPromise];
  210. } catch (internalError) {
  211. if (this.aborted) {
  212. error = internalError;
  213. }
  214. }
  215. callback(error);
  216. }
  217. async flushHeaders() {
  218. if (this[kFlushedHeaders] || this.destroyed) {
  219. return;
  220. }
  221. this[kFlushedHeaders] = true;
  222. const isConnectMethod = this.method === HTTP2_METHOD_CONNECT;
  223. // The real magic is here
  224. const onStream = stream => {
  225. this._request = stream;
  226. if (this.destroyed) {
  227. stream.destroy();
  228. return;
  229. }
  230. // Forwards `timeout`, `continue`, `close` and `error` events to this instance.
  231. if (!isConnectMethod) {
  232. // TODO: Should we proxy `close` here?
  233. proxyEvents(stream, this, ['timeout', 'continue']);
  234. }
  235. stream.once('error', error => {
  236. this.destroy(error);
  237. });
  238. stream.once('aborted', () => {
  239. const {res} = this;
  240. if (res) {
  241. res.aborted = true;
  242. res.emit('aborted');
  243. res.destroy();
  244. } else {
  245. this.destroy(new Error('The server aborted the HTTP/2 stream'));
  246. }
  247. });
  248. const onResponse = (headers, flags, rawHeaders) => {
  249. // If we were to emit raw request stream, it would be as fast as the native approach.
  250. // Note that wrapping the raw stream in a Proxy instance won't improve the performance (already tested it).
  251. const response = new IncomingMessage(this.socket, stream.readableHighWaterMark);
  252. this.res = response;
  253. // Undocumented, but it is used by `cacheable-request`
  254. response.url = `${this[kOrigin].origin}${this.path}`;
  255. response.req = this;
  256. response.statusCode = headers[HTTP2_HEADER_STATUS];
  257. response.headers = headers;
  258. response.rawHeaders = rawHeaders;
  259. response.once('end', () => {
  260. response.complete = true;
  261. // Has no effect, just be consistent with the Node.js behavior
  262. response.socket = null;
  263. response.connection = null;
  264. });
  265. if (isConnectMethod) {
  266. response.upgrade = true;
  267. // The HTTP1 API says the socket is detached here,
  268. // but we can't do that so we pass the original HTTP2 request.
  269. if (this.emit('connect', response, stream, Buffer.alloc(0))) {
  270. this.emit('close');
  271. } else {
  272. // No listeners attached, destroy the original request.
  273. stream.destroy();
  274. }
  275. } else {
  276. // Forwards data
  277. stream.on('data', chunk => {
  278. if (!response._dumped && !response.push(chunk)) {
  279. stream.pause();
  280. }
  281. });
  282. stream.once('end', () => {
  283. if (!this.aborted) {
  284. response.push(null);
  285. }
  286. });
  287. if (!this.emit('response', response)) {
  288. // No listeners attached, dump the response.
  289. response._dump();
  290. }
  291. }
  292. };
  293. // This event tells we are ready to listen for the data.
  294. stream.once('response', onResponse);
  295. // Emits `information` event
  296. stream.once('headers', headers => this.emit('information', {statusCode: headers[HTTP2_HEADER_STATUS]}));
  297. stream.once('trailers', (trailers, flags, rawTrailers) => {
  298. const {res} = this;
  299. // https://github.com/nodejs/node/issues/41251
  300. if (res === null) {
  301. onResponse(trailers, flags, rawTrailers);
  302. return;
  303. }
  304. // Assigns trailers to the response object.
  305. res.trailers = trailers;
  306. res.rawTrailers = rawTrailers;
  307. });
  308. stream.once('close', () => {
  309. const {aborted, res} = this;
  310. if (res) {
  311. if (aborted) {
  312. res.aborted = true;
  313. res.emit('aborted');
  314. res.destroy();
  315. }
  316. const finish = () => {
  317. res.emit('close');
  318. this.destroy();
  319. this.emit('close');
  320. };
  321. if (res.readable) {
  322. res.once('end', finish);
  323. } else {
  324. finish();
  325. }
  326. return;
  327. }
  328. if (!this.destroyed) {
  329. this.destroy(new Error('The HTTP/2 stream has been early terminated'));
  330. this.emit('close');
  331. return;
  332. }
  333. this.destroy();
  334. this.emit('close');
  335. });
  336. this.socket = new Proxy(stream, proxySocketHandler);
  337. for (const job of this[kJobs]) {
  338. job();
  339. }
  340. this[kJobs].length = 0;
  341. this.emit('socket', this.socket);
  342. };
  343. if (!(HTTP2_HEADER_AUTHORITY in this[kHeaders]) && !isConnectMethod) {
  344. this[kHeaders][HTTP2_HEADER_AUTHORITY] = this[kOrigin].host;
  345. }
  346. // Makes a HTTP2 request
  347. if (this[kSession]) {
  348. try {
  349. onStream(this[kSession].request(this[kHeaders]));
  350. } catch (error) {
  351. this.destroy(error);
  352. }
  353. } else {
  354. this.reusedSocket = true;
  355. try {
  356. const promise = this.agent.request(this[kOrigin], this[kOptions], this[kHeaders]);
  357. this[kPendingAgentPromise] = promise;
  358. onStream(await promise);
  359. this[kPendingAgentPromise] = false;
  360. } catch (error) {
  361. this[kPendingAgentPromise] = false;
  362. this.destroy(error);
  363. }
  364. }
  365. }
  366. get connection() {
  367. return this.socket;
  368. }
  369. set connection(value) {
  370. this.socket = value;
  371. }
  372. getHeaderNames() {
  373. return Object.keys(this[kHeaders]);
  374. }
  375. hasHeader(name) {
  376. if (typeof name !== 'string') {
  377. throw new ERR_INVALID_ARG_TYPE('name', 'string', name);
  378. }
  379. return Boolean(this[kHeaders][name.toLowerCase()]);
  380. }
  381. getHeader(name) {
  382. if (typeof name !== 'string') {
  383. throw new ERR_INVALID_ARG_TYPE('name', 'string', name);
  384. }
  385. return this[kHeaders][name.toLowerCase()];
  386. }
  387. get headersSent() {
  388. return this[kFlushedHeaders];
  389. }
  390. removeHeader(name) {
  391. if (typeof name !== 'string') {
  392. throw new ERR_INVALID_ARG_TYPE('name', 'string', name);
  393. }
  394. if (this.headersSent) {
  395. throw new ERR_HTTP_HEADERS_SENT('remove');
  396. }
  397. delete this[kHeaders][name.toLowerCase()];
  398. }
  399. setHeader(name, value) {
  400. if (this.headersSent) {
  401. throw new ERR_HTTP_HEADERS_SENT('set');
  402. }
  403. validateHeaderName(name);
  404. validateHeaderValue(name, value);
  405. const lowercased = name.toLowerCase();
  406. if (lowercased === 'connection') {
  407. if (value.toLowerCase() === 'keep-alive') {
  408. return;
  409. }
  410. throw new Error(`Invalid 'connection' header: ${value}`);
  411. }
  412. if (lowercased === 'host' && this.method === 'CONNECT') {
  413. this[kHeaders][HTTP2_HEADER_AUTHORITY] = value;
  414. } else {
  415. this[kHeaders][lowercased] = value;
  416. }
  417. }
  418. setNoDelay() {
  419. // HTTP2 sockets cannot be malformed, do nothing.
  420. }
  421. setSocketKeepAlive() {
  422. // HTTP2 sockets cannot be malformed, do nothing.
  423. }
  424. setTimeout(ms, callback) {
  425. const applyTimeout = () => this._request.setTimeout(ms, callback);
  426. if (this._request) {
  427. applyTimeout();
  428. } else {
  429. this[kJobs].push(applyTimeout);
  430. }
  431. return this;
  432. }
  433. get maxHeadersCount() {
  434. if (!this.destroyed && this._request) {
  435. return this._request.session.localSettings.maxHeaderListSize;
  436. }
  437. return undefined;
  438. }
  439. set maxHeadersCount(_value) {
  440. // Updating HTTP2 settings would affect all requests, do nothing.
  441. }
  442. }
  443. module.exports = ClientRequest;