mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
BackendSrv: only add content-type on POST, PUT requests (#22910)
* BackendSrv: only add content-type on POST, PUT requests Fixes #22869 * Tests: imports polyfill for Headers
This commit is contained in:
parent
87ffa258e7
commit
8d5c6053db
@ -1,4 +1,3 @@
|
|||||||
import omitBy from 'lodash/omitBy';
|
|
||||||
import { from, merge, MonoTypeOperatorFunction, Observable, of, Subject, throwError } from 'rxjs';
|
import { from, merge, MonoTypeOperatorFunction, Observable, of, Subject, throwError } from 'rxjs';
|
||||||
import { catchError, filter, map, mergeMap, retryWhen, share, takeUntil, tap, throwIfEmpty } from 'rxjs/operators';
|
import { catchError, filter, map, mergeMap, retryWhen, share, takeUntil, tap, throwIfEmpty } from 'rxjs/operators';
|
||||||
import { fromFetch } from 'rxjs/fetch';
|
import { fromFetch } from 'rxjs/fetch';
|
||||||
@ -14,6 +13,7 @@ import { ContextSrv, contextSrv } from './context_srv';
|
|||||||
import { coreModule } from 'app/core/core_module';
|
import { coreModule } from 'app/core/core_module';
|
||||||
import { Emitter } from '../utils/emitter';
|
import { Emitter } from '../utils/emitter';
|
||||||
import { DataSourceResponse } from '../../types/events';
|
import { DataSourceResponse } from '../../types/events';
|
||||||
|
import { parseInitFromOptions, parseUrlFromOptions } from '../utils/fetch';
|
||||||
|
|
||||||
export interface DatasourceRequestOptions {
|
export interface DatasourceRequestOptions {
|
||||||
retry?: number;
|
retry?: number;
|
||||||
@ -54,18 +54,6 @@ enum CancellationType {
|
|||||||
dataSourceRequest,
|
dataSourceRequest,
|
||||||
}
|
}
|
||||||
|
|
||||||
function serializeParams(data: Record<string, any>): string {
|
|
||||||
return Object.keys(data)
|
|
||||||
.map(key => {
|
|
||||||
const value = data[key];
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
return value.map(arrayValue => `${encodeURIComponent(key)}=${encodeURIComponent(arrayValue)}`).join('&');
|
|
||||||
}
|
|
||||||
return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
|
|
||||||
})
|
|
||||||
.join('&');
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BackendSrvDependencies {
|
export interface BackendSrvDependencies {
|
||||||
fromFetch: (input: string | Request, init?: RequestInit) => Observable<Response>;
|
fromFetch: (input: string | Request, init?: RequestInit) => Observable<Response>;
|
||||||
appEvents: Emitter;
|
appEvents: Emitter;
|
||||||
@ -580,62 +568,3 @@ coreModule.factory('backendSrv', () => backendSrv);
|
|||||||
// Used for testing and things that really need BackendSrv
|
// Used for testing and things that really need BackendSrv
|
||||||
export const backendSrv = new BackendSrv();
|
export const backendSrv = new BackendSrv();
|
||||||
export const getBackendSrv = (): BackendSrv => backendSrv;
|
export const getBackendSrv = (): BackendSrv => backendSrv;
|
||||||
|
|
||||||
export const parseUrlFromOptions = (options: BackendSrvRequest): string => {
|
|
||||||
const cleanParams = omitBy(options.params, v => v === undefined || (v && v.length === 0));
|
|
||||||
const serializedParams = serializeParams(cleanParams);
|
|
||||||
return options.params && serializedParams.length ? `${options.url}?${serializedParams}` : options.url;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const parseInitFromOptions = (options: BackendSrvRequest): RequestInit => {
|
|
||||||
const method = options.method;
|
|
||||||
const headers = parseHeaders(options);
|
|
||||||
const isAppJson = isContentTypeApplicationJson(headers);
|
|
||||||
const body = parseBody(options, isAppJson);
|
|
||||||
|
|
||||||
return {
|
|
||||||
method,
|
|
||||||
headers,
|
|
||||||
body,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const parseHeaders = (options: BackendSrvRequest) => {
|
|
||||||
const headers = new Headers({
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
Accept: 'application/json, text/plain, */*',
|
|
||||||
});
|
|
||||||
|
|
||||||
if (options && options.headers) {
|
|
||||||
Object.keys(options.headers).forEach(key => {
|
|
||||||
headers.set(key, options.headers[key]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return headers;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const isContentTypeApplicationJson = (headers: Headers) => {
|
|
||||||
if (!headers) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const contentType = headers.get('content-type');
|
|
||||||
if (contentType && contentType.toLowerCase() === 'application/json') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const parseBody = (options: BackendSrvRequest, isAppJson: boolean) => {
|
|
||||||
if (!options) {
|
|
||||||
return options;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!options.data || typeof options.data === 'string') {
|
|
||||||
return options.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
return isAppJson ? JSON.stringify(options.data) : new URLSearchParams(options.data);
|
|
||||||
};
|
|
||||||
|
@ -3,15 +3,7 @@ import { Observable, of } from 'rxjs';
|
|||||||
import { delay } from 'rxjs/operators';
|
import { delay } from 'rxjs/operators';
|
||||||
import { AppEvents } from '@grafana/data';
|
import { AppEvents } from '@grafana/data';
|
||||||
|
|
||||||
import {
|
import { BackendSrv, getBackendSrv } from '../services/backend_srv';
|
||||||
BackendSrv,
|
|
||||||
getBackendSrv,
|
|
||||||
isContentTypeApplicationJson,
|
|
||||||
parseBody,
|
|
||||||
parseHeaders,
|
|
||||||
parseInitFromOptions,
|
|
||||||
parseUrlFromOptions,
|
|
||||||
} from '../services/backend_srv';
|
|
||||||
import { Emitter } from '../utils/emitter';
|
import { Emitter } from '../utils/emitter';
|
||||||
import { ContextSrv, User } from '../services/context_srv';
|
import { ContextSrv, User } from '../services/context_srv';
|
||||||
import { CoreEvents } from '../../types';
|
import { CoreEvents } from '../../types';
|
||||||
@ -27,7 +19,6 @@ const getTestContext = (overides?: object) => {
|
|||||||
redirected: false,
|
redirected: false,
|
||||||
type: 'basic',
|
type: 'basic',
|
||||||
url: 'http://localhost:3000/api/some-mock',
|
url: 'http://localhost:3000/api/some-mock',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
};
|
};
|
||||||
const props = { ...defaults, ...overides };
|
const props = { ...defaults, ...overides };
|
||||||
const textMock = jest.fn().mockResolvedValue(JSON.stringify(props.data));
|
const textMock = jest.fn().mockResolvedValue(JSON.stringify(props.data));
|
||||||
@ -40,7 +31,6 @@ const getTestContext = (overides?: object) => {
|
|||||||
redirected: false,
|
redirected: false,
|
||||||
type: 'basic',
|
type: 'basic',
|
||||||
url: 'http://localhost:3000/api/some-mock',
|
url: 'http://localhost:3000/api/some-mock',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
};
|
};
|
||||||
return of(mockedResponse);
|
return of(mockedResponse);
|
||||||
});
|
});
|
||||||
@ -358,9 +348,6 @@ describe('backendSrv', () => {
|
|||||||
const result = await backendSrv.datasourceRequest({ url, method: 'GET', silent: true });
|
const result = await backendSrv.datasourceRequest({ url, method: 'GET', silent: true });
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
data: { test: 'hello world' },
|
data: { test: 'hello world' },
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
ok: true,
|
ok: true,
|
||||||
redirected: false,
|
redirected: false,
|
||||||
status: 200,
|
status: 200,
|
||||||
@ -373,7 +360,6 @@ describe('backendSrv', () => {
|
|||||||
body: undefined,
|
body: undefined,
|
||||||
headers: {
|
headers: {
|
||||||
map: {
|
map: {
|
||||||
'content-type': 'application/json',
|
|
||||||
accept: 'application/json, text/plain, */*',
|
accept: 'application/json, text/plain, */*',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -391,9 +377,6 @@ describe('backendSrv', () => {
|
|||||||
const result = await backendSrv.datasourceRequest({ url, method: 'GET' });
|
const result = await backendSrv.datasourceRequest({ url, method: 'GET' });
|
||||||
const expectedResult = {
|
const expectedResult = {
|
||||||
data: { test: 'hello world' },
|
data: { test: 'hello world' },
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
ok: true,
|
ok: true,
|
||||||
redirected: false,
|
redirected: false,
|
||||||
status: 200,
|
status: 200,
|
||||||
@ -406,7 +389,6 @@ describe('backendSrv', () => {
|
|||||||
body: undefined as any,
|
body: undefined as any,
|
||||||
headers: {
|
headers: {
|
||||||
map: {
|
map: {
|
||||||
'content-type': 'application/json',
|
|
||||||
accept: 'application/json, text/plain, */*',
|
accept: 'application/json, text/plain, */*',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -432,11 +414,6 @@ describe('backendSrv', () => {
|
|||||||
status: 200,
|
status: 200,
|
||||||
statusText: 'Ok',
|
statusText: 'Ok',
|
||||||
text: () => Promise.resolve(JSON.stringify(slowData)),
|
text: () => Promise.resolve(JSON.stringify(slowData)),
|
||||||
headers: {
|
|
||||||
map: {
|
|
||||||
'content-type': 'application/json',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
redirected: false,
|
redirected: false,
|
||||||
type: 'basic',
|
type: 'basic',
|
||||||
url,
|
url,
|
||||||
@ -449,11 +426,6 @@ describe('backendSrv', () => {
|
|||||||
status: 200,
|
status: 200,
|
||||||
statusText: 'Ok',
|
statusText: 'Ok',
|
||||||
text: () => Promise.resolve(JSON.stringify(fastData)),
|
text: () => Promise.resolve(JSON.stringify(fastData)),
|
||||||
headers: {
|
|
||||||
map: {
|
|
||||||
'content-type': 'application/json',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
redirected: false,
|
redirected: false,
|
||||||
type: 'basic',
|
type: 'basic',
|
||||||
url,
|
url,
|
||||||
@ -469,11 +441,6 @@ describe('backendSrv', () => {
|
|||||||
const fastResponse = await backendSrv.datasourceRequest(options);
|
const fastResponse = await backendSrv.datasourceRequest(options);
|
||||||
expect(fastResponse).toEqual({
|
expect(fastResponse).toEqual({
|
||||||
data: { message: 'Fast Request' },
|
data: { message: 'Fast Request' },
|
||||||
headers: {
|
|
||||||
map: {
|
|
||||||
'content-type': 'application/json',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
ok: true,
|
ok: true,
|
||||||
redirected: false,
|
redirected: false,
|
||||||
status: 200,
|
status: 200,
|
||||||
@ -486,7 +453,6 @@ describe('backendSrv', () => {
|
|||||||
body: undefined,
|
body: undefined,
|
||||||
headers: {
|
headers: {
|
||||||
map: {
|
map: {
|
||||||
'content-type': 'application/json',
|
|
||||||
accept: 'application/json, text/plain, */*',
|
accept: 'application/json, text/plain, */*',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -504,7 +470,6 @@ describe('backendSrv', () => {
|
|||||||
body: undefined,
|
body: undefined,
|
||||||
headers: {
|
headers: {
|
||||||
map: {
|
map: {
|
||||||
'content-type': 'application/json',
|
|
||||||
accept: 'application/json, text/plain, */*',
|
accept: 'application/json, text/plain, */*',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -641,86 +606,3 @@ describe('backendSrv', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('parseUrlFromOptions', () => {
|
|
||||||
it.each`
|
|
||||||
params | url | expected
|
|
||||||
${undefined} | ${'api/dashboard'} | ${'api/dashboard'}
|
|
||||||
${{ key: 'value' }} | ${'api/dashboard'} | ${'api/dashboard?key=value'}
|
|
||||||
${{ key: undefined }} | ${'api/dashboard'} | ${'api/dashboard'}
|
|
||||||
${{ firstKey: 'first value', secondValue: 'second value' }} | ${'api/dashboard'} | ${'api/dashboard?firstKey=first%20value&secondValue=second%20value'}
|
|
||||||
${{ firstKey: 'first value', secondValue: undefined }} | ${'api/dashboard'} | ${'api/dashboard?firstKey=first%20value'}
|
|
||||||
${{ id: [1, 2, 3] }} | ${'api/dashboard'} | ${'api/dashboard?id=1&id=2&id=3'}
|
|
||||||
${{ id: [] }} | ${'api/dashboard'} | ${'api/dashboard'}
|
|
||||||
`(
|
|
||||||
"when called with params: '$params' and url: '$url' then result should be '$expected'",
|
|
||||||
({ params, url, expected }) => {
|
|
||||||
expect(parseUrlFromOptions({ params, url })).toEqual(expected);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('parseInitFromOptions', () => {
|
|
||||||
it.each`
|
|
||||||
method | expected
|
|
||||||
${undefined} | ${{ method: undefined, headers: { map: { 'content-type': 'application/json', accept: 'application/json, text/plain, */*' } }, body: '{"id":"0"}' }}
|
|
||||||
${'GET'} | ${{ method: 'GET', headers: { map: { 'content-type': 'application/json', accept: 'application/json, text/plain, */*' } }, body: '{"id":"0"}' }}
|
|
||||||
${'POST'} | ${{ method: 'POST', headers: { map: { 'content-type': 'application/json', accept: 'application/json, text/plain, */*' } }, body: '{"id":"0"}' }}
|
|
||||||
${'monkey'} | ${{ method: 'monkey', headers: { map: { 'content-type': 'application/json', accept: 'application/json, text/plain, */*' } }, body: '{"id":"0"}' }}
|
|
||||||
`(
|
|
||||||
"when called with method: '$method', headers: '$headers' and data: '$data' then result should be '$expected'",
|
|
||||||
({ method, expected }) => {
|
|
||||||
expect(parseInitFromOptions({ method, data: { id: '0' }, url: '' })).toEqual(expected);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('parseHeaders', () => {
|
|
||||||
it.each`
|
|
||||||
options | expected
|
|
||||||
${undefined} | ${{ map: { accept: 'application/json, text/plain, */*', 'content-type': 'application/json' } }}
|
|
||||||
${{ propKey: 'some prop value' }} | ${{ map: { accept: 'application/json, text/plain, */*', 'content-type': 'application/json' } }}
|
|
||||||
${{ headers: { 'content-type': 'application/json' } }} | ${{ map: { accept: 'application/json, text/plain, */*', 'content-type': 'application/json' } }}
|
|
||||||
${{ headers: { 'cOnTent-tYpe': 'application/json' } }} | ${{ map: { accept: 'application/json, text/plain, */*', 'content-type': 'application/json' } }}
|
|
||||||
${{ headers: { 'content-type': 'AppLiCatIon/JsOn' } }} | ${{ map: { accept: 'application/json, text/plain, */*', 'content-type': 'AppLiCatIon/JsOn' } }}
|
|
||||||
${{ headers: { 'cOnTent-tYpe': 'AppLiCatIon/JsOn' } }} | ${{ map: { accept: 'application/json, text/plain, */*', 'content-type': 'AppLiCatIon/JsOn' } }}
|
|
||||||
${{ headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }} | ${{ map: { accept: 'application/json, text/plain, */*', 'content-type': 'application/x-www-form-urlencoded' } }}
|
|
||||||
${{ headers: { Accept: 'text/plain' } }} | ${{ map: { accept: 'text/plain', 'content-type': 'application/json' } }}
|
|
||||||
${{ headers: { Auth: 'Basic asdasdasd' } }} | ${{ map: { accept: 'application/json, text/plain, */*', 'content-type': 'application/json', auth: 'Basic asdasdasd' } }}
|
|
||||||
`("when called with options: '$options' then the result should be '$expected'", ({ options, expected }) => {
|
|
||||||
expect(parseHeaders(options)).toEqual(expected);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('isContentTypeApplicationJson', () => {
|
|
||||||
it.each`
|
|
||||||
headers | expected
|
|
||||||
${undefined} | ${false}
|
|
||||||
${new Headers({ 'cOnTent-tYpe': 'application/json' })} | ${true}
|
|
||||||
${new Headers({ 'content-type': 'AppLiCatIon/JsOn' })} | ${true}
|
|
||||||
${new Headers({ 'cOnTent-tYpe': 'AppLiCatIon/JsOn' })} | ${true}
|
|
||||||
${new Headers({ 'content-type': 'application/x-www-form-urlencoded' })} | ${false}
|
|
||||||
${new Headers({ auth: 'Basic akdjasdkjalksdjasd' })} | ${false}
|
|
||||||
`("when called with headers: 'headers' then the result should be '$expected'", ({ headers, expected }) => {
|
|
||||||
expect(isContentTypeApplicationJson(headers)).toEqual(expected);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('parseBody', () => {
|
|
||||||
it.each`
|
|
||||||
options | isAppJson | expected
|
|
||||||
${undefined} | ${false} | ${undefined}
|
|
||||||
${undefined} | ${true} | ${undefined}
|
|
||||||
${{ data: undefined }} | ${false} | ${undefined}
|
|
||||||
${{ data: undefined }} | ${true} | ${undefined}
|
|
||||||
${{ data: 'some data' }} | ${false} | ${'some data'}
|
|
||||||
${{ data: 'some data' }} | ${true} | ${'some data'}
|
|
||||||
${{ data: { id: '0' } }} | ${false} | ${new URLSearchParams({ id: '0' })}
|
|
||||||
${{ data: { id: '0' } }} | ${true} | ${'{"id":"0"}'}
|
|
||||||
`(
|
|
||||||
"when called with options: '$options' and isAppJson: '$isAppJson' then the result should be '$expected'",
|
|
||||||
({ options, isAppJson, expected }) => {
|
|
||||||
expect(parseBody(options, isAppJson)).toEqual(expected);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
101
public/app/core/utils/fetch.test.ts
Normal file
101
public/app/core/utils/fetch.test.ts
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import 'whatwg-fetch'; // fetch polyfill needed for PhantomJs rendering
|
||||||
|
import {
|
||||||
|
isContentTypeApplicationJson,
|
||||||
|
parseBody,
|
||||||
|
parseHeaders,
|
||||||
|
parseInitFromOptions,
|
||||||
|
parseUrlFromOptions,
|
||||||
|
} from './fetch';
|
||||||
|
|
||||||
|
describe('parseUrlFromOptions', () => {
|
||||||
|
it.each`
|
||||||
|
params | url | expected
|
||||||
|
${undefined} | ${'api/dashboard'} | ${'api/dashboard'}
|
||||||
|
${{ key: 'value' }} | ${'api/dashboard'} | ${'api/dashboard?key=value'}
|
||||||
|
${{ key: undefined }} | ${'api/dashboard'} | ${'api/dashboard'}
|
||||||
|
${{ firstKey: 'first value', secondValue: 'second value' }} | ${'api/dashboard'} | ${'api/dashboard?firstKey=first%20value&secondValue=second%20value'}
|
||||||
|
${{ firstKey: 'first value', secondValue: undefined }} | ${'api/dashboard'} | ${'api/dashboard?firstKey=first%20value'}
|
||||||
|
${{ id: [1, 2, 3] }} | ${'api/dashboard'} | ${'api/dashboard?id=1&id=2&id=3'}
|
||||||
|
${{ id: [] }} | ${'api/dashboard'} | ${'api/dashboard'}
|
||||||
|
`(
|
||||||
|
"when called with params: '$params' and url: '$url' then result should be '$expected'",
|
||||||
|
({ params, url, expected }) => {
|
||||||
|
expect(parseUrlFromOptions({ params, url })).toEqual(expected);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('parseInitFromOptions', () => {
|
||||||
|
it.each`
|
||||||
|
method | data | expected
|
||||||
|
${undefined} | ${undefined} | ${{ method: undefined, headers: { map: { accept: 'application/json, text/plain, */*' } }, body: undefined }}
|
||||||
|
${'GET'} | ${undefined} | ${{ method: 'GET', headers: { map: { accept: 'application/json, text/plain, */*' } }, body: undefined }}
|
||||||
|
${'POST'} | ${{ id: '0' }} | ${{ method: 'POST', headers: { map: { 'content-type': 'application/json', accept: 'application/json, text/plain, */*' } }, body: '{"id":"0"}' }}
|
||||||
|
${'PUT'} | ${{ id: '0' }} | ${{ method: 'PUT', headers: { map: { 'content-type': 'application/json', accept: 'application/json, text/plain, */*' } }, body: '{"id":"0"}' }}
|
||||||
|
${'monkey'} | ${undefined} | ${{ method: 'monkey', headers: { map: { accept: 'application/json, text/plain, */*' } }, body: undefined }}
|
||||||
|
`(
|
||||||
|
"when called with method: '$method' and data: '$data' then result should be '$expected'",
|
||||||
|
({ method, data, expected }) => {
|
||||||
|
expect(parseInitFromOptions({ method, data, url: '' })).toEqual(expected);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('parseHeaders', () => {
|
||||||
|
it.each`
|
||||||
|
options | expected
|
||||||
|
${undefined} | ${{ map: { accept: 'application/json, text/plain, */*' } }}
|
||||||
|
${{ propKey: 'some prop value' }} | ${{ map: { accept: 'application/json, text/plain, */*' } }}
|
||||||
|
${{ method: 'GET' }} | ${{ map: { accept: 'application/json, text/plain, */*' } }}
|
||||||
|
${{ method: 'POST' }} | ${{ map: { accept: 'application/json, text/plain, */*', 'content-type': 'application/json' } }}
|
||||||
|
${{ method: 'PUT' }} | ${{ map: { accept: 'application/json, text/plain, */*', 'content-type': 'application/json' } }}
|
||||||
|
${{ headers: { 'content-type': 'application/json' } }} | ${{ map: { accept: 'application/json, text/plain, */*', 'content-type': 'application/json' } }}
|
||||||
|
${{ method: 'GET', headers: { 'content-type': 'application/json' } }} | ${{ map: { accept: 'application/json, text/plain, */*', 'content-type': 'application/json' } }}
|
||||||
|
${{ method: 'POST', headers: { 'content-type': 'application/json' } }} | ${{ map: { accept: 'application/json, text/plain, */*', 'content-type': 'application/json' } }}
|
||||||
|
${{ method: 'PUT', headers: { 'content-type': 'application/json' } }} | ${{ map: { accept: 'application/json, text/plain, */*', 'content-type': 'application/json' } }}
|
||||||
|
${{ headers: { 'cOnTent-tYpe': 'application/json' } }} | ${{ map: { accept: 'application/json, text/plain, */*', 'content-type': 'application/json' } }}
|
||||||
|
${{ headers: { 'content-type': 'AppLiCatIon/JsOn' } }} | ${{ map: { accept: 'application/json, text/plain, */*', 'content-type': 'AppLiCatIon/JsOn' } }}
|
||||||
|
${{ headers: { 'cOnTent-tYpe': 'AppLiCatIon/JsOn' } }} | ${{ map: { accept: 'application/json, text/plain, */*', 'content-type': 'AppLiCatIon/JsOn' } }}
|
||||||
|
${{ headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }} | ${{ map: { accept: 'application/json, text/plain, */*', 'content-type': 'application/x-www-form-urlencoded' } }}
|
||||||
|
${{ method: 'GET', headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }} | ${{ map: { accept: 'application/json, text/plain, */*', 'content-type': 'application/x-www-form-urlencoded' } }}
|
||||||
|
${{ method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }} | ${{ map: { accept: 'application/json, text/plain, */*', 'content-type': 'application/x-www-form-urlencoded' } }}
|
||||||
|
${{ method: 'PUT', headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }} | ${{ map: { accept: 'application/json, text/plain, */*', 'content-type': 'application/x-www-form-urlencoded' } }}
|
||||||
|
${{ headers: { Accept: 'text/plain' } }} | ${{ map: { accept: 'text/plain' } }}
|
||||||
|
${{ headers: { Auth: 'Basic asdasdasd' } }} | ${{ map: { accept: 'application/json, text/plain, */*', auth: 'Basic asdasdasd' } }}
|
||||||
|
`("when called with options: '$options' then the result should be '$expected'", ({ options, expected }) => {
|
||||||
|
expect(parseHeaders(options)).toEqual(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isContentTypeApplicationJson', () => {
|
||||||
|
it.each`
|
||||||
|
headers | expected
|
||||||
|
${undefined} | ${false}
|
||||||
|
${new Headers({ 'cOnTent-tYpe': 'application/json' })} | ${true}
|
||||||
|
${new Headers({ 'content-type': 'AppLiCatIon/JsOn' })} | ${true}
|
||||||
|
${new Headers({ 'cOnTent-tYpe': 'AppLiCatIon/JsOn' })} | ${true}
|
||||||
|
${new Headers({ 'content-type': 'application/x-www-form-urlencoded' })} | ${false}
|
||||||
|
${new Headers({ auth: 'Basic akdjasdkjalksdjasd' })} | ${false}
|
||||||
|
`("when called with headers: 'headers' then the result should be '$expected'", ({ headers, expected }) => {
|
||||||
|
expect(isContentTypeApplicationJson(headers)).toEqual(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('parseBody', () => {
|
||||||
|
it.each`
|
||||||
|
options | isAppJson | expected
|
||||||
|
${undefined} | ${false} | ${undefined}
|
||||||
|
${undefined} | ${true} | ${undefined}
|
||||||
|
${{ data: undefined }} | ${false} | ${undefined}
|
||||||
|
${{ data: undefined }} | ${true} | ${undefined}
|
||||||
|
${{ data: 'some data' }} | ${false} | ${'some data'}
|
||||||
|
${{ data: 'some data' }} | ${true} | ${'some data'}
|
||||||
|
${{ data: { id: '0' } }} | ${false} | ${new URLSearchParams({ id: '0' })}
|
||||||
|
${{ data: { id: '0' } }} | ${true} | ${'{"id":"0"}'}
|
||||||
|
`(
|
||||||
|
"when called with options: '$options' and isAppJson: '$isAppJson' then the result should be '$expected'",
|
||||||
|
({ options, isAppJson, expected }) => {
|
||||||
|
expect(parseBody(options, isAppJson)).toEqual(expected);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
107
public/app/core/utils/fetch.ts
Normal file
107
public/app/core/utils/fetch.ts
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import { BackendSrvRequest } from '@grafana/runtime';
|
||||||
|
import omitBy from 'lodash/omitBy';
|
||||||
|
|
||||||
|
export const parseInitFromOptions = (options: BackendSrvRequest): RequestInit => {
|
||||||
|
const method = options.method;
|
||||||
|
const headers = parseHeaders(options);
|
||||||
|
const isAppJson = isContentTypeApplicationJson(headers);
|
||||||
|
const body = parseBody(options, isAppJson);
|
||||||
|
|
||||||
|
return {
|
||||||
|
method,
|
||||||
|
headers,
|
||||||
|
body,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
interface HeaderParser {
|
||||||
|
canParse: (options: BackendSrvRequest) => boolean;
|
||||||
|
parse: (headers: Headers) => Headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultHeaderParser: HeaderParser = {
|
||||||
|
canParse: () => true,
|
||||||
|
parse: headers => {
|
||||||
|
const accept = headers.get('accept');
|
||||||
|
if (accept) {
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
headers.set('accept', 'application/json, text/plain, */*');
|
||||||
|
return headers;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseHeaderByMethodFactory = (methodPredicate: string): HeaderParser => ({
|
||||||
|
canParse: options => {
|
||||||
|
const method = options?.method ? options?.method.toLowerCase() : '';
|
||||||
|
return method === methodPredicate;
|
||||||
|
},
|
||||||
|
parse: headers => {
|
||||||
|
const contentType = headers.get('content-type');
|
||||||
|
if (contentType) {
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
headers.set('content-type', 'application/json');
|
||||||
|
return headers;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const postHeaderParser: HeaderParser = parseHeaderByMethodFactory('post');
|
||||||
|
const putHeaderParser: HeaderParser = parseHeaderByMethodFactory('put');
|
||||||
|
|
||||||
|
const headerParsers = [postHeaderParser, putHeaderParser, defaultHeaderParser];
|
||||||
|
|
||||||
|
export const parseHeaders = (options: BackendSrvRequest) => {
|
||||||
|
const headers = options?.headers ? new Headers(options.headers) : new Headers();
|
||||||
|
const parsers = headerParsers.filter(parser => parser.canParse(options));
|
||||||
|
const combinedHeaders = parsers.reduce((prev, parser) => {
|
||||||
|
return parser.parse(prev);
|
||||||
|
}, headers);
|
||||||
|
|
||||||
|
return combinedHeaders;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isContentTypeApplicationJson = (headers: Headers) => {
|
||||||
|
if (!headers) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentType = headers.get('content-type');
|
||||||
|
if (contentType && contentType.toLowerCase() === 'application/json') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const parseBody = (options: BackendSrvRequest, isAppJson: boolean) => {
|
||||||
|
if (!options) {
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!options.data || typeof options.data === 'string') {
|
||||||
|
return options.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return isAppJson ? JSON.stringify(options.data) : new URLSearchParams(options.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
function serializeParams(data: Record<string, any>): string {
|
||||||
|
return Object.keys(data)
|
||||||
|
.map(key => {
|
||||||
|
const value = data[key];
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.map(arrayValue => `${encodeURIComponent(key)}=${encodeURIComponent(arrayValue)}`).join('&');
|
||||||
|
}
|
||||||
|
return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
|
||||||
|
})
|
||||||
|
.join('&');
|
||||||
|
}
|
||||||
|
|
||||||
|
export const parseUrlFromOptions = (options: BackendSrvRequest): string => {
|
||||||
|
const cleanParams = omitBy(options.params, v => v === undefined || (v && v.length === 0));
|
||||||
|
const serializedParams = serializeParams(cleanParams);
|
||||||
|
return options.params && serializedParams.length ? `${options.url}?${serializedParams}` : options.url;
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user