Source: lib/net/http_fetch_plugin.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.net.HttpFetchPlugin');
  7. goog.require('goog.asserts');
  8. goog.require('shaka.log');
  9. goog.require('shaka.net.HttpPluginUtils');
  10. goog.require('shaka.net.NetworkingEngine');
  11. goog.require('shaka.util.AbortableOperation');
  12. goog.require('shaka.util.Error');
  13. goog.require('shaka.util.MapUtils');
  14. goog.require('shaka.util.Timer');
  15. /**
  16. * @summary A networking plugin to handle http and https URIs via the Fetch API.
  17. * @export
  18. */
  19. shaka.net.HttpFetchPlugin = class {
  20. /**
  21. * @param {string} uri
  22. * @param {shaka.extern.Request} request
  23. * @param {shaka.net.NetworkingEngine.RequestType} requestType
  24. * @param {shaka.extern.ProgressUpdated} progressUpdated Called when a
  25. * progress event happened.
  26. * @param {shaka.extern.HeadersReceived} headersReceived Called when the
  27. * headers for the download are received, but before the body is.
  28. * @return {!shaka.extern.IAbortableOperation.<shaka.extern.Response>}
  29. * @export
  30. */
  31. static parse(uri, request, requestType, progressUpdated, headersReceived) {
  32. const headers = new shaka.net.HttpFetchPlugin.Headers_();
  33. shaka.util.MapUtils.asMap(request.headers).forEach((value, key) => {
  34. headers.append(key, value);
  35. });
  36. const controller = new shaka.net.HttpFetchPlugin.AbortController_();
  37. /** @type {!RequestInit} */
  38. const init = {
  39. // Edge does not treat null as undefined for body; https://bit.ly/2luyE6x
  40. body: request.body || undefined,
  41. headers: headers,
  42. method: request.method,
  43. signal: controller.signal,
  44. credentials: request.allowCrossSiteCredentials ? 'include' : undefined,
  45. };
  46. /** @type {shaka.net.HttpFetchPlugin.AbortStatus} */
  47. const abortStatus = {
  48. canceled: false,
  49. timedOut: false,
  50. };
  51. const pendingRequest = shaka.net.HttpFetchPlugin.request_(
  52. uri, requestType, init, abortStatus, progressUpdated, headersReceived,
  53. request.streamDataCallback);
  54. /** @type {!shaka.util.AbortableOperation} */
  55. const op = new shaka.util.AbortableOperation(pendingRequest, () => {
  56. abortStatus.canceled = true;
  57. controller.abort();
  58. return Promise.resolve();
  59. });
  60. // The fetch API does not timeout natively, so do a timeout manually using
  61. // the AbortController.
  62. const timeoutMs = request.retryParameters.timeout;
  63. if (timeoutMs) {
  64. const timer = new shaka.util.Timer(() => {
  65. abortStatus.timedOut = true;
  66. controller.abort();
  67. });
  68. timer.tickAfter(timeoutMs / 1000);
  69. // To avoid calling |abort| on the network request after it finished, we
  70. // will stop the timer when the requests resolves/rejects.
  71. op.finally(() => {
  72. timer.stop();
  73. });
  74. }
  75. return op;
  76. }
  77. /**
  78. * @param {string} uri
  79. * @param {shaka.net.NetworkingEngine.RequestType} requestType
  80. * @param {!RequestInit} init
  81. * @param {shaka.net.HttpFetchPlugin.AbortStatus} abortStatus
  82. * @param {shaka.extern.ProgressUpdated} progressUpdated
  83. * @param {shaka.extern.HeadersReceived} headersReceived
  84. * @param {?function(BufferSource):!Promise} streamDataCallback
  85. * @return {!Promise<!shaka.extern.Response>}
  86. * @private
  87. */
  88. static async request_(uri, requestType, init, abortStatus, progressUpdated,
  89. headersReceived, streamDataCallback) {
  90. const fetch = shaka.net.HttpFetchPlugin.fetch_;
  91. const ReadableStream = shaka.net.HttpFetchPlugin.ReadableStream_;
  92. let response;
  93. let arrayBuffer;
  94. let loaded = 0;
  95. let lastLoaded = 0;
  96. // Last time stamp when we got a progress event.
  97. let lastTime = Date.now();
  98. try {
  99. // The promise returned by fetch resolves as soon as the HTTP response
  100. // headers are available. The download itself isn't done until the promise
  101. // for retrieving the data (arrayBuffer, blob, etc) has resolved.
  102. response = await fetch(uri, init);
  103. // At this point in the process, we have the headers of the response, but
  104. // not the body yet.
  105. headersReceived(shaka.net.HttpFetchPlugin.headersToGenericObject_(
  106. response.headers));
  107. // Getting the reader in this way allows us to observe the process of
  108. // downloading the body, instead of just waiting for an opaque promise to
  109. // resolve.
  110. // We first clone the response because calling getReader locks the body
  111. // stream; if we didn't clone it here, we would be unable to get the
  112. // response's arrayBuffer later.
  113. const reader = response.clone().body.getReader();
  114. const contentLengthRaw = response.headers.get('Content-Length');
  115. const contentLength =
  116. contentLengthRaw ? parseInt(contentLengthRaw, 10) : 0;
  117. const start = (controller) => {
  118. const push = async () => {
  119. let readObj;
  120. try {
  121. readObj = await reader.read();
  122. } catch (e) {
  123. // If we abort the request, we'll get an error here. Just ignore it
  124. // since real errors will be reported when we read the buffer below.
  125. shaka.log.v1('error reading from stream', e.message);
  126. return;
  127. }
  128. if (!readObj.done) {
  129. loaded += readObj.value.byteLength;
  130. if (streamDataCallback) {
  131. await streamDataCallback(readObj.value);
  132. }
  133. }
  134. const currentTime = Date.now();
  135. // If the time between last time and this time we got progress event
  136. // is long enough, or if a whole segment is downloaded, call
  137. // progressUpdated().
  138. if (currentTime - lastTime > 100 || readObj.done) {
  139. progressUpdated(currentTime - lastTime, loaded - lastLoaded,
  140. contentLength - loaded);
  141. lastLoaded = loaded;
  142. lastTime = currentTime;
  143. }
  144. if (readObj.done) {
  145. goog.asserts.assert(!readObj.value,
  146. 'readObj should be unset when "done" is true.');
  147. controller.close();
  148. } else {
  149. controller.enqueue(readObj.value);
  150. push();
  151. }
  152. };
  153. push();
  154. };
  155. // Create a ReadableStream to use the reader. We don't need to use the
  156. // actual stream for anything, though, as we are using the response's
  157. // arrayBuffer method to get the body, so we don't store the
  158. // ReadableStream.
  159. new ReadableStream({start}); // eslint-disable-line no-new
  160. arrayBuffer = await response.arrayBuffer();
  161. } catch (error) {
  162. if (abortStatus.canceled) {
  163. throw new shaka.util.Error(
  164. shaka.util.Error.Severity.RECOVERABLE,
  165. shaka.util.Error.Category.NETWORK,
  166. shaka.util.Error.Code.OPERATION_ABORTED,
  167. uri, requestType);
  168. } else if (abortStatus.timedOut) {
  169. throw new shaka.util.Error(
  170. shaka.util.Error.Severity.RECOVERABLE,
  171. shaka.util.Error.Category.NETWORK,
  172. shaka.util.Error.Code.TIMEOUT,
  173. uri, requestType);
  174. } else {
  175. throw new shaka.util.Error(
  176. shaka.util.Error.Severity.RECOVERABLE,
  177. shaka.util.Error.Category.NETWORK,
  178. shaka.util.Error.Code.HTTP_ERROR,
  179. uri, error, requestType);
  180. }
  181. }
  182. const headers = shaka.net.HttpFetchPlugin.headersToGenericObject_(
  183. response.headers);
  184. return shaka.net.HttpPluginUtils.makeResponse(
  185. headers, arrayBuffer, response.status, uri, response.url, requestType);
  186. }
  187. /**
  188. * @param {!Headers} headers
  189. * @return {!Object.<string, string>}
  190. * @private
  191. */
  192. static headersToGenericObject_(headers) {
  193. const headersObj = {};
  194. headers.forEach((value, key) => {
  195. // Since Edge incorrectly return the header with a leading new line
  196. // character ('\n'), we trim the header here.
  197. headersObj[key.trim()] = value;
  198. });
  199. return headersObj;
  200. }
  201. /**
  202. * Determine if the Fetch API is supported in the browser. Note: this is
  203. * deliberately exposed as a method to allow the client app to use the same
  204. * logic as Shaka when determining support.
  205. * @return {boolean}
  206. * @export
  207. */
  208. static isSupported() {
  209. // On Edge, ReadableStream exists, but attempting to construct it results in
  210. // an error. See https://bit.ly/2zwaFLL
  211. // So this has to check that ReadableStream is present AND usable.
  212. if (window.ReadableStream) {
  213. try {
  214. new ReadableStream({}); // eslint-disable-line no-new
  215. } catch (e) {
  216. return false;
  217. }
  218. } else {
  219. return false;
  220. }
  221. return !!(window.fetch && window.AbortController);
  222. }
  223. };
  224. /**
  225. * @typedef {{
  226. * canceled: boolean,
  227. * timedOut: boolean
  228. * }}
  229. * @property {boolean} canceled
  230. * Indicates if the request was canceled.
  231. * @property {boolean} timedOut
  232. * Indicates if the request timed out.
  233. */
  234. shaka.net.HttpFetchPlugin.AbortStatus;
  235. /**
  236. * Overridden in unit tests, but compiled out in production.
  237. *
  238. * @const {function(string, !RequestInit)}
  239. * @private
  240. */
  241. shaka.net.HttpFetchPlugin.fetch_ = window.fetch;
  242. /**
  243. * Overridden in unit tests, but compiled out in production.
  244. *
  245. * @const {function(new: AbortController)}
  246. * @private
  247. */
  248. shaka.net.HttpFetchPlugin.AbortController_ = window.AbortController;
  249. /**
  250. * Overridden in unit tests, but compiled out in production.
  251. *
  252. * @const {function(new: ReadableStream, !Object)}
  253. * @private
  254. */
  255. shaka.net.HttpFetchPlugin.ReadableStream_ = window.ReadableStream;
  256. /**
  257. * Overridden in unit tests, but compiled out in production.
  258. *
  259. * @const {function(new: Headers)}
  260. * @private
  261. */
  262. shaka.net.HttpFetchPlugin.Headers_ = window.Headers;
  263. if (shaka.net.HttpFetchPlugin.isSupported()) {
  264. shaka.net.NetworkingEngine.registerScheme(
  265. 'http', shaka.net.HttpFetchPlugin.parse,
  266. shaka.net.NetworkingEngine.PluginPriority.PREFERRED,
  267. /* progressSupport= */ true);
  268. shaka.net.NetworkingEngine.registerScheme(
  269. 'https', shaka.net.HttpFetchPlugin.parse,
  270. shaka.net.NetworkingEngine.PluginPriority.PREFERRED,
  271. /* progressSupport= */ true);
  272. shaka.net.NetworkingEngine.registerScheme(
  273. 'blob', shaka.net.HttpFetchPlugin.parse,
  274. shaka.net.NetworkingEngine.PluginPriority.PREFERRED,
  275. /* progressSupport= */ true);
  276. }