@uppy/companion-client
Advanced tools
Comparing version 3.6.1 to 3.7.0
# @uppy/companion-client | ||
## 3.7.0 | ||
Released: 2023-12-12 | ||
Included in: Uppy v3.21.0 | ||
- @uppy/companion-client: avoid unnecessary preflight requests (Antoine du Hamel / #4462) | ||
## 3.6.1 | ||
@@ -4,0 +11,0 @@ |
@@ -7,2 +7,5 @@ 'use strict'; | ||
this.name = 'AuthError'; | ||
// we use a property because of instanceof is unsafe: | ||
// https://github.com/transloadit/uppy/pull/4619#discussion_r1406225982 | ||
this.isAuthError = true; | ||
@@ -9,0 +12,0 @@ } |
@@ -6,3 +6,3 @@ 'use strict'; | ||
function _classPrivateFieldLooseKey(name) { return "__private_" + id++ + "_" + name; } | ||
import RequestClient from './RequestClient.js'; | ||
import RequestClient, { authErrorStatusCode } from './RequestClient.js'; | ||
import * as tokenStorage from './tokenStorage.js'; | ||
@@ -31,9 +31,6 @@ const getName = id => { | ||
var _getAuthToken = /*#__PURE__*/_classPrivateFieldLooseKey("getAuthToken"); | ||
var _removeAuthToken = /*#__PURE__*/_classPrivateFieldLooseKey("removeAuthToken"); | ||
export default class Provider extends RequestClient { | ||
constructor(uppy, opts) { | ||
var _opts$supportsRefresh; | ||
super(uppy, opts); | ||
Object.defineProperty(this, _removeAuthToken, { | ||
value: _removeAuthToken2 | ||
}); | ||
Object.defineProperty(this, _getAuthToken, { | ||
@@ -53,3 +50,5 @@ value: _getAuthToken2 | ||
this.preAuthToken = null; | ||
this.supportsRefreshToken = (_opts$supportsRefresh = opts.supportsRefreshToken) != null ? _opts$supportsRefresh : true; // todo false in next major | ||
} | ||
async headers() { | ||
@@ -75,3 +74,3 @@ const [headers, token] = await Promise.all([super.headers(), _classPrivateFieldLooseBase(this, _getAuthToken)[_getAuthToken]()]); | ||
const oldAuthenticated = plugin.getPluginState().authenticated; | ||
const authenticated = oldAuthenticated ? response.status !== 401 : response.status < 400; | ||
const authenticated = oldAuthenticated ? response.status !== authErrorStatusCode : response.status < 400; | ||
plugin.setPluginState({ | ||
@@ -85,2 +84,7 @@ authenticated | ||
} | ||
/** @protected */ | ||
async removeAuthToken() { | ||
return this.uppy.getPlugin(this.pluginId).storage.removeItem(this.tokenKey); | ||
} | ||
/** | ||
@@ -98,11 +102,20 @@ * Ensure we have a preauth token if necessary. Attempts to fetch one if we don't, | ||
} | ||
authUrl(queries) { | ||
if (queries === void 0) { | ||
queries = {}; | ||
} | ||
// eslint-disable-next-line class-methods-use-this | ||
authQuery() { | ||
return {}; | ||
} | ||
authUrl(_temp) { | ||
let { | ||
authFormData, | ||
query | ||
} = _temp === void 0 ? {} : _temp; | ||
const params = new URLSearchParams({ | ||
...query, | ||
state: btoa(JSON.stringify({ | ||
origin: getOrigin() | ||
})), | ||
...queries | ||
...this.authQuery({ | ||
authFormData | ||
}) | ||
}); | ||
@@ -114,7 +127,39 @@ if (this.preAuthToken) { | ||
} | ||
async login(queries) { | ||
/** @protected */ | ||
async loginSimpleAuth(_ref) { | ||
let { | ||
uppyVersions, | ||
authFormData, | ||
signal | ||
} = _ref; | ||
const response = await this.post(`${this.id}/simple-auth`, { | ||
form: authFormData | ||
}, { | ||
qs: { | ||
uppyVersions | ||
}, | ||
signal | ||
}); | ||
this.setAuthToken(response.uppyAuthToken); | ||
} | ||
/** @protected */ | ||
async loginOAuth(_ref2) { | ||
let { | ||
uppyVersions, | ||
authFormData, | ||
signal | ||
} = _ref2; | ||
await this.ensurePreAuth(); | ||
signal.throwIfAborted(); | ||
return new Promise((resolve, reject) => { | ||
const link = this.authUrl(queries); | ||
const link = this.authUrl({ | ||
query: { | ||
uppyVersions | ||
}, | ||
authFormData | ||
}); | ||
const authWindow = window.open(link, '_blank'); | ||
let cleanup; | ||
const handleToken = e => { | ||
@@ -161,9 +206,26 @@ if (e.source !== authWindow) { | ||
} | ||
cleanup(); | ||
resolve(this.setAuthToken(data.token)); | ||
}; | ||
cleanup = () => { | ||
authWindow.close(); | ||
window.removeEventListener('message', handleToken); | ||
this.setAuthToken(data.token).then(() => resolve()).catch(reject); | ||
signal.removeEventListener('abort', cleanup); | ||
}; | ||
signal.addEventListener('abort', cleanup); | ||
window.addEventListener('message', handleToken); | ||
}); | ||
} | ||
async login(_ref3) { | ||
let { | ||
uppyVersions, | ||
authFormData, | ||
signal | ||
} = _ref3; | ||
return this.loginOAuth({ | ||
uppyVersions, | ||
authFormData, | ||
signal | ||
}); | ||
} | ||
refreshTokenUrl() { | ||
@@ -188,2 +250,3 @@ return `${this.hostname}/${this.id}/refresh-token`; | ||
} catch (err) { | ||
if (!this.supportsRefreshToken) throw err; | ||
// only handle auth errors (401 from provider), and only handle them if we have a (refresh) token | ||
@@ -206,3 +269,3 @@ const authTokenAfter = await _classPrivateFieldLooseBase(this, _getAuthToken)[_getAuthToken](); | ||
// if refresh-token has failed with auth error, delete token, so we don't keep trying to refresh in future | ||
await _classPrivateFieldLooseBase(this, _removeAuthToken)[_removeAuthToken](); | ||
await this.removeAuthToken(); | ||
} | ||
@@ -239,3 +302,3 @@ throw err; | ||
const response = await this.get(`${this.id}/logout`, options); | ||
await _classPrivateFieldLooseBase(this, _removeAuthToken)[_removeAuthToken](); | ||
await this.removeAuthToken(); | ||
return response; | ||
@@ -275,5 +338,2 @@ } | ||
return this.uppy.getPlugin(this.pluginId).storage.getItem(this.tokenKey); | ||
} | ||
async function _removeAuthToken2() { | ||
return this.uppy.getPlugin(this.pluginId).storage.removeItem(this.tokenKey); | ||
} |
'use strict'; | ||
// eslint-disable-next-line import/no-extraneous-dependencies | ||
let _Symbol$for; | ||
@@ -8,2 +7,4 @@ function _classPrivateFieldLooseBase(receiver, privateKey) { if (!Object.prototype.hasOwnProperty.call(receiver, privateKey)) { throw new TypeError("attempted to use private field on non-instance"); } return receiver; } | ||
function _classPrivateFieldLooseKey(name) { return "__private_" + id++ + "_" + name; } | ||
import UserFacingApiError from '@uppy/utils/lib/UserFacingApiError'; | ||
// eslint-disable-next-line import/no-extraneous-dependencies | ||
import pRetry, { AbortError } from 'p-retry'; | ||
@@ -16,3 +17,3 @@ import fetchWithNetworkError from '@uppy/utils/lib/fetchWithNetworkError'; | ||
const packageJson = { | ||
"version": "3.6.1" | ||
"version": "3.7.0" | ||
}; // Remove the trailing slash so we can always safely append /xyz. | ||
@@ -25,3 +26,3 @@ function stripSlash(url) { | ||
const authErrorStatusCode = 401; | ||
export const authErrorStatusCode = 401; | ||
class HttpError extends Error { | ||
@@ -35,2 +36,3 @@ constructor(_ref) { | ||
this.statusCode = void 0; | ||
this.name = 'HttpError'; | ||
this.statusCode = statusCode; | ||
@@ -47,9 +49,16 @@ } | ||
let errMsg = `Failed request with status: ${res.status}. ${res.statusText}`; | ||
let errData; | ||
try { | ||
const errData = await res.json(); | ||
errMsg = errData.message ? `${errMsg} message: ${errData.message}` : errMsg; | ||
errMsg = errData.requestId ? `${errMsg} request-Id: ${errData.requestId}` : errMsg; | ||
} catch { | ||
/* if the response contains invalid JSON, let's ignore the error */ | ||
errData = await res.json(); | ||
if (errData.message) errMsg = `${errMsg} message: ${errData.message}`; | ||
if (errData.requestId) errMsg = `${errMsg} request-Id: ${errData.requestId}`; | ||
} catch (cause) { | ||
// if the response contains invalid JSON, let's ignore the error data | ||
throw new Error(errMsg, { | ||
cause | ||
}); | ||
} | ||
if (res.status >= 400 && res.status <= 499 && errData.message) { | ||
throw new UserFacingApiError(errData.message); | ||
} | ||
throw new HttpError({ | ||
@@ -60,5 +69,2 @@ statusCode: res.status, | ||
} | ||
// todo pull out into core instead? | ||
const allowedHeadersCache = new Map(); | ||
var _companionHeaders = /*#__PURE__*/_classPrivateFieldLooseKey("companionHeaders"); | ||
@@ -124,7 +130,12 @@ var _getUrl = /*#__PURE__*/_classPrivateFieldLooseKey("getUrl"); | ||
} | ||
async headers() { | ||
async headers(emptyBody) { | ||
if (emptyBody === void 0) { | ||
emptyBody = false; | ||
} | ||
const defaultHeaders = { | ||
Accept: 'application/json', | ||
'Content-Type': 'application/json', | ||
'Uppy-Versions': `@uppy/companion-client=${RequestClient.VERSION}` | ||
...(emptyBody ? undefined : { | ||
// Passing those headers on requests with no data forces browsers to first make a preflight request. | ||
'Content-Type': 'application/json' | ||
}) | ||
}; | ||
@@ -154,59 +165,4 @@ return { | ||
} | ||
/* | ||
Preflight was added to avoid breaking change between older Companion-client versions and | ||
newer Companion versions and vice-versa. Usually the break will manifest via CORS errors because a | ||
version of companion-client could be sending certain headers to a version of Companion server that | ||
does not support those headers. In which case, the default preflight would lead to CORS. | ||
So to avoid those errors, we do preflight ourselves, to see what headers the Companion server | ||
we are communicating with allows. And based on that, companion-client knows what headers to | ||
send and what headers to not send. | ||
The preflight only happens once throughout the life-cycle of a certain | ||
Companion-client <-> Companion-server pair (allowedHeadersCache). | ||
Subsequent requests use the cached result of the preflight. | ||
However if there is an error retrieving the allowed headers, we will try again next time | ||
*/ | ||
async preflight(path) { | ||
const allowedHeadersCached = allowedHeadersCache.get(this.hostname); | ||
if (allowedHeadersCached != null) return allowedHeadersCached; | ||
const fallbackAllowedHeaders = ['accept', 'content-type', 'uppy-auth-token']; | ||
const promise = (async () => { | ||
try { | ||
const response = await fetch(_classPrivateFieldLooseBase(this, _getUrl)[_getUrl](path), { | ||
method: 'OPTIONS' | ||
}); | ||
const header = response.headers.get('access-control-allow-headers'); | ||
if (header == null || header === '*') { | ||
allowedHeadersCache.set(this.hostname, fallbackAllowedHeaders); | ||
return fallbackAllowedHeaders; | ||
} | ||
this.uppy.log(`[CompanionClient] adding allowed preflight headers to companion cache: ${this.hostname} ${header}`); | ||
const allowedHeaders = header.split(',').map(headerName => headerName.trim().toLowerCase()); | ||
allowedHeadersCache.set(this.hostname, allowedHeaders); | ||
return allowedHeaders; | ||
} catch (err) { | ||
this.uppy.log(`[CompanionClient] unable to make preflight request ${err}`, 'warning'); | ||
// If the user gets a network error or similar, we should try preflight | ||
// again next time, or else we might get incorrect behaviour. | ||
allowedHeadersCache.delete(this.hostname); // re-fetch next time | ||
return fallbackAllowedHeaders; | ||
} | ||
})(); | ||
allowedHeadersCache.set(this.hostname, promise); | ||
return promise; | ||
} | ||
async preflightAndHeaders(path) { | ||
const [allowedHeaders, headers] = await Promise.all([this.preflight(path), this.headers()]); | ||
// filter to keep only allowed Headers | ||
return Object.fromEntries(Object.entries(headers).filter(_ref4 => { | ||
let [header] = _ref4; | ||
if (!allowedHeaders.includes(header.toLowerCase())) { | ||
this.uppy.log(`[CompanionClient] excluding disallowed header ${header}`); | ||
return false; | ||
} | ||
return true; | ||
})); | ||
} | ||
/** @protected */ | ||
async request(_ref5) { | ||
async request(_ref4) { | ||
let { | ||
@@ -218,5 +174,5 @@ path, | ||
signal | ||
} = _ref5; | ||
} = _ref4; | ||
try { | ||
const headers = await this.preflightAndHeaders(path); | ||
const headers = await this.headers(!data); | ||
const response = await fetchWithNetworkError(_classPrivateFieldLooseBase(this, _getUrl)[_getUrl](path), { | ||
@@ -233,3 +189,3 @@ method, | ||
// pass these through | ||
if (err instanceof AuthError || err.name === 'AbortError') throw err; | ||
if (err.isAuthError || err.name === 'UserFacingApiError' || err.name === 'AbortError') throw err; | ||
throw new ErrorWithCause(`Could not ${method} ${_classPrivateFieldLooseBase(this, _getUrl)[_getUrl](path)}`, { | ||
@@ -327,7 +283,7 @@ cause: err | ||
// throwing AbortError will cause p-retry to stop retrying | ||
if (outerErr instanceof AuthError) throw new AbortError(outerErr); | ||
if (outerErr.isAuthError) throw new AbortError(outerErr); | ||
if (outerErr.cause == null) throw outerErr; | ||
const err = outerErr.cause; | ||
const isRetryableHttpError = () => [408, 409, 429, 418, 423].includes(err.statusCode) || err.statusCode >= 500 && err.statusCode <= 599 && ![501, 505].includes(err.statusCode); | ||
if (err instanceof HttpError && !isRetryableHttpError()) throw new AbortError(err); | ||
if (err.name === 'HttpError' && !isRetryableHttpError()) throw new AbortError(err); | ||
@@ -380,3 +336,3 @@ // p-retry will retry most other errors, | ||
} | ||
async function _awaitRemoteFileUpload2(_ref6) { | ||
async function _awaitRemoteFileUpload2(_ref5) { | ||
let { | ||
@@ -386,3 +342,3 @@ file, | ||
signal | ||
} = _ref6; | ||
} = _ref5; | ||
let removeEventHandlers; | ||
@@ -543,7 +499,7 @@ const { | ||
}; | ||
const onCancelAll = _ref7 => { | ||
const onCancelAll = _ref6 => { | ||
var _socketAbortControlle5; | ||
let { | ||
reason | ||
} = _ref7; | ||
} = _ref6; | ||
if (reason === 'user') { | ||
@@ -550,0 +506,0 @@ socketSend('cancel'); |
{ | ||
"name": "@uppy/companion-client", | ||
"description": "Client library for communication with Companion. Intended for use in Uppy plugins.", | ||
"version": "3.6.1", | ||
"version": "3.7.0", | ||
"license": "MIT", | ||
@@ -25,3 +25,3 @@ "main": "lib/index.js", | ||
"dependencies": { | ||
"@uppy/utils": "^5.6.0", | ||
"@uppy/utils": "^5.7.0", | ||
"namespace-emitter": "^2.0.1", | ||
@@ -28,0 +28,0 @@ "p-retry": "^6.1.0" |
'use strict' | ||
class AuthError extends Error { | ||
constructor () { | ||
constructor() { | ||
super('Authorization required') | ||
this.name = 'AuthError' | ||
// we use a property because of instanceof is unsafe: | ||
// https://github.com/transloadit/uppy/pull/4619#discussion_r1406225982 | ||
this.isAuthError = true | ||
@@ -8,0 +11,0 @@ } |
'use strict' | ||
import RequestClient from './RequestClient.js' | ||
import RequestClient, { authErrorStatusCode } from './RequestClient.js' | ||
import * as tokenStorage from './tokenStorage.js' | ||
const getName = (id) => { | ||
@@ -10,3 +11,3 @@ return id.split('-').map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join(' ') | ||
function getOrigin () { | ||
function getOrigin() { | ||
// eslint-disable-next-line no-restricted-globals | ||
@@ -16,3 +17,3 @@ return location.origin | ||
function getRegex (value) { | ||
function getRegex(value) { | ||
if (typeof value === 'string') { | ||
@@ -26,3 +27,3 @@ return new RegExp(`^${value}$`) | ||
function isOriginAllowed (origin, allowedOrigin) { | ||
function isOriginAllowed(origin, allowedOrigin) { | ||
const patterns = Array.isArray(allowedOrigin) ? allowedOrigin.map(getRegex) : [getRegex(allowedOrigin)] | ||
@@ -36,3 +37,3 @@ return patterns | ||
constructor (uppy, opts) { | ||
constructor(uppy, opts) { | ||
super(uppy, opts) | ||
@@ -46,5 +47,6 @@ this.provider = opts.provider | ||
this.preAuthToken = null | ||
this.supportsRefreshToken = opts.supportsRefreshToken ?? true // todo false in next major | ||
} | ||
async headers () { | ||
async headers() { | ||
const [headers, token] = await Promise.all([super.headers(), this.#getAuthToken()]) | ||
@@ -64,7 +66,7 @@ const authHeaders = {} | ||
onReceiveResponse (response) { | ||
onReceiveResponse(response) { | ||
super.onReceiveResponse(response) | ||
const plugin = this.uppy.getPlugin(this.pluginId) | ||
const oldAuthenticated = plugin.getPluginState().authenticated | ||
const authenticated = oldAuthenticated ? response.status !== 401 : response.status < 400 | ||
const authenticated = oldAuthenticated ? response.status !== authErrorStatusCode : response.status < 400 | ||
plugin.setPluginState({ authenticated }) | ||
@@ -74,11 +76,12 @@ return response | ||
async setAuthToken (token) { | ||
async setAuthToken(token) { | ||
return this.uppy.getPlugin(this.pluginId).storage.setItem(this.tokenKey, token) | ||
} | ||
async #getAuthToken () { | ||
async #getAuthToken() { | ||
return this.uppy.getPlugin(this.pluginId).storage.getItem(this.tokenKey) | ||
} | ||
async #removeAuthToken () { | ||
/** @protected */ | ||
async removeAuthToken() { | ||
return this.uppy.getPlugin(this.pluginId).storage.removeItem(this.tokenKey) | ||
@@ -91,3 +94,3 @@ } | ||
*/ | ||
async ensurePreAuth () { | ||
async ensurePreAuth() { | ||
if (this.companionKeysParams && !this.preAuthToken) { | ||
@@ -102,7 +105,14 @@ await this.fetchPreAuthToken() | ||
authUrl (queries = {}) { | ||
// eslint-disable-next-line class-methods-use-this | ||
authQuery() { | ||
return {} | ||
} | ||
authUrl({ authFormData, query } = {}) { | ||
const params = new URLSearchParams({ | ||
...query, | ||
state: btoa(JSON.stringify({ origin: getOrigin() })), | ||
...queries, | ||
...this.authQuery({ authFormData }), | ||
}) | ||
if (this.preAuthToken) { | ||
@@ -115,8 +125,20 @@ params.set('uppyPreAuthToken', this.preAuthToken) | ||
async login (queries) { | ||
/** @protected */ | ||
async loginSimpleAuth({ uppyVersions, authFormData, signal }) { | ||
const response = await this.post(`${this.id}/simple-auth`, { form: authFormData }, { qs: { uppyVersions }, signal }) | ||
this.setAuthToken(response.uppyAuthToken) | ||
} | ||
/** @protected */ | ||
async loginOAuth({ uppyVersions, authFormData, signal }) { | ||
await this.ensurePreAuth() | ||
signal.throwIfAborted() | ||
return new Promise((resolve, reject) => { | ||
const link = this.authUrl(queries) | ||
const link = this.authUrl({ query: { uppyVersions }, authFormData }) | ||
const authWindow = window.open(link, '_blank') | ||
let cleanup | ||
const handleToken = (e) => { | ||
@@ -161,6 +183,13 @@ if (e.source !== authWindow) { | ||
cleanup() | ||
resolve(this.setAuthToken(data.token)) | ||
} | ||
cleanup = () => { | ||
authWindow.close() | ||
window.removeEventListener('message', handleToken) | ||
this.setAuthToken(data.token).then(() => resolve()).catch(reject) | ||
signal.removeEventListener('abort', cleanup) | ||
} | ||
signal.addEventListener('abort', cleanup) | ||
window.addEventListener('message', handleToken) | ||
@@ -170,7 +199,11 @@ }) | ||
refreshTokenUrl () { | ||
async login({ uppyVersions, authFormData, signal }) { | ||
return this.loginOAuth({ uppyVersions, authFormData, signal }) | ||
} | ||
refreshTokenUrl() { | ||
return `${this.hostname}/${this.id}/refresh-token` | ||
} | ||
fileUrl (id) { | ||
fileUrl(id) { | ||
return `${this.hostname}/${this.id}/get/${id}` | ||
@@ -180,3 +213,3 @@ } | ||
/** @protected */ | ||
async request (...args) { | ||
async request(...args) { | ||
await this.#refreshingTokenPromise | ||
@@ -193,2 +226,3 @@ | ||
} catch (err) { | ||
if (!this.supportsRefreshToken) throw err | ||
// only handle auth errors (401 from provider), and only handle them if we have a (refresh) token | ||
@@ -209,3 +243,3 @@ const authTokenAfter = await this.#getAuthToken() | ||
// if refresh-token has failed with auth error, delete token, so we don't keep trying to refresh in future | ||
await this.#removeAuthToken() | ||
await this.removeAuthToken() | ||
} | ||
@@ -226,3 +260,3 @@ throw err | ||
async fetchPreAuthToken () { | ||
async fetchPreAuthToken() { | ||
if (!this.companionKeysParams) { | ||
@@ -240,13 +274,13 @@ return | ||
list (directory, options) { | ||
list(directory, options) { | ||
return this.get(`${this.id}/list/${directory || ''}`, options) | ||
} | ||
async logout (options) { | ||
async logout(options) { | ||
const response = await this.get(`${this.id}/logout`, options) | ||
await this.#removeAuthToken() | ||
await this.removeAuthToken() | ||
return response | ||
} | ||
static initPlugin (plugin, opts, defaultOpts) { | ||
static initPlugin(plugin, opts, defaultOpts) { | ||
/* eslint-disable no-param-reassign */ | ||
@@ -253,0 +287,0 @@ plugin.type = 'acquirer' |
'use strict' | ||
import UserFacingApiError from '@uppy/utils/lib/UserFacingApiError' | ||
// eslint-disable-next-line import/no-extraneous-dependencies | ||
@@ -16,3 +17,3 @@ import pRetry, { AbortError } from 'p-retry' | ||
// Remove the trailing slash so we can always safely append /xyz. | ||
function stripSlash (url) { | ||
function stripSlash(url) { | ||
return url.replace(/\/$/, '') | ||
@@ -24,3 +25,3 @@ } | ||
const authErrorStatusCode = 401 | ||
export const authErrorStatusCode = 401 | ||
@@ -32,2 +33,3 @@ class HttpError extends Error { | ||
super(message) | ||
this.name = 'HttpError' | ||
this.statusCode = statusCode | ||
@@ -37,3 +39,3 @@ } | ||
async function handleJSONResponse (res) { | ||
async function handleJSONResponse(res) { | ||
if (res.status === authErrorStatusCode) { | ||
@@ -48,19 +50,20 @@ throw new AuthError() | ||
let errMsg = `Failed request with status: ${res.status}. ${res.statusText}` | ||
let errData | ||
try { | ||
const errData = await res.json() | ||
errData = await res.json() | ||
errMsg = errData.message ? `${errMsg} message: ${errData.message}` : errMsg | ||
errMsg = errData.requestId | ||
? `${errMsg} request-Id: ${errData.requestId}` | ||
: errMsg | ||
} catch { | ||
/* if the response contains invalid JSON, let's ignore the error */ | ||
if (errData.message) errMsg = `${errMsg} message: ${errData.message}` | ||
if (errData.requestId) errMsg = `${errMsg} request-Id: ${errData.requestId}` | ||
} catch (cause) { | ||
// if the response contains invalid JSON, let's ignore the error data | ||
throw new Error(errMsg, { cause }) | ||
} | ||
if (res.status >= 400 && res.status <= 499 && errData.message) { | ||
throw new UserFacingApiError(errData.message) | ||
} | ||
throw new HttpError({ statusCode: res.status, message: errMsg }) | ||
} | ||
// todo pull out into core instead? | ||
const allowedHeadersCache = new Map() | ||
export default class RequestClient { | ||
@@ -71,3 +74,3 @@ static VERSION = packageJson.version | ||
constructor (uppy, opts) { | ||
constructor(uppy, opts) { | ||
this.uppy = uppy | ||
@@ -79,11 +82,11 @@ this.opts = opts | ||
setCompanionHeaders (headers) { | ||
setCompanionHeaders(headers) { | ||
this.#companionHeaders = headers | ||
} | ||
[Symbol.for('uppy test: getCompanionHeaders')] () { | ||
[Symbol.for('uppy test: getCompanionHeaders')]() { | ||
return this.#companionHeaders | ||
} | ||
get hostname () { | ||
get hostname() { | ||
const { companion } = this.uppy.getState() | ||
@@ -94,7 +97,9 @@ const host = this.opts.companionUrl | ||
async headers () { | ||
async headers (emptyBody = false) { | ||
const defaultHeaders = { | ||
Accept: 'application/json', | ||
'Content-Type': 'application/json', | ||
'Uppy-Versions': `@uppy/companion-client=${RequestClient.VERSION}`, | ||
...(emptyBody ? undefined : { | ||
// Passing those headers on requests with no data forces browsers to first make a preflight request. | ||
'Content-Type': 'application/json', | ||
}), | ||
} | ||
@@ -108,3 +113,3 @@ | ||
onReceiveResponse ({ headers }) { | ||
onReceiveResponse({ headers }) { | ||
const state = this.uppy.getState() | ||
@@ -122,3 +127,3 @@ const companion = state.companion || {} | ||
#getUrl (url) { | ||
#getUrl(url) { | ||
if (/^(https?:|)\/\//.test(url)) { | ||
@@ -130,84 +135,6 @@ return url | ||
/* | ||
Preflight was added to avoid breaking change between older Companion-client versions and | ||
newer Companion versions and vice-versa. Usually the break will manifest via CORS errors because a | ||
version of companion-client could be sending certain headers to a version of Companion server that | ||
does not support those headers. In which case, the default preflight would lead to CORS. | ||
So to avoid those errors, we do preflight ourselves, to see what headers the Companion server | ||
we are communicating with allows. And based on that, companion-client knows what headers to | ||
send and what headers to not send. | ||
The preflight only happens once throughout the life-cycle of a certain | ||
Companion-client <-> Companion-server pair (allowedHeadersCache). | ||
Subsequent requests use the cached result of the preflight. | ||
However if there is an error retrieving the allowed headers, we will try again next time | ||
*/ | ||
async preflight (path) { | ||
const allowedHeadersCached = allowedHeadersCache.get(this.hostname) | ||
if (allowedHeadersCached != null) return allowedHeadersCached | ||
const fallbackAllowedHeaders = [ | ||
'accept', | ||
'content-type', | ||
'uppy-auth-token', | ||
] | ||
const promise = (async () => { | ||
try { | ||
const response = await fetch(this.#getUrl(path), { method: 'OPTIONS' }) | ||
const header = response.headers.get('access-control-allow-headers') | ||
if (header == null || header === '*') { | ||
allowedHeadersCache.set(this.hostname, fallbackAllowedHeaders) | ||
return fallbackAllowedHeaders | ||
} | ||
this.uppy.log( | ||
`[CompanionClient] adding allowed preflight headers to companion cache: ${this.hostname} ${header}`, | ||
) | ||
const allowedHeaders = header | ||
.split(',') | ||
.map((headerName) => headerName.trim().toLowerCase()) | ||
allowedHeadersCache.set(this.hostname, allowedHeaders) | ||
return allowedHeaders | ||
} catch (err) { | ||
this.uppy.log( | ||
`[CompanionClient] unable to make preflight request ${err}`, | ||
'warning', | ||
) | ||
// If the user gets a network error or similar, we should try preflight | ||
// again next time, or else we might get incorrect behaviour. | ||
allowedHeadersCache.delete(this.hostname) // re-fetch next time | ||
return fallbackAllowedHeaders | ||
} | ||
})() | ||
allowedHeadersCache.set(this.hostname, promise) | ||
return promise | ||
} | ||
async preflightAndHeaders (path) { | ||
const [allowedHeaders, headers] = await Promise.all([ | ||
this.preflight(path), | ||
this.headers(), | ||
]) | ||
// filter to keep only allowed Headers | ||
return Object.fromEntries( | ||
Object.entries(headers).filter(([header]) => { | ||
if (!allowedHeaders.includes(header.toLowerCase())) { | ||
this.uppy.log( | ||
`[CompanionClient] excluding disallowed header ${header}`, | ||
) | ||
return false | ||
} | ||
return true | ||
}), | ||
) | ||
} | ||
/** @protected */ | ||
async request ({ path, method = 'GET', data, skipPostResponse, signal }) { | ||
async request({ path, method = 'GET', data, skipPostResponse, signal }) { | ||
try { | ||
const headers = await this.preflightAndHeaders(path) | ||
const headers = await this.headers(!data) | ||
const response = await fetchWithNetworkError(this.#getUrl(path), { | ||
@@ -225,3 +152,3 @@ method, | ||
// pass these through | ||
if (err instanceof AuthError || err.name === 'AbortError') throw err | ||
if (err.isAuthError || err.name === 'UserFacingApiError' || err.name === 'AbortError') throw err | ||
@@ -234,3 +161,3 @@ throw new ErrorWithCause(`Could not ${method} ${this.#getUrl(path)}`, { | ||
async get (path, options = undefined) { | ||
async get(path, options = undefined) { | ||
// TODO: remove boolean support for options that was added for backward compatibility. | ||
@@ -242,3 +169,3 @@ // eslint-disable-next-line no-param-reassign | ||
async post (path, data, options = undefined) { | ||
async post(path, data, options = undefined) { | ||
// TODO: remove boolean support for options that was added for backward compatibility. | ||
@@ -250,3 +177,3 @@ // eslint-disable-next-line no-param-reassign | ||
async delete (path, data = undefined, options) { | ||
async delete(path, data = undefined, options) { | ||
// TODO: remove boolean support for options that was added for backward compatibility. | ||
@@ -271,3 +198,3 @@ // eslint-disable-next-line no-param-reassign | ||
*/ | ||
async uploadRemoteFile (file, reqBody, options = {}) { | ||
async uploadRemoteFile(file, reqBody, options = {}) { | ||
try { | ||
@@ -289,3 +216,3 @@ const { signal, getQueue } = options | ||
// throwing AbortError will cause p-retry to stop retrying | ||
if (outerErr instanceof AuthError) throw new AbortError(outerErr) | ||
if (outerErr.isAuthError) throw new AbortError(outerErr) | ||
@@ -299,4 +226,4 @@ if (outerErr.cause == null) throw outerErr | ||
) | ||
if (err instanceof HttpError && !isRetryableHttpError()) throw new AbortError(err); | ||
if (err.name === 'HttpError' && !isRetryableHttpError()) throw new AbortError(err); | ||
// p-retry will retry most other errors, | ||
@@ -353,3 +280,3 @@ // but it will not retry TypeError (except network error TypeErrors) | ||
*/ | ||
async #awaitRemoteFileUpload ({ file, queue, signal }) { | ||
async #awaitRemoteFileUpload({ file, queue, signal }) { | ||
let removeEventHandlers | ||
@@ -381,3 +308,3 @@ | ||
payload: payload ?? {}, | ||
})) | ||
})) | ||
}; | ||
@@ -402,3 +329,3 @@ | ||
} | ||
// todo instead implement the ability for users to cancel / retry *currently uploading files* in the UI | ||
@@ -438,3 +365,3 @@ function resetActivityTimeout() { | ||
const { action, payload } = JSON.parse(e.data) | ||
switch (action) { | ||
@@ -455,4 +382,4 @@ case 'progress': { | ||
} | ||
default: | ||
this.uppy.log(`Companion socket unknown action ${action}`, 'warning') | ||
default: | ||
this.uppy.log(`Companion socket unknown action ${action}`, 'warning') | ||
} | ||
@@ -470,3 +397,3 @@ } catch (err) { | ||
} | ||
socketAbortController.signal.addEventListener('abort', () => { | ||
@@ -473,0 +400,0 @@ closeSocket() |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
0
147926
1991
Updated@uppy/utils@^5.7.0