/** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.dev/license */ import {ɵRuntimeError as RuntimeError} from '@angular/core'; import {HttpContext} from './context'; import {RuntimeErrorCode} from './errors'; import {HttpHeaders} from './headers'; import {HttpParams} from './params'; /** * Determine whether the given HTTP method may include a body. */ function mightHaveBody(method: string): boolean { switch (method) { case 'DELETE': case 'GET': case 'HEAD': case 'OPTIONS': case 'JSONP': return false; default: return true; } } /** * Safely assert whether the given value is an ArrayBuffer. * * In some execution environments ArrayBuffer is not defined. */ function isArrayBuffer(value: any): value is ArrayBuffer { return typeof ArrayBuffer !== 'undefined' && value instanceof ArrayBuffer; } /** * Safely assert whether the given value is a Blob. * * In some execution environments Blob is not defined. */ function isBlob(value: any): value is Blob { return typeof Blob !== 'undefined' && value instanceof Blob; } /** * Safely assert whether the given value is a FormData instance. * * In some execution environments FormData is not defined. */ function isFormData(value: any): value is FormData { return typeof FormData !== 'undefined' && value instanceof FormData; } /** * Safely assert whether the given value is a URLSearchParams instance. * * In some execution environments URLSearchParams is not defined. */ function isUrlSearchParams(value: any): value is URLSearchParams { return typeof URLSearchParams !== 'undefined' && value instanceof URLSearchParams; } /** * `Content-Type` is an HTTP header used to indicate the media type * (also known as MIME type) of the resource being sent to the client * or received from the server. */ export const CONTENT_TYPE_HEADER = 'Content-Type'; /** * The `Accept` header is an HTTP request header that indicates the media types * (or content types) the client is willing to receive from the server. */ export const ACCEPT_HEADER = 'Accept'; /** * `text/plain` is a content type used to indicate that the content being * sent is plain text with no special formatting or structured data * like HTML, XML, or JSON. */ export const TEXT_CONTENT_TYPE = 'text/plain'; /** * `application/json` is a content type used to indicate that the content * being sent is in the JSON format. */ export const JSON_CONTENT_TYPE = 'application/json'; /** * `application/json, text/plain, *\/*` is a content negotiation string often seen in the * Accept header of HTTP requests. It indicates the types of content the client is willing * to accept from the server, with a preference for `application/json` and `text/plain`, * but also accepting any other type (*\/*). */ export const ACCEPT_HEADER_VALUE = `${JSON_CONTENT_TYPE}, ${TEXT_CONTENT_TYPE}, */*`; export type HttpResponseType = 'arraybuffer' | 'blob' | 'json' | 'text'; export type HttpTransferCacheRequestOptions = {includeHeaders?: string[]} | boolean; /** * Options to configure the `HttpRequest`, like headers, parameters, etc. * * @publicApi 22.0 */ export interface HttpRequestOptions { headers?: HttpHeaders; context?: HttpContext; reportProgress?: boolean; params?: HttpParams; responseType?: HttpResponseType; withCredentials?: boolean; credentials?: RequestCredentials; keepalive?: boolean; priority?: RequestPriority; cache?: RequestCache; mode?: RequestMode; redirect?: RequestRedirect; referrer?: string; integrity?: string; referrerPolicy?: ReferrerPolicy; /** * This property accepts either a boolean to enable/disable transferring cache for eligible * requests performed using `HttpClient`, or an object, which allows to configure cache * parameters, such as which headers should be included (no headers are included by default). * * Setting this property will override the options passed to `provideClientHydration()` for this * particular request */ transferCache?: HttpTransferCacheRequestOptions; timeout?: number; } /** * An outgoing HTTP request with an optional typed body. * * `HttpRequest` represents an outgoing request, including URL, method, * headers, body, and other request configuration options. Instances should be * assumed to be immutable. To modify a `HttpRequest`, the `clone` * method should be used. * * @publicApi */ export class HttpRequest implements HttpRequestOptions { /** * The request body, or `null` if one isn't set. * * Bodies are not enforced to be immutable, as they can include a reference to any * user-defined data type. However, interceptors should take care to preserve * idempotence by treating them as such. */ readonly body: T | null = null; /** * Outgoing headers for this request. */ readonly headers!: HttpHeaders; /** * Shared and mutable context that can be used by interceptors */ readonly context!: HttpContext; /** * Whether this request should be made in a way that exposes progress events. * * Progress events are expensive (change detection runs on each event) and so * they should only be requested if the consumer intends to monitor them. * * Note: The default `HttpBackend` based on fetch, does not support progress report for uploads. * Set the `HttpXhrBackend` with `withXhr()` if you need this feature. */ readonly reportProgress: boolean = false; /** * Whether this request should be sent with outgoing credentials (cookies). */ readonly withCredentials: boolean = false; /** * The credentials mode of the request, which determines how cookies and HTTP authentication are handled. * This can affect whether cookies are sent with the request, and how authentication is handled. */ readonly credentials!: RequestCredentials; /** * When using the fetch implementation and set to `true`, the browser will not abort the associated request if the page that initiated it is unloaded before the request is complete. */ readonly keepalive: boolean = false; /** * Controls how the request will interact with the browser's HTTP cache. * This affects whether a response is retrieved from the cache, how it is stored, or if it bypasses the cache altogether. */ readonly cache!: RequestCache; /** * Indicates the relative priority of the request. This may be used by the browser to decide the order in which requests are dispatched and resources fetched. */ readonly priority!: RequestPriority; /** * The mode of the request, which determines how the request will interact with the browser's security model. * This can affect things like CORS (Cross-Origin Resource Sharing) and same-origin policies. */ readonly mode!: RequestMode; /** * The redirect mode of the request, which determines how redirects are handled. * This can affect whether the request follows redirects automatically, or if it fails when a redirect occurs. */ readonly redirect!: RequestRedirect; /** * The referrer of the request, which can be used to indicate the origin of the request. * This is useful for security and analytics purposes. * Value is a same-origin URL, "about:client", or the empty string, to set request's referrer. */ readonly referrer!: string; /** * The integrity metadata of the request, which can be used to ensure the request is made with the expected content. * A cryptographic hash of the resource to be fetched by request */ readonly integrity!: string; /** * The referrer policy of the request, which can be used to specify the referrer information to be included with the request. * This can affect the amount of referrer information sent with the request, and can be used to enhance privacy and security. */ readonly referrerPolicy!: ReferrerPolicy; /** * The expected response type of the server. * * This is used to parse the response appropriately before returning it to * the requestee. */ readonly responseType: HttpResponseType = 'json'; /** * The outgoing HTTP request method. */ readonly method: string; /** * Outgoing URL parameters. * * To pass a string representation of HTTP parameters in the URL-query-string format, * the `HttpParamsOptions`' `fromString` may be used. For example: * * ```ts * new HttpParams({fromString: 'angular=awesome'}) * ``` */ readonly params!: HttpParams; /** * The outgoing URL with all URL parameters set. */ readonly urlWithParams: string; /** * The HttpTransferCache option for the request */ readonly transferCache?: HttpTransferCacheRequestOptions; /** * The timeout for the backend HTTP request in ms. */ readonly timeout?: number; constructor(method: 'GET' | 'HEAD', url: string, init?: HttpRequestOptions); constructor(method: 'DELETE' | 'JSONP' | 'OPTIONS', url: string, init?: HttpRequestOptions); constructor(method: 'POST', url: string, body: T | null, init?: HttpRequestOptions); constructor(method: 'PUT' | 'PATCH', url: string, body: T | null, init?: HttpRequestOptions); constructor(method: string, url: string, body: T | null, init?: HttpRequestOptions); constructor( method: string, readonly url: string, third?: T | HttpRequestOptions | null, fourth?: HttpRequestOptions, ) { this.method = method.toUpperCase(); // Next, need to figure out which argument holds the HttpRequestOptions // options, if any. let options: HttpRequestOptions | undefined; // Check whether a body argument is expected. The only valid way to omit // the body argument is to use a known no-body method like GET. if (mightHaveBody(this.method) || !!fourth) { // Body is the third argument, options are the fourth. this.body = third !== undefined ? (third as T) : null; options = fourth; } else { // No body required, options are the third argument. The body stays null. options = third as HttpRequestOptions; } // If options have been passed, interpret them. if (options) { // Normalize reportProgress and withCredentials. this.reportProgress = !!options.reportProgress; this.withCredentials = !!options.withCredentials; this.keepalive = !!options.keepalive; // Override default response type of 'json' if one is provided. if (!!options.responseType) { this.responseType = options.responseType; } // Override headers if they're provided. if (options.headers) { this.headers = options.headers; } if (options.context) { this.context = options.context; } if (options.params) { this.params = options.params; } if (options.priority) { this.priority = options.priority; } if (options.cache) { this.cache = options.cache; } if (options.credentials) { this.credentials = options.credentials; } if (typeof options.timeout === 'number') { // XHR will ignore any value below 1. AbortSignals only accept unsigned integers. if (options.timeout < 1 || !Number.isInteger(options.timeout)) { throw new RuntimeError( RuntimeErrorCode.INVALID_TIMEOUT_VALUE, ngDevMode ? '`timeout` must be a positive integer value' : '', ); } this.timeout = options.timeout; } if (options.mode) { this.mode = options.mode; } if (options.redirect) { this.redirect = options.redirect; } if (options.integrity) { this.integrity = options.integrity; } if (options.referrer) { this.referrer = options.referrer; } if (options.referrerPolicy) { this.referrerPolicy = options.referrerPolicy; } // We do want to assign transferCache even if it's falsy (false is valid value) this.transferCache = options.transferCache; } // If no headers have been passed in, construct a new HttpHeaders instance. this.headers ??= new HttpHeaders(); // If no context have been passed in, construct a new HttpContext instance. this.context ??= new HttpContext(); // If no parameters have been passed in, construct a new HttpUrlEncodedParams instance. if (!this.params) { this.params = new HttpParams(); this.urlWithParams = url; } else { // Encode the parameters to a string in preparation for inclusion in the URL. const params = this.params.toString(); if (params.length === 0) { // No parameters, the visible URL is just the URL given at creation time. this.urlWithParams = url; } else { // Does the URL already have query parameters? Look for '?'. const qIdx = url.indexOf('?'); // There are 3 cases to handle: // 1) No existing parameters -> append '?' followed by params. // 2) '?' exists and is followed by existing query string -> // append '&' followed by params. // 3) '?' exists at the end of the url -> append params directly. // This basically amounts to determining the character, if any, with // which to join the URL and parameters. const sep: string = qIdx === -1 ? '?' : qIdx < url.length - 1 ? '&' : ''; this.urlWithParams = url + sep + params; } } } /** * Transform the free-form body into a serialized format suitable for * transmission to the server. */ serializeBody(): ArrayBuffer | Blob | FormData | URLSearchParams | string | null { // If no body is present, no need to serialize it. if (this.body === null) { return null; } // Check whether the body is already in a serialized form. If so, // it can just be returned directly. if ( typeof this.body === 'string' || isArrayBuffer(this.body) || isBlob(this.body) || isFormData(this.body) || isUrlSearchParams(this.body) ) { return this.body; } // Check whether the body is an instance of HttpUrlEncodedParams. if (this.body instanceof HttpParams) { return this.body.toString(); } // Check whether the body is an object or array, and serialize with JSON if so. if ( typeof this.body === 'object' || typeof this.body === 'boolean' || Array.isArray(this.body) ) { return JSON.stringify(this.body); } // Fall back on toString() for everything else. return (this.body as any).toString(); } /** * Examine the body and attempt to infer an appropriate MIME type * for it. * * If no such type can be inferred, this method will return `null`. */ detectContentTypeHeader(): string | null { // An empty body has no content type. if (this.body === null) { return null; } // FormData bodies rely on the browser's content type assignment. if (isFormData(this.body)) { return null; } // Blobs usually have their own content type. If it doesn't, then // no type can be inferred. if (isBlob(this.body)) { return this.body.type || null; } // Array buffers have unknown contents and thus no type can be inferred. if (isArrayBuffer(this.body)) { return null; } // Technically, strings could be a form of JSON data, but it's safe enough // to assume they're plain strings. if (typeof this.body === 'string') { return TEXT_CONTENT_TYPE; } // `HttpUrlEncodedParams` has its own content-type. if (this.body instanceof HttpParams) { return 'application/x-www-form-urlencoded;charset=UTF-8'; } // Arrays, objects, boolean and numbers will be encoded as JSON. if ( typeof this.body === 'object' || typeof this.body === 'number' || typeof this.body === 'boolean' ) { return JSON_CONTENT_TYPE; } // No type could be inferred. return null; } clone(): HttpRequest; clone( update: HttpRequestOptions & { body?: T | null; method?: string; url?: string; setHeaders?: {[name: string]: string | string[]}; setParams?: {[param: string]: string}; }, ): HttpRequest; clone( update: HttpRequestOptions & { body?: V | null; method?: string; url?: string; setHeaders?: {[name: string]: string | string[]}; setParams?: {[param: string]: string}; }, ): HttpRequest; clone( update: HttpRequestOptions & { body?: any | null; method?: string; url?: string; setHeaders?: {[name: string]: string | string[]}; setParams?: {[param: string]: string}; } = {}, ): HttpRequest { // For method, url, and responseType, take the current value unless // it is overridden in the update hash. const method = update.method || this.method; const url = update.url || this.url; const responseType = update.responseType || this.responseType; const keepalive = update.keepalive ?? this.keepalive; const priority = update.priority || this.priority; const cache = update.cache || this.cache; const mode = update.mode || this.mode; const redirect = update.redirect || this.redirect; const credentials = update.credentials || this.credentials; const referrer = update.referrer || this.referrer; const integrity = update.integrity || this.integrity; const referrerPolicy = update.referrerPolicy || this.referrerPolicy; // Carefully handle the transferCache to differentiate between // `false` and `undefined` in the update args. const transferCache = update.transferCache ?? this.transferCache; const timeout = update.timeout ?? this.timeout; // The body is somewhat special - a `null` value in update.body means // whatever current body is present is being overridden with an empty // body, whereas an `undefined` value in update.body implies no // override. const body = update.body !== undefined ? update.body : this.body; // Carefully handle the boolean options to differentiate between // `false` and `undefined` in the update args. const withCredentials = update.withCredentials ?? this.withCredentials; const reportProgress = update.reportProgress ?? this.reportProgress; // Headers and params may be appended to if `setHeaders` or // `setParams` are used. let headers = update.headers || this.headers; let params = update.params || this.params; // Pass on context if needed const context = update.context ?? this.context; // Check whether the caller has asked to add headers. if (update.setHeaders !== undefined) { // Set every requested header. headers = Object.keys(update.setHeaders).reduce( (headers, name) => headers.set(name, update.setHeaders![name]), headers, ); } // Check whether the caller has asked to set params. if (update.setParams) { // Set every requested param. params = Object.keys(update.setParams).reduce( (params, param) => params.set(param, update.setParams![param]), params, ); } // Finally, construct the new HttpRequest using the pieces from above. return new HttpRequest(method, url, body, { params, headers, context, reportProgress, responseType, withCredentials, transferCache, keepalive, cache, priority, timeout, mode, redirect, credentials, referrer, integrity, referrerPolicy, }); } }