fetch-json.js 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. // fetch wrapper to make simple JSON requests:
  2. // - send the CSRF token in the request
  3. // - set the JSON content-type in the request headers
  4. // - throw errors on non-ok response
  5. // - parse JSON response body, unless response is empty
  6. import OError from '@overleaf/o-error'
  7. /**
  8. * @typedef {Object} FetchOptions
  9. * @extends RequestInit
  10. * @property {Object} body
  11. */
  12. /**
  13. * @param {string} path
  14. * @param {FetchOptions} [options]
  15. */
  16. export function getJSON(path, options) {
  17. return fetchJSON(path, { ...options, method: 'GET' })
  18. }
  19. /**
  20. * @param {string} path
  21. * @param {FetchOptions} [options]
  22. */
  23. export function postJSON(path, options) {
  24. return fetchJSON(path, { ...options, method: 'POST' })
  25. }
  26. /**
  27. * @param {string} path
  28. * @param {FetchOptions} [options]
  29. */
  30. export function putJSON(path, options) {
  31. return fetchJSON(path, { ...options, method: 'PUT' })
  32. }
  33. /**
  34. * @param {string} path
  35. * @param {FetchOptions} [options]
  36. */
  37. export function deleteJSON(path, options) {
  38. return fetchJSON(path, { ...options, method: 'DELETE' })
  39. }
  40. export class FetchError extends OError {
  41. /**
  42. * @param {string} message
  43. * @param {string} url
  44. * @param {FetchOptions} [options]
  45. * @param {Response} [response]
  46. * @param {Object} [data]
  47. */
  48. constructor(message, url, options, response, data) {
  49. super(message, { statusCode: response ? response.status : undefined })
  50. this.url = url
  51. this.options = options
  52. this.response = response
  53. this.data = data
  54. }
  55. }
  56. /**
  57. * @param {string} path
  58. * @param {FetchOptions} [options]
  59. *
  60. * @return Promise<Object>
  61. */
  62. function fetchJSON(
  63. path,
  64. {
  65. body = {},
  66. headers = {},
  67. method = 'GET',
  68. credentials = 'same-origin',
  69. ...otherOptions
  70. }
  71. ) {
  72. const options = {
  73. ...otherOptions,
  74. headers: {
  75. ...headers,
  76. 'Content-Type': 'application/json',
  77. 'X-Csrf-Token': window.csrfToken,
  78. Accept: 'application/json',
  79. },
  80. credentials,
  81. method,
  82. }
  83. if (method !== 'GET' && method !== 'HEAD') {
  84. options.body = JSON.stringify(body)
  85. }
  86. return fetch(path, options)
  87. .catch(error => {
  88. // the fetch failed
  89. throw new FetchError(
  90. 'There was an error fetching the JSON',
  91. path,
  92. options
  93. ).withCause(error)
  94. })
  95. .then(response => {
  96. return parseResponseBody(response)
  97. .catch(error => {
  98. // parsing the response body failed
  99. throw new FetchError(
  100. 'There was an error parsing the response body',
  101. path,
  102. options,
  103. response
  104. ).withCause(error)
  105. })
  106. .then(data => {
  107. if (!response.ok) {
  108. // the response from the server was not 2xx
  109. throw new FetchError(
  110. response.statusText,
  111. path,
  112. options,
  113. response,
  114. data
  115. )
  116. }
  117. return data
  118. })
  119. })
  120. }
  121. /**
  122. * @param {Response} response
  123. * @returns {Promise<Object>}
  124. */
  125. function parseResponseBody(response) {
  126. const contentType = response.headers.get('Content-Type')
  127. if (/application\/json/.test(contentType)) {
  128. return response.json()
  129. } else if (
  130. /text\/plain/.test(contentType) ||
  131. /text\/html/.test(contentType)
  132. ) {
  133. return response.text().then(message => ({ message }))
  134. } else {
  135. // response body ignored as content-type is either not set (e.g. 204
  136. // responses) or unsupported
  137. return Promise.resolve({})
  138. }
  139. }