Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unify http error responses #5595

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/custom-provider/server/index.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ app.use((req, res) => {
// handle server errors
app.use((err, req, res) => {
console.error('\x1b[31m', err.stack, '\x1b[0m')
res.status(err.status || 500).json({ message: err.message, error: err })
res.status(500).json({ message: err.message, error: err })
})

uppy.socket(app.listen(3020), uppyOptions)
Expand Down
2 changes: 1 addition & 1 deletion examples/uppy-with-companion/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ app.use((req, res) => {
// handle server errors
app.use((err, req, res) => {
console.error('\x1b[31m', err.stack, '\x1b[0m')
res.status(err.status || 500).json({ message: err.message, error: err })
res.status(500).json({ message: err.message, error: err })
})

companion.socket(app.listen(3020))
Expand Down
3 changes: 3 additions & 0 deletions packages/@uppy/companion/src/server/controllers/get.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
const logger = require('../logger')
const { startDownUpload } = require('../helpers/upload')
const { respondWithError } = require('../provider/error')


async function get (req, res) {
const { id } = req.params
Expand All @@ -17,6 +19,7 @@ async function get (req, res) {
await startDownUpload({ req, res, getSize, download })
} catch (err) {
logger.error(err, 'controller.get.error', req.id)
if (respondWithError(err, res)) return
res.status(500).json({ message: 'Failed to download file' })
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const { getURLMeta } = require('../helpers/request')
const logger = require('../logger')
const { downloadURL } = require('../download')
const { getGoogleFileSize, streamGoogleFile } = require('../provider/google/drive');
const { respondWithError } = require('../provider/error')


const getAuthHeader = (token) => ({ authorization: `Bearer ${token}` });
Expand Down Expand Up @@ -49,7 +50,8 @@ const get = async (req, res) => {
await startDownUpload({ req, res, getSize, download })
} catch (err) {
logger.error(err, 'controller.googlePicker.error', req.id)
res.status(err.status || 500).json({ message: 'failed to fetch Google Picker URL' })
if (respondWithError(err, res)) return
res.status(500).json({ message: 'failed to fetch Google Picker URL' })
}
}

Expand Down
12 changes: 8 additions & 4 deletions packages/@uppy/companion/src/server/controllers/url.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const { downloadURL } = require('../download')
const { validateURL } = require('../helpers/request')
const { getURLMeta } = require('../helpers/request')
const logger = require('../logger')
const { respondWithError } = require('../provider/error')

/**
* @callback downloadCallback
Expand All @@ -24,14 +25,16 @@ const meta = async (req, res) => {
const { allowLocalUrls } = req.companion.options
if (!validateURL(req.body.url, allowLocalUrls)) {
logger.debug('Invalid request body detected. Exiting url meta handler.', null, req.id)
return res.status(400).json({ error: 'Invalid request body' })
res.status(400).json({ error: 'Invalid request body' })
return
}

const urlMeta = await getURLMeta(req.body.url, allowLocalUrls)
return res.json(urlMeta)
res.json(urlMeta)
} catch (err) {
logger.error(err, 'controller.url.meta.error', req.id)
return res.status(err.status || 500).json({ message: 'failed to fetch URL metadata' })
if (respondWithError(err, res)) return
res.status(500).json({ message: 'failed to fetch URL metadata' })
}
}

Expand Down Expand Up @@ -62,7 +65,8 @@ const get = async (req, res) => {
await startDownUpload({ req, res, getSize, download })
} catch (err) {
logger.error(err, 'controller.url.error', req.id)
res.status(err.status || 500).json({ message: 'failed to fetch URL' })
if (respondWithError(err, res)) return
res.status(500).json({ message: 'failed to fetch URL' })
}
}

Expand Down
65 changes: 26 additions & 39 deletions packages/@uppy/companion/src/server/helpers/upload.js
Original file line number Diff line number Diff line change
@@ -1,51 +1,38 @@
const Uploader = require('../Uploader')
const logger = require('../logger')
const { respondWithError } = require('../provider/error')

async function startDownUpload({ req, res, getSize, download }) {
try {
logger.debug('Starting download stream.', null, req.id)
const { stream, size: maybeSize } = await download()
logger.debug('Starting download stream.', null, req.id)
const { stream, size: maybeSize } = await download()

let size
// if the provider already knows the size, we can use that
if (typeof maybeSize === 'number' && !Number.isNaN(maybeSize) && maybeSize > 0) {
size = maybeSize
}
// if not we need to get the size
if (size == null) {
size = await getSize()
}
const { clientSocketConnectTimeout } = req.companion.options

logger.debug('Instantiating uploader.', null, req.id)
const uploader = new Uploader(Uploader.reqToOptions(req, size))

// "Forking" off the upload operation to background, so we can return the http request:
; (async () => {
// wait till the client has connected to the socket, before starting
// the download, so that the client can receive all download/upload progress.
logger.debug('Waiting for socket connection before beginning remote download/upload.', null, req.id)
await uploader.awaitReady(clientSocketConnectTimeout)
logger.debug('Socket connection received. Starting remote download/upload.', null, req.id)
let size
// if the provider already knows the size, we can use that
if (typeof maybeSize === 'number' && !Number.isNaN(maybeSize) && maybeSize > 0) {
size = maybeSize
}
// if not we need to get the size
if (size == null) {
size = await getSize()
}
const { clientSocketConnectTimeout } = req.companion.options

await uploader.tryUploadStream(stream, req)
})().catch((err) => logger.error(err))
logger.debug('Instantiating uploader.', null, req.id)
const uploader = new Uploader(Uploader.reqToOptions(req, size))

// Respond the request
// NOTE: the Uploader will continue running after the http request is responded
res.status(200).json({ token: uploader.token })
} catch (err) {
if (err.name === 'ValidationError') {
logger.debug(err.message, 'uploader.validator.fail')
res.status(400).json({ message: err.message })
return
}
// "Forking" off the upload operation to background, so we can return the http request:
; (async () => {
// wait till the client has connected to the socket, before starting
// the download, so that the client can receive all download/upload progress.
logger.debug('Waiting for socket connection before beginning remote download/upload.', null, req.id)
await uploader.awaitReady(clientSocketConnectTimeout)
logger.debug('Socket connection received. Starting remote download/upload.', null, req.id)

if (respondWithError(err, res)) return
await uploader.tryUploadStream(stream, req)
})().catch((err) => logger.error(err))

throw err
}
// Respond the request
// NOTE: the Uploader will continue running after the http request is responded
res.status(200).json({ token: uploader.token })
}

module.exports = { startDownUpload }
5 changes: 4 additions & 1 deletion packages/@uppy/companion/src/server/helpers/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,9 @@ module.exports.decrypt = (encrypted, secret) => {

module.exports.defaultGetKey = ({ filename }) => `${crypto.randomUUID()}-${filename}`

/**
* Our own HttpError in cases where we can't use `got`'s `HTTPError`
*/
class HttpError extends Error {
statusCode

Expand Down Expand Up @@ -176,7 +179,7 @@ module.exports.prepareStream = async (stream) => new Promise((resolve, reject) =
})
.on('error', (err) => {
// In this case the error object is not a normal GOT HTTPError where json is already parsed,
// we create our own HttpError error for this case
// we use our own HttpError error for this scenario.
if (typeof err.response?.body === 'string' && typeof err.response?.statusCode === 'number') {
let responseJson
try {
Expand Down
30 changes: 29 additions & 1 deletion packages/@uppy/companion/src/server/provider/error.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ class ProviderUserError extends ProviderApiError {
/**
* AuthError is error returned when an adapter encounters
* an authorization error while communication with its corresponding provider
* this signals to the client that the access token is invalid and needs to be
* refreshed or the user needs to re-authenticate
*/
class ProviderAuthError extends ProviderApiError {
constructor() {
Expand All @@ -39,17 +41,38 @@ class ProviderAuthError extends ProviderApiError {
}
}

function parseHttpError(err) {
if (err?.name === 'HTTPError') {
return {
statusCode: err.response?.statusCode,
body: err.response?.body,
}
}
if (err?.name === 'HttpError') {
return {
statusCode: err.statusCode,
body: err.responseJson,
}
}
return undefined
}

/**
* Convert an error instance to an http response if possible
*
* @param {Error | ProviderApiError} err the error instance to convert to an http json response
* @returns {object | undefined} an object with a code and json field if the error can be converted to a response
*/
function errorToResponse(err) {
// @ts-ignore
if (err?.isAuthError) {
return { code: 401, json: { message: err.message } }
}

if (err?.name === 'ValidationError') {
return { code: 400, json: { message: err.message } }
}

if (err?.name === 'ProviderUserError') {
// @ts-ignore
return { code: 400, json: err.json }
Expand All @@ -74,6 +97,11 @@ function errorToResponse(err) {
}
}

const httpError = parseHttpError(err)
if (httpError) {
return { code: 500, json: { statusCode: httpError.statusCode, body: httpError.body } }
}

return undefined
}

Expand All @@ -86,4 +114,4 @@ function respondWithError(err, res) {
return false
}

module.exports = { ProviderAuthError, ProviderApiError, ProviderUserError, respondWithError }
module.exports = { ProviderAuthError, ProviderApiError, ProviderUserError, respondWithError, parseHttpError }
21 changes: 7 additions & 14 deletions packages/@uppy/companion/src/server/provider/providerErrors.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const logger = require('../logger')
const { ProviderApiError, ProviderUserError, ProviderAuthError } = require('./error')
const { ProviderApiError, ProviderUserError, ProviderAuthError, parseHttpError } = require('./error')

/**
*
Expand Down Expand Up @@ -37,18 +37,11 @@ async function withProviderErrorHandling({
try {
return await fn()
} catch (err) {
let statusCode
let body
const httpError = parseHttpError(err)

if (err?.name === 'HTTPError') {
statusCode = err.response?.statusCode
body = err.response?.body
} else if (err?.name === 'HttpError') {
statusCode = err.statusCode
body = err.responseJson
}

if (statusCode != null) {
// Wrap all HTTP errors according to the provider's desired error handling
if (httpError) {
const { statusCode, body } = httpError
let knownErr
if (isAuthError({ statusCode, body })) {
knownErr = new ProviderAuthError()
Expand All @@ -62,8 +55,8 @@ async function withProviderErrorHandling({
throw knownErr
}

// non HTTP errors will be passed through
logger.error(err, tag)

throw err
}
}
Expand All @@ -81,4 +74,4 @@ async function withGoogleErrorHandling (providerName, tag, fn) {
})
}

module.exports = { withProviderErrorHandling, withGoogleErrorHandling }
module.exports = { withProviderErrorHandling, withGoogleErrorHandling, parseHttpError }
4 changes: 2 additions & 2 deletions packages/@uppy/companion/src/standalone/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -182,10 +182,10 @@ module.exports = function server(inputCompanionOptions) {
} else {
logger.error(err, 'root.error', req.id)
}
res.status(err.status || 500).json({ message: 'Something went wrong', requestId: req.id })
res.status(500).json({ message: 'Something went wrong', requestId: req.id })
} else {
logger.error(err, 'root.error', req.id)
res.status(err.status || 500).json({ message: err.message, error: err, requestId: req.id })
res.status(500).json({ message: err.message, error: err, requestId: req.id })
}
})

Expand Down
Loading