import { omitBy } from 'lodash' ; import { format } from 'url' ; import { BehaviorSubject } from 'rxjs' ; import { IBasePath , HttpInterceptor , HttpHandler , HttpFetchOptions , HttpResponse , HttpFetchOptionsWithPath , } from './types' ; import { HttpFetchError } from './http_fetch_error' ; import { HttpInterceptController } from './http_intercept_controller' ; import { interceptRequest , interceptResponse } from './intercept' ; import { HttpInterceptHaltError } from './http_intercept_halt_error' ; interface Params { basePath : IBasePath ; opensearchDashboardsVersion : string ; } const JSON_CONTENT = /^(application\/(json|x-javascript)|text\/(x-)?javascript|x-json)(;.*)?$/ ; const NDJSON_CONTENT = /^(application\/ndjson)(;.*)?$/ ; const removedUndefined = ( obj ) => { return omitBy ( obj , ( v ) => v === undefined ) ; } ; export class Fetch { private readonly interceptors = new Set < HttpInterceptor > ( ) ; private readonly requestCount$ = new BehaviorSubject ( 0 ) ; constructor ( private readonly params ) { } public intercept ( interceptor ) { this . interceptors . add ( interceptor ) ; return ( ) => { this . interceptors . delete ( interceptor ) ; } ; } public removeAllInterceptors ( ) { this . interceptors . clear ( ) ; } public getRequestCount$ ( ) { return this . requestCount$ . asObservable ( ) ; } public readonly delete = this . shorthand ( 'DELETE' ) ; public readonly get = this . shorthand ( 'GET' ) ; public readonly head = this . shorthand ( 'HEAD' ) ; public readonly options = this . shorthand ( 'options' ) ; public readonly patch = this . shorthand ( 'PATCH' ) ; public readonly post = this . shorthand ( 'POST' ) ; public readonly put = this . shorthand ( 'PUT' ) ; public fetch : HttpHandler = async < TResponseBody > ( pathOrOptions , options ? ) => { const optionsWithPath = validateFetchArguments ( pathOrOptions , options ) ; const controller = new HttpInterceptController ( ) ; return new Promise < TResponseBody | HttpResponse < TResponseBody > > ( async ( resolve , reject ) => { try { this . requestCount$ . next ( this . requestCount$ . value + 1 ) ; const interceptedOptions = await interceptRequest ( optionsWithPath , this . interceptors , controller ) ; const initialResponse = this . fetchResponse ( interceptedOptions ) ; const interceptedResponse = await interceptResponse ( interceptedOptions , initialResponse , this . interceptors , controller ) ; if ( optionsWithPath . asResponse ) { resolve ( interceptedResponse ) ; } else { resolve ( interceptedResponse . body ) ; } } catch ( error ) { if ( ! ( error instanceof HttpInterceptHaltError ) ) { reject ( error ) ; } } finally { this . requestCount$ . next ( this . requestCount$ . value - 1 ) ; } } ) ; } ; private createRequest ( options ) { const { query , prependBasePath : shouldPrependBasePath , asResponse , asSystemRequest , ... fetchOptions } = { method : 'GET' , credentials : 'same-origin' , prependBasePath : true , ... options , headers : removedUndefined ( { 'Content-Type' : 'application/json' , ... options . headers , 'osd-version' : this . params . opensearchDashboardsVersion , } ) , } ; const url = format ( { pathname : shouldPrependBasePath ? this . params . basePath . prepend ( options . path ) : options . path , query : removedUndefined ( query ) , } ) ; if ( asSystemRequest ) { fetchOptions . headers [ 'osd-system-request' ] = 'true' ; } return new Request ( url , fetchOptions as RequestInit ) ; } private async fetchResponse ( fetchOptions ) { const request = this . createRequest ( fetchOptions ) ; let response ; let body = null ; try { response = await window . fetch ( request ) ; } catch ( err ) { throw new HttpFetchError ( err . message , err . name ?? 'Error' , request ) ; } const contentType = response . headers . get ( 'Content-Type' ) || '' ; try { if ( NDJSON_CONTENT . test ( contentType ) ) { body = await response . blob ( ) ; } else if ( JSON_CONTENT . test ( contentType ) ) { body = await response . json ( ) ; } else { const text = await response . text ( ) ; try { body = JSON . parse ( text ) ; } catch ( err ) { body = text ; } } } catch ( err ) { throw new HttpFetchError ( err . message , err . name ?? 'Error' , request , response , body ) ; } if ( ! response . ok ) { throw new HttpFetchError ( response . statusText , 'Error' , request , response , body ) ; } return { fetchOptions , request , response , body } ; } private shorthand ( method ) { return ( pathOrOptions , options ? ) => { const optionsWithPath = validateFetchArguments ( pathOrOptions , options ) ; return this . fetch ( { ... optionsWithPath , method } ) ; } ; } } /**
 * Ensure that the overloaded arguments to `HttpHandler` are valid.
 */ const validateFetchArguments = ( pathOrOptions , options ? ) : HttpFetchOptionsWithPath => { let fullOptions ; if ( typeof pathOrOptions === 'string' && ( typeof options === 'object' || options === undefined ) ) { fullOptions = { ... options , path : pathOrOptions } ; } else if ( typeof pathOrOptions === 'object' && options === undefined ) { fullOptions = pathOrOptions ; } else { throw new Error ( `Invalid fetch arguments, must either be (string, object) or (object, undefined), received (${ typeof pathOrOptions }, ${ typeof options })` ) ; } const invalidHeaders = Object . keys ( fullOptions . headers ?? { } ) . filter ( ( headerName ) => headerName . startsWith ( 'osd-' ) ) ; if ( invalidHeaders . length ) { throw new Error ( `Invalid fetch headers, headers beginning with "osd-" are not allowed: [${ invalidHeaders . join ( ',' ) }]` ) ; } return fullOptions ; } ;