import { Awaitable, CallContext, CallContextLike, HoistService, LoadSpec, LoadSpecConfig, PlainObject, TrackOptions, Span } from '@xh/hoist/core'; import { HoistException } from '@xh/hoist/exception'; import { PromiseTimeoutSpec } from '@xh/hoist/promise'; import { StatusCodes } from 'http-status-codes'; import { IStringifyOptions } from 'qs'; export interface FetchServiceDefaults { autoGenCorrelationIds?: boolean | ((opts: FetchOptions) => boolean); correlationIdHeaderKey?: string; genCorrelationId?: () => string; } /** * Service for making managed HTTP requests, both to the app's own Hoist server and to remote APIs. * * Typically accessed via `XH.fetchService` or the matching convenience aliases on `XH` * (`XH.fetchJson()`, `XH.postJson()`, etc.), which delegate here. * * Wraps the standard Fetch API with CORS enabled, credentials included, and redirects followed. * Provides JSON convenience methods (`fetchJson`, `postJson`, `putJson`, `patchJson`, * `deleteJson`, `getJson`) that handle serialization and content-type headers automatically. * * Key features: * - Configurable timeouts (default 30s) via {@link FetchOptions.timeout} * - Auto-abort of duplicate in-flight requests via {@link FetchOptions.autoAbortKey} * - Optional correlation IDs for request tracking (see `defaults.autoGenCorrelationIds`) * - Request/response interceptors via {@link addInterceptor} * - Default headers for all requests via {@link addDefaultHeaders} * - Rich exception handling with HTTP status, server messages, and trace IDs * * All convenience methods accept the same {@link FetchOptions} as the main `fetch()` entry point. * * @see FetchOptions */ export declare class FetchService extends HoistService { static instance: FetchService; /** App-level defaults for FetchService. Instance options take precedence. */ static defaults: FetchServiceDefaults; NO_JSON_RESPONSES: StatusCodes[]; /** * Regex applied during failed response handling to determine if contentType indicates JSON. * Matches `application/json` as well as variants such as `application/problem+json` */ JSON_CONTENT_TYPE_RE: RegExp; private autoAborters; private _defaultHeaders; private _interceptors; /** Default timeout to be used for all requests made via this service */ defaultTimeout: PromiseTimeoutSpec; /** Default headers to be sent with all subsequent requests. */ get defaultHeaders(): DefaultHeaders[]; /** * Promise handlers to be executed before fulfilling or rejecting returned Promise. * * Use the `onRejected` handler for apps requiring common handling for particular exceptions. * Useful for recognizing 401s (i.e., session end), or wrapping, logging, or enhancing exceptions. * The simplest onRejected handler will simply rethrow the passed exception, or a wrapped version of it. * Such handlers may also return `never()` to prevent further processing of the request -- this * is useful, i.e., if the handler is going to redirect the entire app, or otherwise end normal * app processing. Rejected handlers may also be able to retry and return valid results via * another call to fetch. * * Use the `onFulfilled` handler for enhancing, tracking, or even rejecting "successful" returns. * For example, a handler of this form could be used to transform a 200 response returned by * an API with an "error" flag into a proper client-side exception. */ addInterceptor(handler: FetchInterceptor): void; /** * Add default headers to be sent with all subsequent requests. * @param headers - to be sent with all fetch requests, or a function to generate. */ addDefaultHeaders(headers: DefaultHeaders): void; /** * Send a request via the underlying fetch API. * * This is the main entry point for this API, and can be used to satisfy all * requests. Other shortcut variants will delegate to this method, after setting * default options and pre-processing content. * * Set `asJson` to true return a parsed JSON result, rather than the raw Response. * Note that shortcut variant of this method (e.g. `fetchJson`, `postJson`) will set this * flag for you. * * @param opts - request options. * @param ctx - optional {@link CallContextLike} supplying parent span and load context. * @returns Promise which resolves to a Response or JSON. */ fetch(opts: FetchOptions, ctx?: CallContextLike): Promise; /** * Send an HTTP request and decode the response as JSON. * @returns the decoded JSON object, or null if the response has status in {@link NO_JSON_RESPONSES}. */ fetchJson(opts: FetchOptions, ctx?: CallContextLike): Promise; /** * Send a GET request and decode the response as JSON. * @returns the decoded JSON object, or null if the response status is in {@link NO_JSON_RESPONSES}. */ getJson(opts: FetchOptions, ctx?: CallContextLike): Promise; /** * Send a POST request with a JSON body and decode the response as JSON. * @returns the decoded JSON object, or null if the response status is in {@link NO_JSON_RESPONSES}. */ postJson(opts: FetchOptions, ctx?: CallContextLike): Promise; /** * Send a PUT request with a JSON body and decode the response as JSON. * @returns the decoded JSON object, or null if the response status is in {@link NO_JSON_RESPONSES}. */ putJson(opts: FetchOptions, ctx?: CallContextLike): Promise; /** * Send a PATCH request with a JSON body and decode the response as JSON. * @returns the decoded JSON object, or null if the response status is in {@link NO_JSON_RESPONSES}. */ patchJson(opts: FetchOptions, ctx?: CallContextLike): Promise; /** * Send a DELETE request with optional JSON body and decode the optional response as JSON. * @returns the decoded JSON object, or null if the response status is in {@link NO_JSON_RESPONSES}. */ deleteJson(opts: FetchOptions, ctx?: CallContextLike): Promise; /** * Manually abort any pending request for a given autoAbortKey. * @returns false if no request pending for the given key. */ abort(autoAbortKey: string): boolean; private fetchInternalAsync; private sendJsonInternalAsync; private withCorrelationId; private withTraceId; private withResolvedHeadersAsync; private managedFetchAsync; private abortableFetchAsync; private parseJsonAsync; private safeResponseTextAsync; private createSpanConfig; /** Prefix relative URLs with {@link XH.baseUrl}; leave absolute/root-relative URLs as-is. */ private resolveUrl; private buildFullUrl; private qsFilterFn; /** * Create an Error to throw when a fetch call returns a !ok response. * @param fetchOptions - original options passed to FetchService. * @param response - return value of native fetch. * @param responseText - optional additional details from the server. */ private exceptionFromResponse; /** * Create an Error to throw when a fetchJson call encounters a SyntaxError. * @param fetchOptions - original options passed to FetchService. * @param cause - object thrown by native {@link response.json}. */ private jsonParseException; /** * Create an Error to throw when a fetch call is aborted. * @param fetchOptions - original options passed to FetchService. * @param cause - object thrown by native fetch */ private abortedException; /** * Create an Error to throw when a fetch call times out. * @param fetchOptions - original options the app passed when calling FetchService. * @param cause - underlying timeout exception * @param message - optional custom message * * @returns an exception that is both a TimeoutException, and a FetchException, with the * underlying TimeoutException as its cause. */ private timeoutException; /** * Create an Error for when the server called by fetch does not respond * @param fetchOptions - original options the app passed to FetchService.fetch * @param cause - object thrown by native fetch */ private serverUnavailableException; private createException; private safeParseJson; private extractMessage; } /** Headers to be applied to all requests. Specified as object, or dynamic function to create. */ export type DefaultHeaders = PlainObject | ((opts: FetchOptions) => Awaitable); /** Handlers to be executed before fufilling or rejecting any exception to caller. */ export interface FetchInterceptor { onFulfilled: (opts: FetchOptions, value: any) => Promise; onRejected: (opts: FetchOptions, cause: unknown) => Promise; } /** * Standard options to pass through to fetch, with some additions. * See MDN for available options - {@link https://developer.mozilla.org/en-US/docs/Web/API/Request}. */ export interface FetchOptions { /** URL for the request. Relative urls will be appended to XH.baseUrl. */ url: string; /** * Data to send in the request body (for POSTs/PUTs of JSON). * When using `fetch`, provide a string. Otherwise, provide a JSON Serializable object */ body?: any; /** * Unique identifier for this request, used for tracking and logging. If `false`, no * `correlationId` will be set. If `true`, one will be auto-generated. */ correlationId?: string | boolean; /** * Parameters to encode and append as a query string, or send with the request body * (for POSTs/PUTs sending form-url-encoded). */ params?: PlainObject; /** * HTTP Request method to use for the request. If not specified, the method will be set to POST * if there are params, otherwise GET. */ method?: string; /** * Headers to send with this request. A Content-Type header will be set if not provided by * the caller directly or via one of the xxxJson convenience methods. */ headers?: PlainObject; /** * MS to wait for response before rejecting with a timeout exception. Defaults to 30 seconds, * but may be specified as null to specify no timeout. */ timeout?: PromiseTimeoutSpec; /** * Optional metadata about the underlying request. Passed through for downstream processing by * utils such as {@link ExceptionHandler}. * * @deprecated Pass a {@link CallContextLike} as the second argument to the fetch method instead. */ loadSpec?: LoadSpec | LoadSpecConfig; /** * Options to pass to the underlying fetch request. * @see https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch */ fetchOpts?: PlainObject; /** * Options for qs, the library used to encode query strings. */ qsOpts?: Partial; /** * If set, any pending requests made with the same autoAbortKey will be immediately * aborted in favor of the new request. */ autoAbortKey?: string; /** * True to decode the HTTP response as JSON. Default false. */ asJson?: boolean; /** * If set, the request will be tracked via Hoist activity tracking. (Do not set `correlationId` * here - use the top-level `correlationId` property instead.) */ track?: string | TrackOptions; /** * Parent span for this fetch request. Use to nest fetch calls under a business-level span. * * @deprecated Pass a {@link CallContextLike} as the second argument to the fetch method instead. */ span?: Span; /** * Distributed trace ID for this request. Set automatically by FetchService * @internal */ traceId?: string; } /** * Exception thrown to indicate an HTTP error resulting from a call to FetchService. */ export interface FetchException extends HoistException { /** Http Status code associated with exception. 0 if no response received. */ httpStatus: number; /** Rich object or string containing details about the exception as sent by server. */ serverDetails: string | PlainObject; /** Options of underlying fetch call. */ fetchOptions: FetchOptions; /** CallContext (parent span / load context) in effect when the fetch was issued. */ callContext: CallContext; /** Distributed trace ID associated with the failed request, if tracing was enabled. */ traceId: string; /** * True if exception resulted from the fetch being aborted by fetchService, or the application. * @see FetchService.abort * @see FetchOptions.autoAbortKey */ isFetchAborted: boolean; }