fetch-json.js 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  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. // The returned Promise and the `.then(handleSuccess, handleError)` handlers are needed
  87. // to avoid calling `finally` in a Promise chain (and thus updating the component's state)
  88. // after a component has unmounted.
  89. // `resolve` will be called when the request succeeds, `reject` will be called when the request fails,
  90. // but nothing will be called if the request is cancelled via an AbortController.
  91. return new Promise((resolve, reject) => {
  92. fetch(path, options).then(
  93. response => {
  94. return parseResponseBody(response).then(
  95. data => {
  96. if (response.ok) {
  97. resolve(data)
  98. } else {
  99. // the response from the server was not 2xx
  100. reject(
  101. new FetchError(
  102. response.statusText,
  103. path,
  104. options,
  105. response,
  106. data
  107. )
  108. )
  109. }
  110. },
  111. error => {
  112. // parsing the response body failed
  113. reject(
  114. new FetchError(
  115. 'There was an error parsing the response body',
  116. path,
  117. options,
  118. response
  119. ).withCause(error)
  120. )
  121. }
  122. )
  123. },
  124. error => {
  125. // swallow the error if the fetch was cancelled (e.g. by cancelling an AbortController on component unmount)
  126. if (error.name !== 'AbortError') {
  127. // the fetch failed
  128. reject(
  129. new FetchError(
  130. 'There was an error fetching the JSON',
  131. path,
  132. options
  133. ).withCause(error)
  134. )
  135. }
  136. }
  137. )
  138. })
  139. }
  140. /**
  141. * @param {Response} response
  142. * @returns {Promise<Object>}
  143. */
  144. async function parseResponseBody(response) {
  145. const contentType = response.headers.get('Content-Type')
  146. if (/application\/json/.test(contentType)) {
  147. return response.json()
  148. }
  149. if (/text\/plain/.test(contentType)) {
  150. const message = await response.text()
  151. return { message }
  152. }
  153. if (/text\/html/.test(contentType)) {
  154. const message = await response.text()
  155. // only use HTML responses which don't start with `<`
  156. if (!/^\s*</.test(message)) {
  157. return { message }
  158. }
  159. }
  160. // response body ignored as content-type is either not set (e.g. 204
  161. // responses) or unsupported
  162. return {}
  163. }