timed-out.js 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. 'use strict';
  2. const net = require('net');
  3. class TimeoutError extends Error {
  4. constructor(threshold, event) {
  5. super(`Timeout awaiting '${event}' for ${threshold}ms`);
  6. this.name = 'TimeoutError';
  7. this.code = 'ETIMEDOUT';
  8. this.event = event;
  9. }
  10. }
  11. const reentry = Symbol('reentry');
  12. const noop = () => {};
  13. module.exports = (request, delays, options) => {
  14. /* istanbul ignore next: this makes sure timed-out isn't called twice */
  15. if (request[reentry]) {
  16. return;
  17. }
  18. request[reentry] = true;
  19. let stopNewTimeouts = false;
  20. const addTimeout = (delay, callback, ...args) => {
  21. // An error had been thrown before. Going further would result in uncaught errors.
  22. // See https://github.com/sindresorhus/got/issues/631#issuecomment-435675051
  23. if (stopNewTimeouts) {
  24. return noop;
  25. }
  26. // Event loop order is timers, poll, immediates.
  27. // The timed event may emit during the current tick poll phase, so
  28. // defer calling the handler until the poll phase completes.
  29. let immediate;
  30. const timeout = setTimeout(() => {
  31. immediate = setImmediate(callback, delay, ...args);
  32. /* istanbul ignore next: added in node v9.7.0 */
  33. if (immediate.unref) {
  34. immediate.unref();
  35. }
  36. }, delay);
  37. /* istanbul ignore next: in order to support electron renderer */
  38. if (timeout.unref) {
  39. timeout.unref();
  40. }
  41. const cancel = () => {
  42. clearTimeout(timeout);
  43. clearImmediate(immediate);
  44. };
  45. cancelers.push(cancel);
  46. return cancel;
  47. };
  48. const {host, hostname} = options;
  49. const timeoutHandler = (delay, event) => {
  50. request.emit('error', new TimeoutError(delay, event));
  51. request.once('error', () => {}); // Ignore the `socket hung up` error made by request.abort()
  52. request.abort();
  53. };
  54. const cancelers = [];
  55. const cancelTimeouts = () => {
  56. stopNewTimeouts = true;
  57. cancelers.forEach(cancelTimeout => cancelTimeout());
  58. };
  59. request.once('error', cancelTimeouts);
  60. request.once('response', response => {
  61. response.once('end', cancelTimeouts);
  62. });
  63. if (delays.request !== undefined) {
  64. addTimeout(delays.request, timeoutHandler, 'request');
  65. }
  66. if (delays.socket !== undefined) {
  67. const socketTimeoutHandler = () => {
  68. timeoutHandler(delays.socket, 'socket');
  69. };
  70. request.setTimeout(delays.socket, socketTimeoutHandler);
  71. // `request.setTimeout(0)` causes a memory leak.
  72. // We can just remove the listener and forget about the timer - it's unreffed.
  73. // See https://github.com/sindresorhus/got/issues/690
  74. cancelers.push(() => request.removeListener('timeout', socketTimeoutHandler));
  75. }
  76. if (delays.lookup !== undefined && !request.socketPath && !net.isIP(hostname || host)) {
  77. request.once('socket', socket => {
  78. /* istanbul ignore next: hard to test */
  79. if (socket.connecting) {
  80. const cancelTimeout = addTimeout(delays.lookup, timeoutHandler, 'lookup');
  81. socket.once('lookup', cancelTimeout);
  82. }
  83. });
  84. }
  85. if (delays.connect !== undefined) {
  86. request.once('socket', socket => {
  87. /* istanbul ignore next: hard to test */
  88. if (socket.connecting) {
  89. const timeConnect = () => addTimeout(delays.connect, timeoutHandler, 'connect');
  90. if (request.socketPath || net.isIP(hostname || host)) {
  91. socket.once('connect', timeConnect());
  92. } else {
  93. socket.once('lookup', error => {
  94. if (error === null) {
  95. socket.once('connect', timeConnect());
  96. }
  97. });
  98. }
  99. }
  100. });
  101. }
  102. if (delays.secureConnect !== undefined && options.protocol === 'https:') {
  103. request.once('socket', socket => {
  104. /* istanbul ignore next: hard to test */
  105. if (socket.connecting) {
  106. socket.once('connect', () => {
  107. const cancelTimeout = addTimeout(delays.secureConnect, timeoutHandler, 'secureConnect');
  108. socket.once('secureConnect', cancelTimeout);
  109. });
  110. }
  111. });
  112. }
  113. if (delays.send !== undefined) {
  114. request.once('socket', socket => {
  115. const timeRequest = () => addTimeout(delays.send, timeoutHandler, 'send');
  116. /* istanbul ignore next: hard to test */
  117. if (socket.connecting) {
  118. socket.once('connect', () => {
  119. request.once('upload-complete', timeRequest());
  120. });
  121. } else {
  122. request.once('upload-complete', timeRequest());
  123. }
  124. });
  125. }
  126. if (delays.response !== undefined) {
  127. request.once('upload-complete', () => {
  128. const cancelTimeout = addTimeout(delays.response, timeoutHandler, 'response');
  129. request.once('response', cancelTimeout);
  130. });
  131. }
  132. };
  133. module.exports.TimeoutError = TimeoutError;