RequestClient.js 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. 'use strict'
  2. import fetchWithNetworkError from '@uppy/utils/lib/fetchWithNetworkError'
  3. import ErrorWithCause from '@uppy/utils/lib/ErrorWithCause'
  4. import AuthError from './AuthError.js'
  5. import packageJson from '../package.json'
  6. // Remove the trailing slash so we can always safely append /xyz.
  7. function stripSlash (url) {
  8. return url.replace(/\/$/, '')
  9. }
  10. async function handleJSONResponse (res) {
  11. if (res.status === 401) {
  12. throw new AuthError()
  13. }
  14. const jsonPromise = res.json()
  15. if (res.status < 200 || res.status > 300) {
  16. let errMsg = `Failed request with status: ${res.status}. ${res.statusText}`
  17. try {
  18. const errData = await jsonPromise
  19. errMsg = errData.message ? `${errMsg} message: ${errData.message}` : errMsg
  20. errMsg = errData.requestId ? `${errMsg} request-Id: ${errData.requestId}` : errMsg
  21. } finally {
  22. // eslint-disable-next-line no-unsafe-finally
  23. throw new Error(errMsg)
  24. }
  25. }
  26. return jsonPromise
  27. }
  28. export default class RequestClient {
  29. static VERSION = packageJson.version
  30. #getPostResponseFunc = skip => response => (skip ? response : this.onReceiveResponse(response))
  31. constructor (uppy, opts) {
  32. this.uppy = uppy
  33. this.opts = opts
  34. this.onReceiveResponse = this.onReceiveResponse.bind(this)
  35. this.allowedHeaders = ['accept', 'content-type', 'uppy-auth-token']
  36. this.preflightDone = false
  37. }
  38. get hostname () {
  39. const { companion } = this.uppy.getState()
  40. const host = this.opts.companionUrl
  41. return stripSlash(companion && companion[host] ? companion[host] : host)
  42. }
  43. static defaultHeaders = {
  44. Accept: 'application/json',
  45. 'Content-Type': 'application/json',
  46. 'Uppy-Versions': `@uppy/companion-client=${RequestClient.VERSION}`,
  47. }
  48. headers () {
  49. const userHeaders = this.opts.companionHeaders || {}
  50. return Promise.resolve({
  51. ...RequestClient.defaultHeaders,
  52. ...userHeaders,
  53. })
  54. }
  55. onReceiveResponse (response) {
  56. const state = this.uppy.getState()
  57. const companion = state.companion || {}
  58. const host = this.opts.companionUrl
  59. const { headers } = response
  60. // Store the self-identified domain name for the Companion instance we just hit.
  61. if (headers.has('i-am') && headers.get('i-am') !== companion[host]) {
  62. this.uppy.setState({
  63. companion: { ...companion, [host]: headers.get('i-am') },
  64. })
  65. }
  66. return response
  67. }
  68. #getUrl (url) {
  69. if (/^(https?:|)\/\//.test(url)) {
  70. return url
  71. }
  72. return `${this.hostname}/${url}`
  73. }
  74. #errorHandler (method, path) {
  75. return (err) => {
  76. if (!err?.isAuthError) {
  77. // eslint-disable-next-line no-param-reassign
  78. err = new ErrorWithCause(`Could not ${method} ${this.#getUrl(path)}`, { cause: err })
  79. }
  80. return Promise.reject(err)
  81. }
  82. }
  83. preflight (path) {
  84. if (this.preflightDone) {
  85. return Promise.resolve(this.allowedHeaders.slice())
  86. }
  87. return fetch(this.#getUrl(path), {
  88. method: 'OPTIONS',
  89. })
  90. .then((response) => {
  91. if (response.headers.has('access-control-allow-headers')) {
  92. this.allowedHeaders = response.headers.get('access-control-allow-headers')
  93. .split(',').map((headerName) => headerName.trim().toLowerCase())
  94. }
  95. this.preflightDone = true
  96. return this.allowedHeaders.slice()
  97. })
  98. .catch((err) => {
  99. this.uppy.log(`[CompanionClient] unable to make preflight request ${err}`, 'warning')
  100. this.preflightDone = true
  101. return this.allowedHeaders.slice()
  102. })
  103. }
  104. preflightAndHeaders (path) {
  105. return Promise.all([this.preflight(path), this.headers()])
  106. .then(([allowedHeaders, headers]) => {
  107. // filter to keep only allowed Headers
  108. Object.keys(headers).forEach((header) => {
  109. if (!allowedHeaders.includes(header.toLowerCase())) {
  110. this.uppy.log(`[CompanionClient] excluding disallowed header ${header}`)
  111. delete headers[header] // eslint-disable-line no-param-reassign
  112. }
  113. })
  114. return headers
  115. })
  116. }
  117. get (path, skipPostResponse) {
  118. const method = 'get'
  119. return this.preflightAndHeaders(path)
  120. .then((headers) => fetchWithNetworkError(this.#getUrl(path), {
  121. method,
  122. headers,
  123. credentials: this.opts.companionCookiesRule || 'same-origin',
  124. }))
  125. .then(this.#getPostResponseFunc(skipPostResponse))
  126. .then(handleJSONResponse)
  127. .catch(this.#errorHandler(method, path))
  128. }
  129. post (path, data, skipPostResponse) {
  130. const method = 'post'
  131. return this.preflightAndHeaders(path)
  132. .then((headers) => fetchWithNetworkError(this.#getUrl(path), {
  133. method,
  134. headers,
  135. credentials: this.opts.companionCookiesRule || 'same-origin',
  136. body: JSON.stringify(data),
  137. }))
  138. .then(this.#getPostResponseFunc(skipPostResponse))
  139. .then(handleJSONResponse)
  140. .catch(this.#errorHandler(method, path))
  141. }
  142. delete (path, data, skipPostResponse) {
  143. const method = 'delete'
  144. return this.preflightAndHeaders(path)
  145. .then((headers) => fetchWithNetworkError(`${this.hostname}/${path}`, {
  146. method,
  147. headers,
  148. credentials: this.opts.companionCookiesRule || 'same-origin',
  149. body: data ? JSON.stringify(data) : null,
  150. }))
  151. .then(this.#getPostResponseFunc(skipPostResponse))
  152. .then(handleJSONResponse)
  153. .catch(this.#errorHandler(method, path))
  154. }
  155. }