fetch.ts 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. import { omitBy } from 'lodash';
  2. import { deprecationWarning } from '@grafana/data';
  3. import { BackendSrvRequest } from '@grafana/runtime';
  4. export const parseInitFromOptions = (options: BackendSrvRequest): RequestInit => {
  5. const method = options.method;
  6. const headers = parseHeaders(options);
  7. const isAppJson = isContentTypeApplicationJson(headers);
  8. const body = parseBody(options, isAppJson);
  9. const credentials = parseCredentials(options);
  10. return {
  11. method,
  12. headers,
  13. body,
  14. credentials,
  15. };
  16. };
  17. interface HeaderParser {
  18. canParse: (options: BackendSrvRequest) => boolean;
  19. parse: (headers: Headers) => Headers;
  20. }
  21. const defaultHeaderParser: HeaderParser = {
  22. canParse: () => true,
  23. parse: (headers) => {
  24. const accept = headers.get('accept');
  25. if (accept) {
  26. return headers;
  27. }
  28. headers.set('accept', 'application/json, text/plain, */*');
  29. return headers;
  30. },
  31. };
  32. const parseHeaderByMethodFactory = (methodPredicate: string): HeaderParser => ({
  33. canParse: (options) => {
  34. const method = options?.method ? options?.method.toLowerCase() : '';
  35. return method === methodPredicate;
  36. },
  37. parse: (headers) => {
  38. const contentType = headers.get('content-type');
  39. if (contentType) {
  40. return headers;
  41. }
  42. headers.set('content-type', 'application/json');
  43. return headers;
  44. },
  45. });
  46. const postHeaderParser: HeaderParser = parseHeaderByMethodFactory('post');
  47. const putHeaderParser: HeaderParser = parseHeaderByMethodFactory('put');
  48. const patchHeaderParser: HeaderParser = parseHeaderByMethodFactory('patch');
  49. const headerParsers = [postHeaderParser, putHeaderParser, patchHeaderParser, defaultHeaderParser];
  50. export const parseHeaders = (options: BackendSrvRequest) => {
  51. const headers = options?.headers ? new Headers(options.headers) : new Headers();
  52. const parsers = headerParsers.filter((parser) => parser.canParse(options));
  53. const combinedHeaders = parsers.reduce((prev, parser) => {
  54. return parser.parse(prev);
  55. }, headers);
  56. return combinedHeaders;
  57. };
  58. export const isContentTypeApplicationJson = (headers: Headers) => {
  59. if (!headers) {
  60. return false;
  61. }
  62. const contentType = headers.get('content-type');
  63. if (contentType && contentType.toLowerCase() === 'application/json') {
  64. return true;
  65. }
  66. return false;
  67. };
  68. export const parseBody = (options: BackendSrvRequest, isAppJson: boolean) => {
  69. if (!options) {
  70. return options;
  71. }
  72. if (!options.data || typeof options.data === 'string') {
  73. return options.data;
  74. }
  75. return isAppJson ? JSON.stringify(options.data) : new URLSearchParams(options.data);
  76. };
  77. export async function parseResponseBody<T>(
  78. response: Response,
  79. responseType?: 'json' | 'text' | 'arraybuffer' | 'blob'
  80. ): Promise<T> {
  81. if (responseType) {
  82. switch (responseType) {
  83. case 'arraybuffer':
  84. return response.arrayBuffer() as any;
  85. case 'blob':
  86. return response.blob() as any;
  87. case 'json':
  88. // An empty string is not a valid JSON.
  89. // Sometimes (unfortunately) our APIs declare their Content-Type as JSON, however they return an empty body.
  90. if (response.headers.get('Content-Length') === '0') {
  91. console.warn(`${response.url} returned an invalid JSON`);
  92. return {} as unknown as T;
  93. }
  94. return await response.json();
  95. case 'text':
  96. return response.text() as any;
  97. }
  98. }
  99. const textData = await response.text(); // this could be just a string, prometheus requests for instance
  100. try {
  101. return JSON.parse(textData); // majority of the requests this will be something that can be parsed
  102. } catch {}
  103. return textData as any;
  104. }
  105. export function serializeParams(data: Record<string, any>): string {
  106. return Object.keys(data)
  107. .map((key) => {
  108. const value = data[key];
  109. if (Array.isArray(value)) {
  110. return value.map((arrayValue) => `${encodeURIComponent(key)}=${encodeURIComponent(arrayValue)}`).join('&');
  111. }
  112. return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
  113. })
  114. .join('&');
  115. }
  116. export const parseUrlFromOptions = (options: BackendSrvRequest): string => {
  117. const cleanParams = omitBy(options.params, (v) => v === undefined || (v && v.length === 0));
  118. const serializedParams = serializeParams(cleanParams);
  119. return options.params && serializedParams.length ? `${options.url}?${serializedParams}` : options.url;
  120. };
  121. export const parseCredentials = (options: BackendSrvRequest): RequestCredentials => {
  122. if (!options) {
  123. return options;
  124. }
  125. if (options.credentials) {
  126. return options.credentials;
  127. }
  128. if (options.withCredentials) {
  129. deprecationWarning('BackendSrvRequest', 'withCredentials', 'credentials');
  130. return 'include';
  131. }
  132. return 'same-origin';
  133. };