diff --git a/src/core/public/http/http_service.test.ts b/src/core/public/http/http_service.test.ts index a2a79eee064fdd7..be2d573b8c99399 100644 --- a/src/core/public/http/http_service.test.ts +++ b/src/core/public/http/http_service.test.ts @@ -200,16 +200,18 @@ describe('http requests', async () => { it('should make requests for NDJSON content', async () => { const { http } = setupService(); const content = readFileSync(join(__dirname, '_import_objects.ndjson'), { encoding: 'utf-8' }); + const body = new FormData(); + body.append('file', content); fetchMock.post('*', { body: content, headers: { 'Content-Type': 'application/ndjson' }, }); const data = await http.post('/my/path', { - body: content, + body, headers: { - 'Content-Type': 'multipart/form-data', + 'Content-Type': undefined, }, }); diff --git a/src/core/public/http/http_service.ts b/src/core/public/http/http_service.ts index f7933666fa63c14..635c1704bb8cf6d 100644 --- a/src/core/public/http/http_service.ts +++ b/src/core/public/http/http_service.ts @@ -44,28 +44,34 @@ export class HttpService { private readonly stop$ = new Rx.Subject(); public setup({ basePath, injectedMetadata, fatalErrors }: Deps) { - const defaults: HttpFetchOptions = { - method: 'GET', - credentials: 'same-origin', - prependBasePath: true, - headers: { - 'Content-Type': 'application/json', - 'kbn-version': injectedMetadata.getKibanaVersion(), - }, - }; - async function fetch(path: string, options: HttpFetchOptions = {}): Promise { - const { query, prependBasePath, ...fetchOptions } = merge({}, defaults, options); + const { query, prependBasePath, ...fetchOptions } = merge({ + method: 'GET', + credentials: 'same-origin', + prependBasePath: true, + headers: { + 'kbn-version': injectedMetadata.getKibanaVersion(), + 'Content-Type': 'application/json', + } + }, options); const url = format({ pathname: prependBasePath ? basePath.addToPath(path) : path, query, }); + if ( + options.headers && + 'Content-Type' in options.headers && + options.headers['Content-Type'] === undefined + ) { + delete options.headers['Content-Type']; + } + let response; let body = null; try { - response = await window.fetch(url, fetchOptions); + response = await window.fetch(url, fetchOptions as RequestInit); } catch (err) { throw new HttpFetchError(err.message); } diff --git a/src/core/public/http/types.ts b/src/core/public/http/types.ts index cdce74498a74a65..6a862c52b66f031 100644 --- a/src/core/public/http/types.ts +++ b/src/core/public/http/types.ts @@ -21,6 +21,24 @@ import { BasePathSetup } from '../base_path'; import { InjectedMetadataSetup } from '../injected_metadata'; import { FatalErrorsSetup } from '../fatal_errors'; +export interface HttpHeadersInit { + [name: string]: any; +} +export interface HttpRequestInit { + body?: BodyInit | null; + cache?: RequestCache; + credentials?: RequestCredentials; + headers?: HttpHeadersInit; + integrity?: string; + keepalive?: boolean; + method?: string; + mode?: RequestMode; + redirect?: RequestRedirect; + referrer?: string; + referrerPolicy?: ReferrerPolicy; + signal?: AbortSignal | null; + window?: any; +} export interface Deps { basePath: BasePathSetup; injectedMetadata: InjectedMetadataSetup; @@ -29,9 +47,10 @@ export interface Deps { export interface HttpFetchQuery { [key: string]: string | number | boolean | undefined; } -export interface HttpFetchOptions extends RequestInit { +export interface HttpFetchOptions extends HttpRequestInit { query?: HttpFetchQuery; prependBasePath?: boolean; + headers?: HttpHeadersInit; } export interface Abortable { abort: () => Promise; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/import_file.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/import_file.js index efff1db3e38f459..12630501663162c 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/import_file.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/import_file.js @@ -27,7 +27,8 @@ export async function importFile(file, overwriteAll = false) { pathname: '/api/saved_objects/_import', body: formData, headers: { - 'Content-Type': 'multipart/form-data', + // Important to be undefined, it forces proper headers to be set for FormData + 'Content-Type': undefined, }, query: { overwrite: overwriteAll diff --git a/src/legacy/ui/public/kfetch/kfetch.ts b/src/legacy/ui/public/kfetch/kfetch.ts index 43f733795905cc3..12a57ebd20ed8ed 100644 --- a/src/legacy/ui/public/kfetch/kfetch.ts +++ b/src/legacy/ui/public/kfetch/kfetch.ts @@ -22,12 +22,14 @@ import { merge } from 'lodash'; import { KFetchError } from './kfetch_error'; import { HttpSetup } from '../../../../core/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { HttpRequestInit } from '../../../../core/public/http/types'; export interface KFetchQuery { [key: string]: string | number | boolean | undefined; } -export interface KFetchOptions extends RequestInit { +export interface KFetchOptions extends HttpRequestInit { pathname: string; query?: KFetchQuery; } @@ -100,18 +102,28 @@ function responseInterceptors(responsePromise: Promise) { } export function withDefaultOptions(options?: KFetchOptions): KFetchOptions { - return merge( + const withDefaults = merge( { method: 'GET', credentials: 'same-origin', headers: { - ...(options && options.headers && options.headers.hasOwnProperty('Content-Type') - ? {} - : { - 'Content-Type': 'application/json', - }), + 'Content-Type': 'application/json', }, }, options - ); + ) as KFetchOptions; + + if ( + options && + options.headers && + 'Content-Type' in options.headers && + options.headers['Content-Type'] === undefined + ) { + // TS thinks headers could be undefined here, but that isn't possible because + // of the merge above. + // @ts-ignore + withDefaults.headers['Content-Type'] = undefined; + } + + return withDefaults; }