http.mjs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403
  1. /**
  2. * @license Angular v19.2.13
  3. * (c) 2010-2025 Google LLC. https://angular.io/
  4. * License: MIT
  5. */
  6. import { HttpErrorResponse, HttpEventType, HttpClient, HttpHeaders, HttpParams, HttpRequest, HTTP_ROOT_INTERCEPTOR_FNS, HttpResponse } from './module-z3bvLlVg.mjs';
  7. export { FetchBackend, HTTP_INTERCEPTORS, HttpBackend, HttpClientJsonpModule, HttpClientModule, HttpClientXsrfModule, HttpContext, HttpContextToken, HttpFeatureKind, HttpHandler, HttpHeaderResponse, HttpResponseBase, HttpStatusCode, HttpUrlEncodingCodec, HttpXhrBackend, HttpXsrfTokenExtractor, JsonpClientBackend, JsonpInterceptor, provideHttpClient, withFetch, withInterceptors, withInterceptorsFromDi, withJsonpSupport, withNoXsrfProtection, withRequestsMadeViaParent, withXsrfConfiguration, HttpInterceptorHandler as ɵHttpInterceptingHandler, HttpInterceptorHandler as ɵHttpInterceptorHandler, REQUESTS_CONTRIBUTE_TO_STABILITY as ɵREQUESTS_CONTRIBUTE_TO_STABILITY } from './module-z3bvLlVg.mjs';
  8. import { assertInInjectionContext, inject, Injector, ɵResourceImpl as _ResourceImpl, linkedSignal, computed, ResourceStatus, signal, InjectionToken, APP_BOOTSTRAP_LISTENER, ɵperformanceMarkFeature as _performanceMarkFeature, ApplicationRef, TransferState, ɵRuntimeError as _RuntimeError, makeStateKey, ɵtruncateMiddle as _truncateMiddle, ɵformatRuntimeError as _formatRuntimeError } from '@angular/core';
  9. import { of } from 'rxjs';
  10. import { tap } from 'rxjs/operators';
  11. import './xhr-BfNfxNDv.mjs';
  12. import './dom_tokens-rA0ACyx7.mjs';
  13. /**
  14. * `httpResource` makes a reactive HTTP request and exposes the request status and response value as
  15. * a `WritableResource`. By default, it assumes that the backend will return JSON data. To make a
  16. * request that expects a different kind of data, you can use a sub-constructor of `httpResource`,
  17. * such as `httpResource.text`.
  18. *
  19. * @experimental
  20. * @initializerApiFunction
  21. */
  22. const httpResource = (() => {
  23. const jsonFn = makeHttpResourceFn('json');
  24. jsonFn.arrayBuffer = makeHttpResourceFn('arraybuffer');
  25. jsonFn.blob = makeHttpResourceFn('blob');
  26. jsonFn.text = makeHttpResourceFn('text');
  27. return jsonFn;
  28. })();
  29. function makeHttpResourceFn(responseType) {
  30. return function httpResourceRef(request, options) {
  31. options?.injector || assertInInjectionContext(httpResource);
  32. const injector = options?.injector ?? inject(Injector);
  33. return new HttpResourceImpl(injector, () => normalizeRequest(request, responseType), options?.defaultValue, options?.parse, options?.equal);
  34. };
  35. }
  36. function normalizeRequest(request, responseType) {
  37. let unwrappedRequest = typeof request === 'function' ? request() : request;
  38. if (unwrappedRequest === undefined) {
  39. return undefined;
  40. }
  41. else if (typeof unwrappedRequest === 'string') {
  42. unwrappedRequest = { url: unwrappedRequest };
  43. }
  44. const headers = unwrappedRequest.headers instanceof HttpHeaders
  45. ? unwrappedRequest.headers
  46. : new HttpHeaders(unwrappedRequest.headers);
  47. const params = unwrappedRequest.params instanceof HttpParams
  48. ? unwrappedRequest.params
  49. : new HttpParams({ fromObject: unwrappedRequest.params });
  50. return new HttpRequest(unwrappedRequest.method ?? 'GET', unwrappedRequest.url, unwrappedRequest.body ?? null, {
  51. headers,
  52. params,
  53. reportProgress: unwrappedRequest.reportProgress,
  54. withCredentials: unwrappedRequest.withCredentials,
  55. responseType,
  56. context: unwrappedRequest.context,
  57. transferCache: unwrappedRequest.transferCache,
  58. });
  59. }
  60. class HttpResourceImpl extends _ResourceImpl {
  61. client;
  62. _headers = linkedSignal({
  63. source: this.extRequest,
  64. computation: () => undefined,
  65. });
  66. _progress = linkedSignal({
  67. source: this.extRequest,
  68. computation: () => undefined,
  69. });
  70. _statusCode = linkedSignal({
  71. source: this.extRequest,
  72. computation: () => undefined,
  73. });
  74. headers = computed(() => this.status() === ResourceStatus.Resolved || this.status() === ResourceStatus.Error
  75. ? this._headers()
  76. : undefined);
  77. progress = this._progress.asReadonly();
  78. statusCode = this._statusCode.asReadonly();
  79. constructor(injector, request, defaultValue, parse, equal) {
  80. super(request, ({ request, abortSignal }) => {
  81. let sub;
  82. // Track the abort listener so it can be removed if the Observable completes (as a memory
  83. // optimization).
  84. const onAbort = () => sub.unsubscribe();
  85. abortSignal.addEventListener('abort', onAbort);
  86. // Start off stream as undefined.
  87. const stream = signal({ value: undefined });
  88. let resolve;
  89. const promise = new Promise((r) => (resolve = r));
  90. const send = (value) => {
  91. stream.set(value);
  92. resolve?.(stream);
  93. resolve = undefined;
  94. };
  95. sub = this.client.request(request).subscribe({
  96. next: (event) => {
  97. switch (event.type) {
  98. case HttpEventType.Response:
  99. this._headers.set(event.headers);
  100. this._statusCode.set(event.status);
  101. try {
  102. send({ value: parse ? parse(event.body) : event.body });
  103. }
  104. catch (error) {
  105. send({ error });
  106. }
  107. break;
  108. case HttpEventType.DownloadProgress:
  109. this._progress.set(event);
  110. break;
  111. }
  112. },
  113. error: (error) => {
  114. if (error instanceof HttpErrorResponse) {
  115. this._headers.set(error.headers);
  116. this._statusCode.set(error.status);
  117. }
  118. send({ error });
  119. abortSignal.removeEventListener('abort', onAbort);
  120. },
  121. complete: () => {
  122. if (resolve) {
  123. send({ error: new Error('Resource completed before producing a value') });
  124. }
  125. abortSignal.removeEventListener('abort', onAbort);
  126. },
  127. });
  128. return promise;
  129. }, defaultValue, equal, injector);
  130. this.client = injector.get(HttpClient);
  131. }
  132. }
  133. /**
  134. * If your application uses different HTTP origins to make API calls (via `HttpClient`) on the server and
  135. * on the client, the `HTTP_TRANSFER_CACHE_ORIGIN_MAP` token allows you to establish a mapping
  136. * between those origins, so that `HttpTransferCache` feature can recognize those requests as the same
  137. * ones and reuse the data cached on the server during hydration on the client.
  138. *
  139. * **Important note**: the `HTTP_TRANSFER_CACHE_ORIGIN_MAP` token should *only* be provided in
  140. * the *server* code of your application (typically in the `app.server.config.ts` script). Angular throws an
  141. * error if it detects that the token is defined while running on the client.
  142. *
  143. * @usageNotes
  144. *
  145. * When the same API endpoint is accessed via `http://internal-domain.com:8080` on the server and
  146. * via `https://external-domain.com` on the client, you can use the following configuration:
  147. * ```ts
  148. * // in app.server.config.ts
  149. * {
  150. * provide: HTTP_TRANSFER_CACHE_ORIGIN_MAP,
  151. * useValue: {
  152. * 'http://internal-domain.com:8080': 'https://external-domain.com'
  153. * }
  154. * }
  155. * ```
  156. *
  157. * @publicApi
  158. */
  159. const HTTP_TRANSFER_CACHE_ORIGIN_MAP = new InjectionToken(ngDevMode ? 'HTTP_TRANSFER_CACHE_ORIGIN_MAP' : '');
  160. /**
  161. * Keys within cached response data structure.
  162. */
  163. const BODY = 'b';
  164. const HEADERS = 'h';
  165. const STATUS = 's';
  166. const STATUS_TEXT = 'st';
  167. const REQ_URL = 'u';
  168. const RESPONSE_TYPE = 'rt';
  169. const CACHE_OPTIONS = new InjectionToken(ngDevMode ? 'HTTP_TRANSFER_STATE_CACHE_OPTIONS' : '');
  170. /**
  171. * A list of allowed HTTP methods to cache.
  172. */
  173. const ALLOWED_METHODS = ['GET', 'HEAD'];
  174. function transferCacheInterceptorFn(req, next) {
  175. const { isCacheActive, ...globalOptions } = inject(CACHE_OPTIONS);
  176. const { transferCache: requestOptions, method: requestMethod } = req;
  177. // In the following situations we do not want to cache the request
  178. if (!isCacheActive ||
  179. requestOptions === false ||
  180. // POST requests are allowed either globally or at request level
  181. (requestMethod === 'POST' && !globalOptions.includePostRequests && !requestOptions) ||
  182. (requestMethod !== 'POST' && !ALLOWED_METHODS.includes(requestMethod)) ||
  183. // Do not cache request that require authorization when includeRequestsWithAuthHeaders is falsey
  184. (!globalOptions.includeRequestsWithAuthHeaders && hasAuthHeaders(req)) ||
  185. globalOptions.filter?.(req) === false) {
  186. return next(req);
  187. }
  188. const transferState = inject(TransferState);
  189. const originMap = inject(HTTP_TRANSFER_CACHE_ORIGIN_MAP, {
  190. optional: true,
  191. });
  192. if (typeof ngServerMode !== 'undefined' && !ngServerMode && originMap) {
  193. throw new _RuntimeError(2803 /* RuntimeErrorCode.HTTP_ORIGIN_MAP_USED_IN_CLIENT */, ngDevMode &&
  194. 'Angular detected that the `HTTP_TRANSFER_CACHE_ORIGIN_MAP` token is configured and ' +
  195. 'present in the client side code. Please ensure that this token is only provided in the ' +
  196. 'server code of the application.');
  197. }
  198. const requestUrl = typeof ngServerMode !== 'undefined' && ngServerMode && originMap
  199. ? mapRequestOriginUrl(req.url, originMap)
  200. : req.url;
  201. const storeKey = makeCacheKey(req, requestUrl);
  202. const response = transferState.get(storeKey, null);
  203. let headersToInclude = globalOptions.includeHeaders;
  204. if (typeof requestOptions === 'object' && requestOptions.includeHeaders) {
  205. // Request-specific config takes precedence over the global config.
  206. headersToInclude = requestOptions.includeHeaders;
  207. }
  208. if (response) {
  209. const { [BODY]: undecodedBody, [RESPONSE_TYPE]: responseType, [HEADERS]: httpHeaders, [STATUS]: status, [STATUS_TEXT]: statusText, [REQ_URL]: url, } = response;
  210. // Request found in cache. Respond using it.
  211. let body = undecodedBody;
  212. switch (responseType) {
  213. case 'arraybuffer':
  214. body = new TextEncoder().encode(undecodedBody).buffer;
  215. break;
  216. case 'blob':
  217. body = new Blob([undecodedBody]);
  218. break;
  219. }
  220. // We want to warn users accessing a header provided from the cache
  221. // That HttpTransferCache alters the headers
  222. // The warning will be logged a single time by HttpHeaders instance
  223. let headers = new HttpHeaders(httpHeaders);
  224. if (typeof ngDevMode === 'undefined' || ngDevMode) {
  225. // Append extra logic in dev mode to produce a warning when a header
  226. // that was not transferred to the client is accessed in the code via `get`
  227. // and `has` calls.
  228. headers = appendMissingHeadersDetection(req.url, headers, headersToInclude ?? []);
  229. }
  230. return of(new HttpResponse({
  231. body,
  232. headers,
  233. status,
  234. statusText,
  235. url,
  236. }));
  237. }
  238. // Request not found in cache. Make the request and cache it if on the server.
  239. return next(req).pipe(tap((event) => {
  240. if (event instanceof HttpResponse && typeof ngServerMode !== 'undefined' && ngServerMode) {
  241. transferState.set(storeKey, {
  242. [BODY]: event.body,
  243. [HEADERS]: getFilteredHeaders(event.headers, headersToInclude),
  244. [STATUS]: event.status,
  245. [STATUS_TEXT]: event.statusText,
  246. [REQ_URL]: requestUrl,
  247. [RESPONSE_TYPE]: req.responseType,
  248. });
  249. }
  250. }));
  251. }
  252. /** @returns true when the requests contains autorization related headers. */
  253. function hasAuthHeaders(req) {
  254. return req.headers.has('authorization') || req.headers.has('proxy-authorization');
  255. }
  256. function getFilteredHeaders(headers, includeHeaders) {
  257. if (!includeHeaders) {
  258. return {};
  259. }
  260. const headersMap = {};
  261. for (const key of includeHeaders) {
  262. const values = headers.getAll(key);
  263. if (values !== null) {
  264. headersMap[key] = values;
  265. }
  266. }
  267. return headersMap;
  268. }
  269. function sortAndConcatParams(params) {
  270. return [...params.keys()]
  271. .sort()
  272. .map((k) => `${k}=${params.getAll(k)}`)
  273. .join('&');
  274. }
  275. function makeCacheKey(request, mappedRequestUrl) {
  276. // make the params encoded same as a url so it's easy to identify
  277. const { params, method, responseType } = request;
  278. const encodedParams = sortAndConcatParams(params);
  279. let serializedBody = request.serializeBody();
  280. if (serializedBody instanceof URLSearchParams) {
  281. serializedBody = sortAndConcatParams(serializedBody);
  282. }
  283. else if (typeof serializedBody !== 'string') {
  284. serializedBody = '';
  285. }
  286. const key = [method, responseType, mappedRequestUrl, serializedBody, encodedParams].join('|');
  287. const hash = generateHash(key);
  288. return makeStateKey(hash);
  289. }
  290. /**
  291. * A method that returns a hash representation of a string using a variant of DJB2 hash
  292. * algorithm.
  293. *
  294. * This is the same hashing logic that is used to generate component ids.
  295. */
  296. function generateHash(value) {
  297. let hash = 0;
  298. for (const char of value) {
  299. hash = (Math.imul(31, hash) + char.charCodeAt(0)) << 0;
  300. }
  301. // Force positive number hash.
  302. // 2147483647 = equivalent of Integer.MAX_VALUE.
  303. hash += 2147483647 + 1;
  304. return hash.toString();
  305. }
  306. /**
  307. * Returns the DI providers needed to enable HTTP transfer cache.
  308. *
  309. * By default, when using server rendering, requests are performed twice: once on the server and
  310. * other one on the browser.
  311. *
  312. * When these providers are added, requests performed on the server are cached and reused during the
  313. * bootstrapping of the application in the browser thus avoiding duplicate requests and reducing
  314. * load time.
  315. *
  316. */
  317. function withHttpTransferCache(cacheOptions) {
  318. return [
  319. {
  320. provide: CACHE_OPTIONS,
  321. useFactory: () => {
  322. _performanceMarkFeature('NgHttpTransferCache');
  323. return { isCacheActive: true, ...cacheOptions };
  324. },
  325. },
  326. {
  327. provide: HTTP_ROOT_INTERCEPTOR_FNS,
  328. useValue: transferCacheInterceptorFn,
  329. multi: true,
  330. },
  331. {
  332. provide: APP_BOOTSTRAP_LISTENER,
  333. multi: true,
  334. useFactory: () => {
  335. const appRef = inject(ApplicationRef);
  336. const cacheState = inject(CACHE_OPTIONS);
  337. return () => {
  338. appRef.whenStable().then(() => {
  339. cacheState.isCacheActive = false;
  340. });
  341. };
  342. },
  343. },
  344. ];
  345. }
  346. /**
  347. * This function will add a proxy to an HttpHeader to intercept calls to get/has
  348. * and log a warning if the header entry requested has been removed
  349. */
  350. function appendMissingHeadersDetection(url, headers, headersToInclude) {
  351. const warningProduced = new Set();
  352. return new Proxy(headers, {
  353. get(target, prop) {
  354. const value = Reflect.get(target, prop);
  355. const methods = new Set(['get', 'has', 'getAll']);
  356. if (typeof value !== 'function' || !methods.has(prop)) {
  357. return value;
  358. }
  359. return (headerName) => {
  360. // We log when the key has been removed and a warning hasn't been produced for the header
  361. const key = (prop + ':' + headerName).toLowerCase(); // e.g. `get:cache-control`
  362. if (!headersToInclude.includes(headerName) && !warningProduced.has(key)) {
  363. warningProduced.add(key);
  364. const truncatedUrl = _truncateMiddle(url);
  365. // TODO: create Error guide for this warning
  366. console.warn(_formatRuntimeError(2802 /* RuntimeErrorCode.HEADERS_ALTERED_BY_TRANSFER_CACHE */, `Angular detected that the \`${headerName}\` header is accessed, but the value of the header ` +
  367. `was not transferred from the server to the client by the HttpTransferCache. ` +
  368. `To include the value of the \`${headerName}\` header for the \`${truncatedUrl}\` request, ` +
  369. `use the \`includeHeaders\` list. The \`includeHeaders\` can be defined either ` +
  370. `on a request level by adding the \`transferCache\` parameter, or on an application ` +
  371. `level by adding the \`httpCacheTransfer.includeHeaders\` argument to the ` +
  372. `\`provideClientHydration()\` call. `));
  373. }
  374. // invoking the original method
  375. return value.apply(target, [headerName]);
  376. };
  377. },
  378. });
  379. }
  380. function mapRequestOriginUrl(url, originMap) {
  381. const origin = new URL(url, 'resolve://').origin;
  382. const mappedOrigin = originMap[origin];
  383. if (!mappedOrigin) {
  384. return url;
  385. }
  386. if (typeof ngDevMode === 'undefined' || ngDevMode) {
  387. verifyMappedOrigin(mappedOrigin);
  388. }
  389. return url.replace(origin, mappedOrigin);
  390. }
  391. function verifyMappedOrigin(url) {
  392. if (new URL(url, 'resolve://').pathname !== '/') {
  393. throw new _RuntimeError(2804 /* RuntimeErrorCode.HTTP_ORIGIN_MAP_CONTAINS_PATH */, 'Angular detected a URL with a path segment in the value provided for the ' +
  394. `\`HTTP_TRANSFER_CACHE_ORIGIN_MAP\` token: ${url}. The map should only contain origins ` +
  395. 'without any other segments.');
  396. }
  397. }
  398. export { HTTP_TRANSFER_CACHE_ORIGIN_MAP, HttpClient, HttpErrorResponse, HttpEventType, HttpHeaders, HttpParams, HttpRequest, HttpResponse, httpResource, HTTP_ROOT_INTERCEPTOR_FNS as ɵHTTP_ROOT_INTERCEPTOR_FNS, withHttpTransferCache as ɵwithHttpTransferCache };
  399. //# sourceMappingURL=http.mjs.map