fetch-json.js 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  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. /**
  41. * @param {number} statusCode
  42. * @returns {string}
  43. */
  44. function getErrorMessageForStatusCode(statusCode) {
  45. switch (statusCode) {
  46. case 400:
  47. return 'Bad Request'
  48. case 401:
  49. return 'Unauthorized'
  50. case 403:
  51. return 'Forbidden'
  52. case 404:
  53. return 'Not Found'
  54. case 500:
  55. return 'Internal Server Error'
  56. case 502:
  57. return 'Bad Gateway'
  58. case 503:
  59. return 'Service Unavailable'
  60. default:
  61. return `Unexpected Error: ${statusCode}`
  62. }
  63. }
  64. export class FetchError extends OError {
  65. /**
  66. * @param {string} message
  67. * @param {string} url
  68. * @param {FetchOptions} [options]
  69. * @param {Response} [response]
  70. * @param {Object} [data]
  71. */
  72. constructor(message, url, options, response, data) {
  73. // On HTTP2, the `statusText` property is not set,
  74. // so this `message` will be undefined. We need to
  75. // set a message based on the response `status`, so
  76. // our error UI rendering will work
  77. if (!message) {
  78. message = getErrorMessageForStatusCode(response.status)
  79. }
  80. super(message, { statusCode: response ? response.status : undefined })
  81. this.url = url
  82. this.options = options
  83. this.response = response
  84. this.data = data
  85. }
  86. }
  87. /**
  88. * @param {string} path
  89. * @param {FetchOptions} [options]
  90. *
  91. * @return Promise<Object>
  92. */
  93. function fetchJSON(
  94. path,
  95. {
  96. body = {},
  97. headers = {},
  98. method = 'GET',
  99. credentials = 'same-origin',
  100. ...otherOptions
  101. }
  102. ) {
  103. const options = {
  104. ...otherOptions,
  105. headers: {
  106. ...headers,
  107. 'Content-Type': 'application/json',
  108. 'X-Csrf-Token': window.csrfToken,
  109. Accept: 'application/json',
  110. },
  111. credentials,
  112. method,
  113. }
  114. if (method !== 'GET' && method !== 'HEAD') {
  115. options.body = JSON.stringify(body)
  116. }
  117. // The returned Promise and the `.then(handleSuccess, handleError)` handlers are needed
  118. // to avoid calling `finally` in a Promise chain (and thus updating the component's state)
  119. // after a component has unmounted.
  120. // `resolve` will be called when the request succeeds, `reject` will be called when the request fails,
  121. // but nothing will be called if the request is cancelled via an AbortController.
  122. return new Promise((resolve, reject) => {
  123. fetch(path, options).then(
  124. response => {
  125. return parseResponseBody(response).then(
  126. data => {
  127. if (response.ok) {
  128. resolve(data)
  129. } else {
  130. // the response from the server was not 2xx
  131. reject(
  132. new FetchError(
  133. response.statusText,
  134. path,
  135. options,
  136. response,
  137. data
  138. )
  139. )
  140. }
  141. },
  142. error => {
  143. // parsing the response body failed
  144. reject(
  145. new FetchError(
  146. 'There was an error parsing the response body',
  147. path,
  148. options,
  149. response
  150. ).withCause(error)
  151. )
  152. }
  153. )
  154. },
  155. error => {
  156. // swallow the error if the fetch was cancelled (e.g. by cancelling an AbortController on component unmount)
  157. if (error.name !== 'AbortError') {
  158. // the fetch failed
  159. reject(
  160. new FetchError(
  161. 'There was an error fetching the JSON',
  162. path,
  163. options
  164. ).withCause(error)
  165. )
  166. }
  167. }
  168. )
  169. })
  170. }
  171. /**
  172. * @param {Response} response
  173. * @returns {Promise<Object>}
  174. */
  175. async function parseResponseBody(response) {
  176. const contentType = response.headers.get('Content-Type')
  177. if (/application\/json/.test(contentType)) {
  178. return response.json()
  179. }
  180. if (/text\/plain/.test(contentType)) {
  181. const message = await response.text()
  182. return { message }
  183. }
  184. if (/text\/html/.test(contentType)) {
  185. const message = await response.text()
  186. // only use HTML responses which don't start with `<`
  187. if (!/^\s*</.test(message)) {
  188. return { message }
  189. }
  190. }
  191. // response body ignored as content-type is either not set (e.g. 204
  192. // responses) or unsupported
  193. return {}
  194. }