http.mjs 19 KB

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