diff --git a/client/src/app/+videos/+video-watch/video-watch.component.ts b/client/src/app/+videos/+video-watch/video-watch.component.ts index 0acd44524..8034ccebf 100644 --- a/client/src/app/+videos/+video-watch/video-watch.component.ts +++ b/client/src/app/+videos/+video-watch/video-watch.component.ts @@ -431,7 +431,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { .pipe( // If 400, 403 or 404, the video is private or blocked so redirect to 404 catchError(err => { - if (err.body.errorCode === ServerErrorCode.DOES_NOT_RESPECT_FOLLOW_CONSTRAINTS && err.body.originUrl) { + if (err.body.type === ServerErrorCode.DOES_NOT_RESPECT_FOLLOW_CONSTRAINTS && err.body.originUrl) { const search = window.location.search let originUrl = err.body.originUrl if (search) originUrl += search diff --git a/client/src/app/core/rest/rest-extractor.service.ts b/client/src/app/core/rest/rest-extractor.service.ts index b8a95cca6..08ab49512 100644 --- a/client/src/app/core/rest/rest-extractor.service.ts +++ b/client/src/app/core/rest/rest-extractor.service.ts @@ -41,7 +41,7 @@ export class RestExtractor { if (err.error instanceof Error) { // A client-side or network error occurred. Handle it accordingly. - errorMessage = err.error.message + errorMessage = err.error.detail || err.error.title console.error('An error occurred:', errorMessage) } else if (typeof err.error === 'string') { errorMessage = err.error diff --git a/package.json b/package.json index 73830b605..a5a47b6c9 100644 --- a/package.json +++ b/package.json @@ -100,6 +100,7 @@ "fs-extra": "^10.0.0", "got": "^11.8.2", "helmet": "^4.1.0", + "http-problem-details": "^0.1.5", "http-signature": "1.3.5", "ip-anonymize": "^0.1.0", "ipaddr.js": "2.0.0", diff --git a/server.ts b/server.ts index 7aaf1e553..1834256d5 100644 --- a/server.ts +++ b/server.ts @@ -128,6 +128,7 @@ import { LiveManager } from './server/lib/live-manager' import { HttpStatusCode } from './shared/core-utils/miscs/http-error-codes' import { VideosTorrentCache } from '@server/lib/files-cache/videos-torrent-cache' import { ServerConfigManager } from '@server/lib/server-config-manager' +import { apiResponseHelpers } from '@server/helpers/express-utils' // ----------- Command line ----------- @@ -186,6 +187,9 @@ app.use(cookieParser()) // W3C DNT Tracking Status app.use(advertiseDoNotTrack) +// Response helpers used in developement +app.use(apiResponseHelpers) + // ----------- Views, routes and static files ----------- // API @@ -235,7 +239,11 @@ app.use(function (err, req, res, next) { const sql = err.parent ? err.parent.sql : undefined logger.error('Error in controller.', { err: error, sql }) - return res.status(err.status || HttpStatusCode.INTERNAL_SERVER_ERROR_500).end() + return res.fail({ + status: err.status || HttpStatusCode.INTERNAL_SERVER_ERROR_500, + message: err.message, + type: err.name + }) }) const server = createWebsocketTrackerServer(app) diff --git a/server/controllers/api/abuse.ts b/server/controllers/api/abuse.ts index 0ab74bdff..108627f81 100644 --- a/server/controllers/api/abuse.ts +++ b/server/controllers/api/abuse.ts @@ -142,7 +142,7 @@ async function updateAbuse (req: express.Request, res: express.Response) { // Do not send the delete to other instances, we updated OUR copy of this abuse - return res.sendStatus(HttpStatusCode.NO_CONTENT_204) + return res.status(HttpStatusCode.NO_CONTENT_204).end() } async function deleteAbuse (req: express.Request, res: express.Response) { @@ -154,7 +154,7 @@ async function deleteAbuse (req: express.Request, res: express.Response) { // Do not send the delete to other instances, we delete OUR copy of this abuse - return res.sendStatus(HttpStatusCode.NO_CONTENT_204) + return res.status(HttpStatusCode.NO_CONTENT_204).end() } async function reportAbuse (req: express.Request, res: express.Response) { @@ -244,5 +244,5 @@ async function deleteAbuseMessage (req: express.Request, res: express.Response) return abuseMessage.destroy({ transaction: t }) }) - return res.sendStatus(HttpStatusCode.NO_CONTENT_204) + return res.status(HttpStatusCode.NO_CONTENT_204).end() } diff --git a/server/controllers/api/bulk.ts b/server/controllers/api/bulk.ts index 649351029..192daccde 100644 --- a/server/controllers/api/bulk.ts +++ b/server/controllers/api/bulk.ts @@ -34,7 +34,7 @@ async function bulkRemoveCommentsOf (req: express.Request, res: express.Response const comments = await VideoCommentModel.listForBulkDelete(account, filter) // Don't wait result - res.sendStatus(HttpStatusCode.NO_CONTENT_204) + res.status(HttpStatusCode.NO_CONTENT_204).end() for (const comment of comments) { await removeComment(comment) diff --git a/server/controllers/api/custom-page.ts b/server/controllers/api/custom-page.ts index 3c47f7b9a..c19f03c56 100644 --- a/server/controllers/api/custom-page.ts +++ b/server/controllers/api/custom-page.ts @@ -27,7 +27,12 @@ export { async function getInstanceHomepage (req: express.Request, res: express.Response) { const page = await ActorCustomPageModel.loadInstanceHomepage() - if (!page) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) + if (!page) { + return res.fail({ + status: HttpStatusCode.NOT_FOUND_404, + message: 'Instance homepage could not be found' + }) + } return res.json(page.toFormattedJSON()) } @@ -38,5 +43,5 @@ async function updateInstanceHomepage (req: express.Request, res: express.Respon await ActorCustomPageModel.updateInstanceHomepage(content) ServerConfigManager.Instance.updateHomepageState(content) - return res.sendStatus(HttpStatusCode.NO_CONTENT_204) + return res.status(HttpStatusCode.NO_CONTENT_204).end() } diff --git a/server/controllers/api/oauth-clients.ts b/server/controllers/api/oauth-clients.ts index c21e2298d..48a10d31f 100644 --- a/server/controllers/api/oauth-clients.ts +++ b/server/controllers/api/oauth-clients.ts @@ -24,7 +24,10 @@ async function getLocalClient (req: express.Request, res: express.Response, next // Don't make this check if this is a test instance if (process.env.NODE_ENV !== 'test' && req.get('host') !== headerHostShouldBe) { logger.info('Getting client tokens for host %s is forbidden (expected %s).', req.get('host'), headerHostShouldBe) - return res.type('json').status(HttpStatusCode.FORBIDDEN_403).end() + return res.fail({ + status: HttpStatusCode.FORBIDDEN_403, + message: `Getting client tokens for host ${req.get('host')} is forbidden` + }) } const client = await OAuthClientModel.loadFirstClient() diff --git a/server/controllers/api/plugins.ts b/server/controllers/api/plugins.ts index e18eed332..b64062287 100644 --- a/server/controllers/api/plugins.ts +++ b/server/controllers/api/plugins.ts @@ -144,7 +144,7 @@ async function installPlugin (req: express.Request, res: express.Response) { return res.json(plugin.toFormattedJSON()) } catch (err) { logger.warn('Cannot install plugin %s.', toInstall, { err }) - return res.sendStatus(HttpStatusCode.BAD_REQUEST_400) + return res.fail({ message: 'Cannot install plugin ' + toInstall }) } } @@ -159,7 +159,7 @@ async function updatePlugin (req: express.Request, res: express.Response) { return res.json(plugin.toFormattedJSON()) } catch (err) { logger.warn('Cannot update plugin %s.', toUpdate, { err }) - return res.sendStatus(HttpStatusCode.BAD_REQUEST_400) + return res.fail({ message: 'Cannot update plugin ' + toUpdate }) } } @@ -168,7 +168,7 @@ async function uninstallPlugin (req: express.Request, res: express.Response) { await PluginManager.Instance.uninstall(body.npmName) - return res.sendStatus(HttpStatusCode.NO_CONTENT_204) + return res.status(HttpStatusCode.NO_CONTENT_204).end() } function getPublicPluginSettings (req: express.Request, res: express.Response) { @@ -197,7 +197,7 @@ async function updatePluginSettings (req: express.Request, res: express.Response await PluginManager.Instance.onSettingsChanged(plugin.name, plugin.settings) - return res.sendStatus(HttpStatusCode.NO_CONTENT_204) + return res.status(HttpStatusCode.NO_CONTENT_204).end() } async function listAvailablePlugins (req: express.Request, res: express.Response) { @@ -206,8 +206,10 @@ async function listAvailablePlugins (req: express.Request, res: express.Response const resultList = await listAvailablePluginsFromIndex(query) if (!resultList) { - return res.status(HttpStatusCode.SERVICE_UNAVAILABLE_503) - .json({ error: 'Plugin index unavailable. Please retry later' }) + return res.fail({ + status: HttpStatusCode.SERVICE_UNAVAILABLE_503, + message: 'Plugin index unavailable. Please retry later' + }) } return res.json(resultList) diff --git a/server/controllers/api/search.ts b/server/controllers/api/search.ts index f0cdf3a89..77e3a024d 100644 --- a/server/controllers/api/search.ts +++ b/server/controllers/api/search.ts @@ -102,7 +102,10 @@ async function searchVideoChannelsIndex (query: VideoChannelsSearchQuery, res: e } catch (err) { logger.warn('Cannot use search index to make video channels search.', { err }) - return res.sendStatus(HttpStatusCode.INTERNAL_SERVER_ERROR_500) + return res.fail({ + status: HttpStatusCode.INTERNAL_SERVER_ERROR_500, + message: 'Cannot use search index to make video channels search' + }) } } @@ -202,7 +205,10 @@ async function searchVideosIndex (query: VideosSearchQuery, res: express.Respons } catch (err) { logger.warn('Cannot use search index to make video search.', { err }) - return res.sendStatus(HttpStatusCode.INTERNAL_SERVER_ERROR_500) + return res.fail({ + status: HttpStatusCode.INTERNAL_SERVER_ERROR_500, + message: 'Cannot use search index to make video search' + }) } } diff --git a/server/controllers/api/server/debug.ts b/server/controllers/api/server/debug.ts index ff0d9ca3c..a6e9147f3 100644 --- a/server/controllers/api/server/debug.ts +++ b/server/controllers/api/server/debug.ts @@ -1,5 +1,6 @@ import { InboxManager } from '@server/lib/activitypub/inbox-manager' import { RemoveDanglingResumableUploadsScheduler } from '@server/lib/schedulers/remove-dangling-resumable-uploads-scheduler' +import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' import { SendDebugCommand } from '@shared/models' import * as express from 'express' import { UserRight } from '../../../../shared/models/users' @@ -41,5 +42,5 @@ async function runCommand (req: express.Request, res: express.Response) { await RemoveDanglingResumableUploadsScheduler.Instance.execute() } - return res.sendStatus(204) + return res.status(HttpStatusCode.NO_CONTENT_204).end() } diff --git a/server/controllers/api/server/redundancy.ts b/server/controllers/api/server/redundancy.ts index 7c13dc21b..bc593ad43 100644 --- a/server/controllers/api/server/redundancy.ts +++ b/server/controllers/api/server/redundancy.ts @@ -90,13 +90,13 @@ async function addVideoRedundancy (req: express.Request, res: express.Response) payload }) - return res.sendStatus(HttpStatusCode.NO_CONTENT_204) + return res.status(HttpStatusCode.NO_CONTENT_204).end() } async function removeVideoRedundancyController (req: express.Request, res: express.Response) { await removeVideoRedundancy(res.locals.videoRedundancy) - return res.sendStatus(HttpStatusCode.NO_CONTENT_204) + return res.status(HttpStatusCode.NO_CONTENT_204).end() } async function updateRedundancy (req: express.Request, res: express.Response) { @@ -110,5 +110,5 @@ async function updateRedundancy (req: express.Request, res: express.Response) { removeRedundanciesOfServer(server.id) .catch(err => logger.error('Cannot remove redundancy of %s.', server.host, { err })) - return res.sendStatus(HttpStatusCode.NO_CONTENT_204) + return res.status(HttpStatusCode.NO_CONTENT_204).end() } diff --git a/server/controllers/api/users/index.ts b/server/controllers/api/users/index.ts index f384f0f28..d907b49bf 100644 --- a/server/controllers/api/users/index.ts +++ b/server/controllers/api/users/index.ts @@ -314,7 +314,7 @@ async function removeUser (req: express.Request, res: express.Response) { Hooks.runAction('action:api.user.deleted', { user }) - return res.sendStatus(HttpStatusCode.NO_CONTENT_204) + return res.status(HttpStatusCode.NO_CONTENT_204).end() } async function updateUser (req: express.Request, res: express.Response) { @@ -349,7 +349,7 @@ async function updateUser (req: express.Request, res: express.Response) { // Don't need to send this update to followers, these attributes are not federated - return res.sendStatus(HttpStatusCode.NO_CONTENT_204) + return res.status(HttpStatusCode.NO_CONTENT_204).end() } async function askResetUserPassword (req: express.Request, res: express.Response) { diff --git a/server/controllers/api/users/me.ts b/server/controllers/api/users/me.ts index a609abaa6..810e4295e 100644 --- a/server/controllers/api/users/me.ts +++ b/server/controllers/api/users/me.ts @@ -183,7 +183,7 @@ async function deleteMe (req: express.Request, res: express.Response) { await user.destroy() - return res.sendStatus(HttpStatusCode.NO_CONTENT_204) + return res.status(HttpStatusCode.NO_CONTENT_204).end() } async function updateMe (req: express.Request, res: express.Response) { @@ -237,7 +237,7 @@ async function updateMe (req: express.Request, res: express.Response) { await sendVerifyUserEmail(user, true) } - return res.sendStatus(HttpStatusCode.NO_CONTENT_204) + return res.status(HttpStatusCode.NO_CONTENT_204).end() } async function updateMyAvatar (req: express.Request, res: express.Response) { @@ -257,5 +257,5 @@ async function deleteMyAvatar (req: express.Request, res: express.Response) { const userAccount = await AccountModel.load(user.Account.id) await deleteLocalActorImageFile(userAccount, ActorImageType.AVATAR) - return res.sendStatus(HttpStatusCode.NO_CONTENT_204) + return res.status(HttpStatusCode.NO_CONTENT_204).end() } diff --git a/server/controllers/api/users/token.ts b/server/controllers/api/users/token.ts index 694bb0a92..863a3d74c 100644 --- a/server/controllers/api/users/token.ts +++ b/server/controllers/api/users/token.ts @@ -78,9 +78,10 @@ async function handleToken (req: express.Request, res: express.Response, next: e } catch (err) { logger.warn('Login error', { err }) - return res.status(err.code || 400).json({ - code: err.name, - error: err.message + return res.fail({ + status: err.code, + message: err.message, + type: err.name }) } } diff --git a/server/controllers/api/video-channel.ts b/server/controllers/api/video-channel.ts index 859d8b3c0..34207ea8a 100644 --- a/server/controllers/api/video-channel.ts +++ b/server/controllers/api/video-channel.ts @@ -180,7 +180,7 @@ async function deleteVideoChannelAvatar (req: express.Request, res: express.Resp await deleteLocalActorImageFile(videoChannel, ActorImageType.AVATAR) - return res.sendStatus(HttpStatusCode.NO_CONTENT_204) + return res.status(HttpStatusCode.NO_CONTENT_204).end() } async function deleteVideoChannelBanner (req: express.Request, res: express.Response) { @@ -188,7 +188,7 @@ async function deleteVideoChannelBanner (req: express.Request, res: express.Resp await deleteLocalActorImageFile(videoChannel, ActorImageType.BANNER) - return res.sendStatus(HttpStatusCode.NO_CONTENT_204) + return res.status(HttpStatusCode.NO_CONTENT_204).end() } async function addVideoChannel (req: express.Request, res: express.Response) { diff --git a/server/controllers/api/videos/blacklist.ts b/server/controllers/api/videos/blacklist.ts index fa8448c86..ca2b85ea5 100644 --- a/server/controllers/api/videos/blacklist.ts +++ b/server/controllers/api/videos/blacklist.ts @@ -70,7 +70,7 @@ async function addVideoToBlacklistController (req: express.Request, res: express logger.info('Video %s blacklisted.', videoInstance.uuid) - return res.type('json').sendStatus(HttpStatusCode.NO_CONTENT_204) + return res.type('json').status(HttpStatusCode.NO_CONTENT_204).end() } async function updateVideoBlacklistController (req: express.Request, res: express.Response) { @@ -82,7 +82,7 @@ async function updateVideoBlacklistController (req: express.Request, res: expres return videoBlacklist.save({ transaction: t }) }) - return res.type('json').sendStatus(HttpStatusCode.NO_CONTENT_204) + return res.type('json').status(HttpStatusCode.NO_CONTENT_204).end() } async function listBlacklist (req: express.Request, res: express.Response) { @@ -105,5 +105,5 @@ async function removeVideoFromBlacklistController (req: express.Request, res: ex logger.info('Video %s removed from blacklist.', video.uuid) - return res.type('json').sendStatus(HttpStatusCode.NO_CONTENT_204) + return res.type('json').status(HttpStatusCode.NO_CONTENT_204).end() } diff --git a/server/controllers/api/videos/comment.ts b/server/controllers/api/videos/comment.ts index cfdf2773f..e6f28c1cb 100644 --- a/server/controllers/api/videos/comment.ts +++ b/server/controllers/api/videos/comment.ts @@ -166,7 +166,10 @@ async function listVideoThreadComments (req: express.Request, res: express.Respo } if (resultList.data.length === 0) { - return res.sendStatus(HttpStatusCode.NOT_FOUND_404) + return res.fail({ + status: HttpStatusCode.NOT_FOUND_404, + message: 'No comments were found' + }) } return res.json(buildFormattedCommentTree(resultList)) diff --git a/server/controllers/api/videos/import.ts b/server/controllers/api/videos/import.ts index 0d5d7a962..6ee109a8f 100644 --- a/server/controllers/api/videos/import.ts +++ b/server/controllers/api/videos/import.ts @@ -18,7 +18,6 @@ import { } from '@server/types/models' import { MVideoImportFormattable } from '@server/types/models/video/video-import' import { ServerErrorCode, VideoImportCreate, VideoImportState, VideoPrivacy, VideoState } from '../../../../shared' -import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type' import { auditLoggerFactory, getAuditIdFromRes, VideoImportAuditView } from '../../../helpers/audit-logger' import { moveAndProcessCaptionFile } from '../../../helpers/captions-utils' @@ -143,10 +142,12 @@ async function addYoutubeDLImport (req: express.Request, res: express.Response) } catch (err) { logger.info('Cannot fetch information from import for URL %s.', targetUrl, { err }) - return res.status(HttpStatusCode.BAD_REQUEST_400) - .json({ - error: 'Cannot fetch remote information of this URL.' - }) + return res.fail({ + message: 'Cannot fetch remote information of this URL.', + data: { + targetUrl + } + }) } const video = buildVideo(res.locals.videoChannel.id, body, youtubeDLInfo) @@ -333,12 +334,10 @@ async function processTorrentOrAbortRequest (req: express.Request, res: express. if (parsedTorrent.files.length !== 1) { cleanUpReqFiles(req) - res.status(HttpStatusCode.BAD_REQUEST_400) - .json({ - code: ServerErrorCode.INCORRECT_FILES_IN_TORRENT, - error: 'Torrents with only 1 file are supported.' - }) - + res.fail({ + type: ServerErrorCode.INCORRECT_FILES_IN_TORRENT.toString(), + message: 'Torrents with only 1 file are supported.' + }) return undefined } diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts index 6483d2e8a..47ab098ef 100644 --- a/server/controllers/api/videos/index.ts +++ b/server/controllers/api/videos/index.ts @@ -146,7 +146,7 @@ async function viewVideo (req: express.Request, res: express.Response) { const exists = await Redis.Instance.doesVideoIPViewExist(ip, immutableVideoAttrs.uuid) if (exists) { logger.debug('View for ip %s and video %s already exists.', ip, immutableVideoAttrs.uuid) - return res.sendStatus(HttpStatusCode.NO_CONTENT_204) + return res.status(HttpStatusCode.NO_CONTENT_204).end() } const video = await VideoModel.load(immutableVideoAttrs.id) @@ -179,7 +179,7 @@ async function viewVideo (req: express.Request, res: express.Response) { Hooks.runAction('action:api.video.viewed', { video, ip }) - return res.sendStatus(HttpStatusCode.NO_CONTENT_204) + return res.status(HttpStatusCode.NO_CONTENT_204).end() } async function getVideoDescription (req: express.Request, res: express.Response) { diff --git a/server/controllers/api/videos/live.ts b/server/controllers/api/videos/live.ts index 04d2494ce..6b733c577 100644 --- a/server/controllers/api/videos/live.ts +++ b/server/controllers/api/videos/live.ts @@ -76,7 +76,7 @@ async function updateLiveVideo (req: express.Request, res: express.Response) { await federateVideoIfNeeded(video, false) - return res.sendStatus(HttpStatusCode.NO_CONTENT_204) + return res.status(HttpStatusCode.NO_CONTENT_204).end() } async function addLiveVideo (req: express.Request, res: express.Response) { diff --git a/server/controllers/api/videos/ownership.ts b/server/controllers/api/videos/ownership.ts index 6102f28dc..2d6ca60a8 100644 --- a/server/controllers/api/videos/ownership.ts +++ b/server/controllers/api/videos/ownership.ts @@ -122,7 +122,7 @@ function acceptOwnership (req: express.Request, res: express.Response) { videoChangeOwnership.status = VideoChangeOwnershipStatus.ACCEPTED await videoChangeOwnership.save({ transaction: t }) - return res.sendStatus(HttpStatusCode.NO_CONTENT_204) + return res.status(HttpStatusCode.NO_CONTENT_204).end() }) } @@ -133,6 +133,6 @@ function refuseOwnership (req: express.Request, res: express.Response) { videoChangeOwnership.status = VideoChangeOwnershipStatus.REFUSED await videoChangeOwnership.save({ transaction: t }) - return res.sendStatus(HttpStatusCode.NO_CONTENT_204) + return res.status(HttpStatusCode.NO_CONTENT_204).end() }) } diff --git a/server/controllers/api/videos/upload.ts b/server/controllers/api/videos/upload.ts index ebc17c760..c33d7fcb9 100644 --- a/server/controllers/api/videos/upload.ts +++ b/server/controllers/api/videos/upload.ts @@ -97,8 +97,11 @@ export async function addVideoLegacy (req: express.Request, res: express.Respons // Uploading the video could be long // Set timeout to 10 minutes, as Express's default is 2 minutes req.setTimeout(1000 * 60 * 10, () => { - logger.error('Upload video has timed out.') - return res.sendStatus(HttpStatusCode.REQUEST_TIMEOUT_408) + logger.error('Video upload has timed out.') + return res.fail({ + status: HttpStatusCode.REQUEST_TIMEOUT_408, + message: 'Video upload has timed out.' + }) }) const videoPhysicalFile = req.files['videofile'][0] diff --git a/server/controllers/client.ts b/server/controllers/client.ts index fcccc48e0..eb1ee6cbd 100644 --- a/server/controllers/client.ts +++ b/server/controllers/client.ts @@ -78,7 +78,7 @@ clientsRouter.use('/client', express.static(distPath, { maxAge: STATIC_MAX_AGE.C // 404 for static files not found clientsRouter.use('/client/*', (req: express.Request, res: express.Response) => { - res.sendStatus(HttpStatusCode.NOT_FOUND_404) + res.status(HttpStatusCode.NOT_FOUND_404).end() }) // Always serve index client page (the client is a single page application, let it handle routing) @@ -105,7 +105,7 @@ function serveServerTranslations (req: express.Request, res: express.Response) { return res.sendFile(path, { maxAge: STATIC_MAX_AGE.SERVER }) } - return res.sendStatus(HttpStatusCode.NOT_FOUND_404) + return res.status(HttpStatusCode.NOT_FOUND_404).end() } async function generateEmbedHtmlPage (req: express.Request, res: express.Response) { diff --git a/server/controllers/download.ts b/server/controllers/download.ts index 9a8194c5c..4293a32e2 100644 --- a/server/controllers/download.ts +++ b/server/controllers/download.ts @@ -41,7 +41,12 @@ export { async function downloadTorrent (req: express.Request, res: express.Response) { const result = await VideosTorrentCache.Instance.getFilePath(req.params.filename) - if (!result) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) + if (!result) { + return res.fail({ + status: HttpStatusCode.NOT_FOUND_404, + message: 'Torrent file not found' + }) + } const allowParameters = { torrentPath: result.path, downloadName: result.downloadName } @@ -60,7 +65,12 @@ async function downloadVideoFile (req: express.Request, res: express.Response) { const video = res.locals.videoAll const videoFile = getVideoFile(req, video.VideoFiles) - if (!videoFile) return res.status(HttpStatusCode.NOT_FOUND_404).end() + if (!videoFile) { + return res.fail({ + status: HttpStatusCode.NOT_FOUND_404, + message: 'Video file not found' + }) + } const allowParameters = { video, videoFile } @@ -81,7 +91,12 @@ async function downloadHLSVideoFile (req: express.Request, res: express.Response if (!streamingPlaylist) return res.status(HttpStatusCode.NOT_FOUND_404).end const videoFile = getVideoFile(req, streamingPlaylist.VideoFiles) - if (!videoFile) return res.status(HttpStatusCode.NOT_FOUND_404).end() + if (!videoFile) { + return res.fail({ + status: HttpStatusCode.NOT_FOUND_404, + message: 'Video file not found' + }) + } const allowParameters = { video, streamingPlaylist, videoFile } @@ -131,9 +146,11 @@ function isVideoDownloadAllowed (_object: { function checkAllowResult (res: express.Response, allowParameters: any, result?: AllowedResult) { if (!result || result.allowed !== true) { logger.info('Download is not allowed.', { result, allowParameters }) - res.status(HttpStatusCode.FORBIDDEN_403) - .json({ error: result?.errorMessage || 'Refused download' }) + res.fail({ + status: HttpStatusCode.FORBIDDEN_403, + message: result?.errorMessage || 'Refused download' + }) return false } diff --git a/server/controllers/lazy-static.ts b/server/controllers/lazy-static.ts index 25d3b49b4..9f260cef0 100644 --- a/server/controllers/lazy-static.ts +++ b/server/controllers/lazy-static.ts @@ -56,10 +56,10 @@ async function getActorImage (req: express.Request, res: express.Response) { } const image = await ActorImageModel.loadByName(filename) - if (!image) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) + if (!image) return res.status(HttpStatusCode.NOT_FOUND_404).end() if (image.onDisk === false) { - if (!image.fileUrl) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) + if (!image.fileUrl) return res.status(HttpStatusCode.NOT_FOUND_404).end() logger.info('Lazy serve remote actor image %s.', image.fileUrl) @@ -67,7 +67,7 @@ async function getActorImage (req: express.Request, res: express.Response) { await pushActorImageProcessInQueue({ filename: image.filename, fileUrl: image.fileUrl, type: image.type }) } catch (err) { logger.warn('Cannot process remote actor image %s.', image.fileUrl, { err }) - return res.sendStatus(HttpStatusCode.NOT_FOUND_404) + return res.status(HttpStatusCode.NOT_FOUND_404).end() } image.onDisk = true @@ -83,21 +83,21 @@ async function getActorImage (req: express.Request, res: express.Response) { async function getPreview (req: express.Request, res: express.Response) { const result = await VideosPreviewCache.Instance.getFilePath(req.params.filename) - if (!result) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) + if (!result) return res.status(HttpStatusCode.NOT_FOUND_404).end() return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE.LAZY_SERVER }) } async function getVideoCaption (req: express.Request, res: express.Response) { const result = await VideosCaptionCache.Instance.getFilePath(req.params.filename) - if (!result) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) + if (!result) return res.status(HttpStatusCode.NOT_FOUND_404).end() return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE.LAZY_SERVER }) } async function getTorrent (req: express.Request, res: express.Response) { const result = await VideosTorrentCache.Instance.getFilePath(req.params.filename) - if (!result) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) + if (!result) return res.status(HttpStatusCode.NOT_FOUND_404).end() // Torrents still use the old naming convention (video uuid + .torrent) return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE.SERVER }) diff --git a/server/controllers/live.ts b/server/controllers/live.ts index ff48b0e21..cfb4741b7 100644 --- a/server/controllers/live.ts +++ b/server/controllers/live.ts @@ -25,7 +25,7 @@ function getSegmentsSha256 (req: express.Request, res: express.Response) { const result = LiveManager.Instance.getSegmentsSha256(videoUUID) if (!result) { - return res.sendStatus(HttpStatusCode.NOT_FOUND_404) + return res.status(HttpStatusCode.NOT_FOUND_404).end() } return res.json(mapToJSON(result)) diff --git a/server/controllers/plugins.ts b/server/controllers/plugins.ts index 105f51518..7213e3f15 100644 --- a/server/controllers/plugins.ts +++ b/server/controllers/plugins.ts @@ -100,7 +100,7 @@ function getPluginTranslations (req: express.Request, res: express.Response) { return res.json(json) } - return res.sendStatus(HttpStatusCode.NOT_FOUND_404) + return res.status(HttpStatusCode.NOT_FOUND_404).end() } function servePluginStaticDirectory (req: express.Request, res: express.Response) { @@ -110,7 +110,7 @@ function servePluginStaticDirectory (req: express.Request, res: express.Response const [ directory, ...file ] = staticEndpoint.split('/') const staticPath = plugin.staticDirs[directory] - if (!staticPath) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) + if (!staticPath) return res.status(HttpStatusCode.NOT_FOUND_404).end() const filepath = file.join('/') return res.sendFile(join(plugin.path, staticPath, filepath), sendFileOptions) @@ -120,7 +120,7 @@ function servePluginCustomRoutes (req: express.Request, res: express.Response, n const plugin: RegisteredPlugin = res.locals.registeredPlugin const router = PluginManager.Instance.getRouter(plugin.npmName) - if (!router) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) + if (!router) return res.status(HttpStatusCode.NOT_FOUND_404).end() return router(req, res, next) } @@ -130,7 +130,7 @@ function servePluginClientScripts (req: express.Request, res: express.Response) const staticEndpoint = req.params.staticEndpoint const file = plugin.clientScripts[staticEndpoint] - if (!file) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) + if (!file) return res.status(HttpStatusCode.NOT_FOUND_404).end() return res.sendFile(join(plugin.path, staticEndpoint), sendFileOptions) } @@ -140,7 +140,7 @@ function serveThemeCSSDirectory (req: express.Request, res: express.Response) { const staticEndpoint = req.params.staticEndpoint if (plugin.css.includes(staticEndpoint) === false) { - return res.sendStatus(HttpStatusCode.NOT_FOUND_404) + return res.status(HttpStatusCode.NOT_FOUND_404).end() } return res.sendFile(join(plugin.path, staticEndpoint), sendFileOptions) diff --git a/server/controllers/static.ts b/server/controllers/static.ts index 3870ebfe9..52e104346 100644 --- a/server/controllers/static.ts +++ b/server/controllers/static.ts @@ -160,10 +160,9 @@ async function generateNodeinfo (req: express.Request, res: express.Response) { const { totalVideos } = await VideoModel.getStats() const { totalLocalVideoComments } = await VideoCommentModel.getStats() const { totalUsers, totalMonthlyActiveUsers, totalHalfYearActiveUsers } = await UserModel.getStats() - let json = {} if (req.params.version && (req.params.version === '2.0')) { - json = { + const json = { version: '2.0', software: { name: 'peertube', @@ -291,12 +290,14 @@ async function generateNodeinfo (req: express.Request, res: express.Response) { } } as HttpNodeinfoDiasporaSoftwareNsSchema20 res.contentType('application/json; profile="http://nodeinfo.diaspora.software/ns/schema/2.0#"') - } else { - json = { error: 'Nodeinfo schema version not handled' } - res.status(HttpStatusCode.NOT_FOUND_404) + .send(json) + .end() } - return res.send(json).end() + return res.fail({ + status: HttpStatusCode.NOT_FOUND_404, + message: 'Nodeinfo schema version not handled' + }) } function getCup (req: express.Request, res: express.Response, next: express.NextFunction) { diff --git a/server/helpers/custom-validators/video-comments.ts b/server/helpers/custom-validators/video-comments.ts index 8d3ce580e..5c88447ad 100644 --- a/server/helpers/custom-validators/video-comments.ts +++ b/server/helpers/custom-validators/video-comments.ts @@ -16,26 +16,20 @@ async function doesVideoCommentThreadExist (idArg: number | string, video: MVide const videoComment = await VideoCommentModel.loadById(id) if (!videoComment) { - res.status(HttpStatusCode.NOT_FOUND_404) - .json({ error: 'Video comment thread not found' }) - .end() - + res.fail({ + status: HttpStatusCode.NOT_FOUND_404, + message: 'Video comment thread not found' + }) return false } if (videoComment.videoId !== video.id) { - res.status(HttpStatusCode.BAD_REQUEST_400) - .json({ error: 'Video comment is not associated to this video.' }) - .end() - + res.fail({ message: 'Video comment is not associated to this video.' }) return false } if (videoComment.inReplyToCommentId !== null) { - res.status(HttpStatusCode.BAD_REQUEST_400) - .json({ error: 'Video comment is not a thread.' }) - .end() - + res.fail({ message: 'Video comment is not a thread.' }) return false } @@ -48,18 +42,15 @@ async function doesVideoCommentExist (idArg: number | string, video: MVideoId, r const videoComment = await VideoCommentModel.loadByIdAndPopulateVideoAndAccountAndReply(id) if (!videoComment) { - res.status(HttpStatusCode.NOT_FOUND_404) - .json({ error: 'Video comment thread not found' }) - .end() - + res.fail({ + status: HttpStatusCode.NOT_FOUND_404, + message: 'Video comment thread not found' + }) return false } if (videoComment.videoId !== video.id) { - res.status(HttpStatusCode.BAD_REQUEST_400) - .json({ error: 'Video comment is not associated to this video.' }) - .end() - + res.fail({ message: 'Video comment is not associated to this video.' }) return false } @@ -72,14 +63,14 @@ async function doesCommentIdExist (idArg: number | string, res: express.Response const videoComment = await VideoCommentModel.loadByIdAndPopulateVideoAndAccountAndReply(id) if (!videoComment) { - res.status(HttpStatusCode.NOT_FOUND_404) - .json({ error: 'Video comment thread not found' }) - + res.fail({ + status: HttpStatusCode.NOT_FOUND_404, + message: 'Video comment thread not found' + }) return false } res.locals.videoCommentFull = videoComment - return true } diff --git a/server/helpers/custom-validators/video-imports.ts b/server/helpers/custom-validators/video-imports.ts index 0063d3337..3ad7a4648 100644 --- a/server/helpers/custom-validators/video-imports.ts +++ b/server/helpers/custom-validators/video-imports.ts @@ -36,10 +36,10 @@ async function doesVideoImportExist (id: number, res: express.Response) { const videoImport = await VideoImportModel.loadAndPopulateVideo(id) if (!videoImport) { - res.status(HttpStatusCode.NOT_FOUND_404) - .json({ error: 'Video import not found' }) - .end() - + res.fail({ + status: HttpStatusCode.NOT_FOUND_404, + message: 'Video import not found' + }) return false } diff --git a/server/helpers/custom-validators/video-ownership.ts b/server/helpers/custom-validators/video-ownership.ts index ee3cebe10..21a6b7203 100644 --- a/server/helpers/custom-validators/video-ownership.ts +++ b/server/helpers/custom-validators/video-ownership.ts @@ -9,10 +9,10 @@ export async function doesChangeVideoOwnershipExist (idArg: number | string, res const videoChangeOwnership = await VideoChangeOwnershipModel.load(id) if (!videoChangeOwnership) { - res.status(HttpStatusCode.NOT_FOUND_404) - .json({ error: 'Video change ownership not found' }) - .end() - + res.fail({ + status: HttpStatusCode.NOT_FOUND_404, + message: 'Video change ownership not found' + }) return false } @@ -25,8 +25,9 @@ export function checkUserCanTerminateOwnershipChange (user: MUserId, videoChange return true } - res.status(HttpStatusCode.FORBIDDEN_403) - .json({ error: 'Cannot terminate an ownership change of another user' }) - .end() + res.fail({ + status: HttpStatusCode.FORBIDDEN_403, + message: 'Cannot terminate an ownership change of another user' + }) return false } diff --git a/server/helpers/express-utils.ts b/server/helpers/express-utils.ts index 010c6961a..e3ff93cdd 100644 --- a/server/helpers/express-utils.ts +++ b/server/helpers/express-utils.ts @@ -8,6 +8,7 @@ import { isArray } from './custom-validators/misc' import { logger } from './logger' import { deleteFileAndCatch, generateRandomString } from './utils' import { getExtFromMimetype } from './video' +import { ProblemDocument, ProblemDocumentExtension } from 'http-problem-details' function buildNSFWFilter (res?: express.Response, paramNSFW?: string) { if (paramNSFW === 'true') return true @@ -125,6 +126,34 @@ function getCountVideos (req: express.Request) { return req.query.skipCount !== true } +// helpers added in server.ts and used in subsequent controllers used +const apiResponseHelpers = (req, res: express.Response, next = null) => { + res.fail = (options) => { + const { data, status, message, title, type, docs, instance } = { + data: null, + status: HttpStatusCode.BAD_REQUEST_400, + ...options + } + + const extension = new ProblemDocumentExtension({ + ...data, + docs: docs || res.docs + }) + + res.status(status) + res.setHeader('Content-Type', 'application/problem+json') + res.json(new ProblemDocument({ + status, + title, + instance, + type: type && '' + type, + detail: message + }, extension)) + } + + if (next !== null) next() +} + // --------------------------------------------------------------------------- export { @@ -134,5 +163,6 @@ export { badRequest, createReqFiles, cleanUpReqFiles, - getCountVideos + getCountVideos, + apiResponseHelpers } diff --git a/server/helpers/middlewares/abuses.ts b/server/helpers/middlewares/abuses.ts index c53bd9efd..f0b1caba8 100644 --- a/server/helpers/middlewares/abuses.ts +++ b/server/helpers/middlewares/abuses.ts @@ -6,8 +6,10 @@ async function doesAbuseExist (abuseId: number | string, res: Response) { const abuse = await AbuseModel.loadByIdWithReporter(parseInt(abuseId + '', 10)) if (!abuse) { - res.status(HttpStatusCode.NOT_FOUND_404) - .json({ error: 'Abuse not found' }) + res.fail({ + status: HttpStatusCode.NOT_FOUND_404, + message: 'Abuse not found' + }) return false } diff --git a/server/helpers/middlewares/accounts.ts b/server/helpers/middlewares/accounts.ts index 5addd3e1a..7db79bc48 100644 --- a/server/helpers/middlewares/accounts.ts +++ b/server/helpers/middlewares/accounts.ts @@ -27,15 +27,15 @@ async function doesAccountExist (p: Promise, res: Response, sen if (!account) { if (sendNotFound === true) { - res.status(HttpStatusCode.NOT_FOUND_404) - .json({ error: 'Account not found' }) + res.fail({ + status: HttpStatusCode.NOT_FOUND_404, + message: 'Account not found' + }) } - return false } res.locals.account = account - return true } @@ -43,14 +43,14 @@ async function doesUserFeedTokenCorrespond (id: number, token: string, res: Resp const user = await UserModel.loadByIdWithChannels(parseInt(id + '', 10)) if (token !== user.feedToken) { - res.status(HttpStatusCode.FORBIDDEN_403) - .json({ error: 'User and token mismatch' }) - + res.fail({ + status: HttpStatusCode.FORBIDDEN_403, + message: 'User and token mismatch' + }) return false } res.locals.user = user - return true } diff --git a/server/helpers/middlewares/video-blacklists.ts b/server/helpers/middlewares/video-blacklists.ts index eda1324d3..3494fd6b0 100644 --- a/server/helpers/middlewares/video-blacklists.ts +++ b/server/helpers/middlewares/video-blacklists.ts @@ -6,10 +6,10 @@ async function doesVideoBlacklistExist (videoId: number, res: Response) { const videoBlacklist = await VideoBlacklistModel.loadByVideoId(videoId) if (videoBlacklist === null) { - res.status(HttpStatusCode.NOT_FOUND_404) - .json({ error: 'Blacklisted video not found' }) - .end() - + res.fail({ + status: HttpStatusCode.NOT_FOUND_404, + message: 'Blacklisted video not found' + }) return false } diff --git a/server/helpers/middlewares/video-captions.ts b/server/helpers/middlewares/video-captions.ts index 226d3c5f8..2a12c4813 100644 --- a/server/helpers/middlewares/video-captions.ts +++ b/server/helpers/middlewares/video-captions.ts @@ -7,9 +7,10 @@ async function doesVideoCaptionExist (video: MVideoId, language: string, res: Re const videoCaption = await VideoCaptionModel.loadByVideoIdAndLanguage(video.id, language) if (!videoCaption) { - res.status(HttpStatusCode.NOT_FOUND_404) - .json({ error: 'Video caption not found' }) - + res.fail({ + status: HttpStatusCode.NOT_FOUND_404, + message: 'Video caption not found' + }) return false } diff --git a/server/helpers/middlewares/video-channels.ts b/server/helpers/middlewares/video-channels.ts index 602555921..f5ed5ef0f 100644 --- a/server/helpers/middlewares/video-channels.ts +++ b/server/helpers/middlewares/video-channels.ts @@ -31,9 +31,10 @@ export { function processVideoChannelExist (videoChannel: MChannelBannerAccountDefault, res: express.Response) { if (!videoChannel) { - res.status(HttpStatusCode.NOT_FOUND_404) - .json({ error: 'Video channel not found' }) - + res.fail({ + status: HttpStatusCode.NOT_FOUND_404, + message: 'Video channel not found' + }) return false } diff --git a/server/helpers/middlewares/video-playlists.ts b/server/helpers/middlewares/video-playlists.ts index d2dd80a35..3faeab677 100644 --- a/server/helpers/middlewares/video-playlists.ts +++ b/server/helpers/middlewares/video-playlists.ts @@ -28,10 +28,10 @@ export { function handleVideoPlaylist (videoPlaylist: MVideoPlaylist, res: express.Response) { if (!videoPlaylist) { - res.status(HttpStatusCode.NOT_FOUND_404) - .json({ error: 'Video playlist not found' }) - .end() - + res.fail({ + status: HttpStatusCode.NOT_FOUND_404, + message: 'Video playlist not found' + }) return false } diff --git a/server/helpers/middlewares/videos.ts b/server/helpers/middlewares/videos.ts index 403cae092..52b934eb7 100644 --- a/server/helpers/middlewares/videos.ts +++ b/server/helpers/middlewares/videos.ts @@ -21,10 +21,10 @@ async function doesVideoExist (id: number | string, res: Response, fetchType: Vi const video = await fetchVideo(id, fetchType, userId) if (video === null) { - res.status(HttpStatusCode.NOT_FOUND_404) - .json({ error: 'Video not found' }) - .end() - + res.fail({ + status: HttpStatusCode.NOT_FOUND_404, + message: 'Video not found' + }) return false } @@ -55,10 +55,10 @@ async function doesVideoExist (id: number | string, res: Response, fetchType: Vi async function doesVideoFileOfVideoExist (id: number, videoIdOrUUID: number | string, res: Response) { if (!await VideoFileModel.doesVideoExistForVideoFile(id, videoIdOrUUID)) { - res.status(HttpStatusCode.NOT_FOUND_404) - .json({ error: 'VideoFile matching Video not found' }) - .end() - + res.fail({ + status: HttpStatusCode.NOT_FOUND_404, + message: 'VideoFile matching Video not found' + }) return false } @@ -69,9 +69,7 @@ async function doesVideoChannelOfAccountExist (channelId: number, user: MUserAcc const videoChannel = await VideoChannelModel.loadAndPopulateAccount(channelId) if (videoChannel === null) { - res.status(HttpStatusCode.BAD_REQUEST_400) - .json({ error: 'Unknown video "video channel" for this instance.' }) - + res.fail({ message: 'Unknown video "video channel" for this instance.' }) return false } @@ -82,9 +80,9 @@ async function doesVideoChannelOfAccountExist (channelId: number, user: MUserAcc } if (videoChannel.Account.id !== user.Account.id) { - res.status(HttpStatusCode.BAD_REQUEST_400) - .json({ error: 'Unknown video "video channel" for this account.' }) - + res.fail({ + message: 'Unknown video "video channel" for this account.' + }) return false } @@ -95,9 +93,10 @@ async function doesVideoChannelOfAccountExist (channelId: number, user: MUserAcc function checkUserCanManageVideo (user: MUser, video: MVideoAccountLight, right: UserRight, res: Response, onlyOwned = true) { // Retrieve the user who did the request if (onlyOwned && video.isOwned() === false) { - res.status(HttpStatusCode.FORBIDDEN_403) - .json({ error: 'Cannot manage a video of another server.' }) - .end() + res.fail({ + status: HttpStatusCode.FORBIDDEN_403, + message: 'Cannot manage a video of another server.' + }) return false } @@ -106,9 +105,10 @@ function checkUserCanManageVideo (user: MUser, video: MVideoAccountLight, right: // Or if s/he is the video's account const account = video.VideoChannel.Account if (user.hasRight(right) === false && account.userId !== user.id) { - res.status(HttpStatusCode.FORBIDDEN_403) - .json({ error: 'Cannot manage a video of another user.' }) - .end() + res.fail({ + status: HttpStatusCode.FORBIDDEN_403, + message: 'Cannot manage a video of another user.' + }) return false } diff --git a/server/lib/client-html.ts b/server/lib/client-html.ts index 3c09332b5..4068e3d7b 100644 --- a/server/lib/client-html.ts +++ b/server/lib/client-html.ts @@ -549,11 +549,11 @@ async function serveIndexHTML (req: express.Request, res: express.Response) { return } catch (err) { logger.error('Cannot generate HTML page.', err) - return res.sendStatus(HttpStatusCode.INTERNAL_SERVER_ERROR_500) + return res.status(HttpStatusCode.INTERNAL_SERVER_ERROR_500).end() } } - return res.sendStatus(HttpStatusCode.NOT_ACCEPTABLE_406) + return res.status(HttpStatusCode.NOT_ACCEPTABLE_406).end() } // --------------------------------------------------------------------------- diff --git a/server/middlewares/activitypub.ts b/server/middlewares/activitypub.ts index ce94a2129..6cd23f230 100644 --- a/server/middlewares/activitypub.ts +++ b/server/middlewares/activitypub.ts @@ -29,11 +29,14 @@ async function checkSignature (req: Request, res: Response, next: NextFunction) const activity: ActivityDelete = req.body if (isActorDeleteActivityValid(activity) && activity.object === activity.actor) { logger.debug('Handling signature error on actor delete activity', { err }) - return res.sendStatus(HttpStatusCode.NO_CONTENT_204) + return res.status(HttpStatusCode.NO_CONTENT_204).end() } logger.warn('Error in ActivityPub signature checker.', { err }) - return res.sendStatus(HttpStatusCode.FORBIDDEN_403) + return res.fail({ + status: HttpStatusCode.FORBIDDEN_403, + message: 'ActivityPub signature could not be checked' + }) } } @@ -71,13 +74,22 @@ async function checkHttpSignature (req: Request, res: Response) { } catch (err) { logger.warn('Invalid signature because of exception in signature parser', { reqBody: req.body, err }) - res.status(HttpStatusCode.FORBIDDEN_403).json({ error: err.message }) + res.fail({ + status: HttpStatusCode.FORBIDDEN_403, + message: err.message + }) return false } const keyId = parsed.keyId if (!keyId) { - res.sendStatus(HttpStatusCode.FORBIDDEN_403) + res.fail({ + status: HttpStatusCode.FORBIDDEN_403, + message: 'Invalid key ID', + data: { + keyId + } + }) return false } @@ -94,12 +106,17 @@ async function checkHttpSignature (req: Request, res: Response) { if (verified !== true) { logger.warn('Signature from %s is invalid', actorUrl, { parsed }) - res.sendStatus(HttpStatusCode.FORBIDDEN_403) + res.fail({ + status: HttpStatusCode.FORBIDDEN_403, + message: 'Invalid signature', + data: { + actorUrl + } + }) return false } res.locals.signature = { actor } - return true } @@ -107,7 +124,10 @@ async function checkJsonLDSignature (req: Request, res: Response) { const signatureObject: ActivityPubSignature = req.body.signature if (!signatureObject || !signatureObject.creator) { - res.sendStatus(HttpStatusCode.FORBIDDEN_403) + res.fail({ + status: HttpStatusCode.FORBIDDEN_403, + message: 'Object and creator signature do not match' + }) return false } @@ -121,11 +141,13 @@ async function checkJsonLDSignature (req: Request, res: Response) { if (verified !== true) { logger.warn('Signature not verified.', req.body) - res.sendStatus(HttpStatusCode.FORBIDDEN_403) + res.fail({ + status: HttpStatusCode.FORBIDDEN_403, + message: 'Signature could not be verified' + }) return false } res.locals.signature = { actor } - return true } diff --git a/server/middlewares/auth.ts b/server/middlewares/auth.ts index f38373624..176461cc2 100644 --- a/server/middlewares/auth.ts +++ b/server/middlewares/auth.ts @@ -16,11 +16,11 @@ function authenticate (req: express.Request, res: express.Response, next: expres .catch(err => { logger.warn('Cannot authenticate.', { err }) - return res.status(err.status) - .json({ - error: 'Token is invalid.', - code: err.name - }) + return res.fail({ + status: err.status, + message: 'Token is invalid', + type: err.name + }) }) } @@ -52,7 +52,12 @@ function authenticatePromiseIfNeeded (req: express.Request, res: express.Respons // Already authenticated? (or tried to) if (res.locals.oauth?.token.User) return resolve() - if (res.locals.authenticated === false) return res.sendStatus(HttpStatusCode.UNAUTHORIZED_401) + if (res.locals.authenticated === false) { + return res.fail({ + status: HttpStatusCode.UNAUTHORIZED_401, + message: 'Not authenticated' + }) + } authenticate(req, res, () => resolve(), authenticateInQuery) }) diff --git a/server/middlewares/servers.ts b/server/middlewares/servers.ts index 5e1c165f0..9aa56bc93 100644 --- a/server/middlewares/servers.ts +++ b/server/middlewares/servers.ts @@ -10,7 +10,10 @@ function setBodyHostsPort (req: express.Request, res: express.Response, next: ex // Problem with the url parsing? if (hostWithPort === null) { - return res.sendStatus(HttpStatusCode.INTERNAL_SERVER_ERROR_500) + return res.fail({ + status: HttpStatusCode.INTERNAL_SERVER_ERROR_500, + message: 'Could not parse hosts' + }) } req.body.hosts[i] = hostWithPort diff --git a/server/middlewares/user-right.ts b/server/middlewares/user-right.ts index 45dda4781..d1888c2d3 100644 --- a/server/middlewares/user-right.ts +++ b/server/middlewares/user-right.ts @@ -10,8 +10,10 @@ function ensureUserHasRight (userRight: UserRight) { const message = `User ${user.username} does not have right ${userRight} to access to ${req.path}.` logger.info(message) - return res.status(HttpStatusCode.FORBIDDEN_403) - .json({ error: message }) + return res.fail({ + status: HttpStatusCode.FORBIDDEN_403, + message + }) } return next() diff --git a/server/middlewares/validators/abuse.ts b/server/middlewares/validators/abuse.ts index 3b897fdef..7f002e0d5 100644 --- a/server/middlewares/validators/abuse.ts +++ b/server/middlewares/validators/abuse.ts @@ -71,9 +71,7 @@ const abuseReportValidator = [ if (body.comment?.id && !await doesCommentIdExist(body.comment.id, res)) return if (!body.video?.id && !body.account?.id && !body.comment?.id) { - res.status(HttpStatusCode.BAD_REQUEST_400) - .json({ error: 'video id or account id or comment id is required.' }) - + res.fail({ message: 'video id or account id or comment id is required.' }) return } @@ -195,8 +193,10 @@ const getAbuseValidator = [ const message = `User ${user.username} does not have right to get abuse ${abuse.id}` logger.warn(message) - return res.status(HttpStatusCode.FORBIDDEN_403) - .json({ error: message }) + return res.fail({ + status: HttpStatusCode.FORBIDDEN_403, + message + }) } return next() @@ -209,10 +209,7 @@ const checkAbuseValidForMessagesValidator = [ const abuse = res.locals.abuse if (abuse.ReporterAccount.isOwned() === false) { - return res.status(HttpStatusCode.BAD_REQUEST_400) - .json({ - error: 'This abuse was created by a user of your instance.' - }) + return res.fail({ message: 'This abuse was created by a user of your instance.' }) } return next() @@ -246,13 +243,17 @@ const deleteAbuseMessageValidator = [ const abuseMessage = await AbuseMessageModel.loadByIdAndAbuseId(messageId, abuse.id) if (!abuseMessage) { - return res.status(HttpStatusCode.NOT_FOUND_404) - .json({ error: 'Abuse message not found' }) + return res.fail({ + status: HttpStatusCode.NOT_FOUND_404, + message: 'Abuse message not found' + }) } if (user.hasRight(UserRight.MANAGE_ABUSES) !== true && abuseMessage.accountId !== user.Account.id) { - return res.status(HttpStatusCode.FORBIDDEN_403) - .json({ error: 'Cannot delete this abuse message' }) + return res.fail({ + status: HttpStatusCode.FORBIDDEN_403, + message: 'Cannot delete this abuse message' + }) } res.locals.abuseMessage = abuseMessage diff --git a/server/middlewares/validators/activitypub/activity.ts b/server/middlewares/validators/activitypub/activity.ts index e78ef07ef..59355e855 100644 --- a/server/middlewares/validators/activitypub/activity.ts +++ b/server/middlewares/validators/activitypub/activity.ts @@ -10,7 +10,7 @@ async function activityPubValidator (req: express.Request, res: express.Response if (!isRootActivityValid(req.body)) { logger.warn('Incorrect activity parameters.', { activity: req.body }) return res.status(HttpStatusCode.BAD_REQUEST_400) - .json({ error: 'Incorrect activity.' }) + .end() } const serverActor = await getServerActor() diff --git a/server/middlewares/validators/blocklist.ts b/server/middlewares/validators/blocklist.ts index f61811a1a..125ff882c 100644 --- a/server/middlewares/validators/blocklist.ts +++ b/server/middlewares/validators/blocklist.ts @@ -24,9 +24,10 @@ const blockAccountValidator = [ const accountToBlock = res.locals.account if (user.Account.id === accountToBlock.id) { - res.status(HttpStatusCode.CONFLICT_409) - .json({ error: 'You cannot block yourself.' }) - + res.fail({ + status: HttpStatusCode.CONFLICT_409, + message: 'You cannot block yourself.' + }) return } @@ -79,8 +80,10 @@ const blockServerValidator = [ const host: string = req.body.host if (host === WEBSERVER.HOST) { - return res.status(HttpStatusCode.CONFLICT_409) - .json({ error: 'You cannot block your own server.' }) + return res.fail({ + status: HttpStatusCode.CONFLICT_409, + message: 'You cannot block your own server.' + }) } const server = await ServerModel.loadOrCreateByHost(host) @@ -137,27 +140,27 @@ export { async function doesUnblockAccountExist (accountId: number, targetAccountId: number, res: express.Response) { const accountBlock = await AccountBlocklistModel.loadByAccountAndTarget(accountId, targetAccountId) if (!accountBlock) { - res.status(HttpStatusCode.NOT_FOUND_404) - .json({ error: 'Account block entry not found.' }) - + res.fail({ + status: HttpStatusCode.NOT_FOUND_404, + message: 'Account block entry not found.' + }) return false } res.locals.accountBlock = accountBlock - return true } async function doesUnblockServerExist (accountId: number, host: string, res: express.Response) { const serverBlock = await ServerBlocklistModel.loadByAccountAndHost(accountId, host) if (!serverBlock) { - res.status(HttpStatusCode.NOT_FOUND_404) - .json({ error: 'Server block entry not found.' }) - + res.fail({ + status: HttpStatusCode.NOT_FOUND_404, + message: 'Server block entry not found.' + }) return false } res.locals.serverBlock = serverBlock - return true } diff --git a/server/middlewares/validators/bulk.ts b/server/middlewares/validators/bulk.ts index cfb16d352..847885101 100644 --- a/server/middlewares/validators/bulk.ts +++ b/server/middlewares/validators/bulk.ts @@ -23,9 +23,9 @@ const bulkRemoveCommentsOfValidator = [ const body = req.body as BulkRemoveCommentsOfBody if (body.scope === 'instance' && user.hasRight(UserRight.REMOVE_ANY_VIDEO_COMMENT) !== true) { - return res.status(HttpStatusCode.FORBIDDEN_403) - .json({ - error: 'User cannot remove any comments of this instance.' + return res.fail({ + status: HttpStatusCode.FORBIDDEN_403, + message: 'User cannot remove any comments of this instance.' }) } diff --git a/server/middlewares/validators/config.ts b/server/middlewares/validators/config.ts index e3e0c2058..b5d6b4622 100644 --- a/server/middlewares/validators/config.ts +++ b/server/middlewares/validators/config.ts @@ -2,7 +2,6 @@ import * as express from 'express' import { body } from 'express-validator' import { isIntOrNull } from '@server/helpers/custom-validators/misc' import { isEmailEnabled } from '@server/initializers/config' -import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' import { CustomConfig } from '../../../shared/models/server/custom-config.model' import { isThemeNameValid } from '../../helpers/custom-validators/plugins' import { isUserNSFWPolicyValid, isUserVideoQuotaDailyValid, isUserVideoQuotaValid } from '../../helpers/custom-validators/users' @@ -115,9 +114,7 @@ function checkInvalidConfigIfEmailDisabled (customConfig: CustomConfig, res: exp if (isEmailEnabled()) return true if (customConfig.signup.requiresEmailVerification === true) { - res.status(HttpStatusCode.BAD_REQUEST_400) - .send({ error: 'Emailer is disabled but you require signup email verification.' }) - .end() + res.fail({ message: 'Emailer is disabled but you require signup email verification.' }) return false } @@ -128,9 +125,7 @@ function checkInvalidTranscodingConfig (customConfig: CustomConfig, res: express if (customConfig.transcoding.enabled === false) return true if (customConfig.transcoding.webtorrent.enabled === false && customConfig.transcoding.hls.enabled === false) { - res.status(HttpStatusCode.BAD_REQUEST_400) - .send({ error: 'You need to enable at least webtorrent transcoding or hls transcoding' }) - .end() + res.fail({ message: 'You need to enable at least webtorrent transcoding or hls transcoding' }) return false } @@ -141,9 +136,7 @@ function checkInvalidLiveConfig (customConfig: CustomConfig, res: express.Respon if (customConfig.live.enabled === false) return true if (customConfig.live.allowReplay === true && customConfig.transcoding.enabled === false) { - res.status(HttpStatusCode.BAD_REQUEST_400) - .send({ error: 'You cannot allow live replay if transcoding is not enabled' }) - .end() + res.fail({ message: 'You cannot allow live replay if transcoding is not enabled' }) return false } diff --git a/server/middlewares/validators/feeds.ts b/server/middlewares/validators/feeds.ts index 617661813..aa16cc993 100644 --- a/server/middlewares/validators/feeds.ts +++ b/server/middlewares/validators/feeds.ts @@ -36,10 +36,10 @@ function setFeedFormatContentType (req: express.Request, res: express.Response, if (req.accepts(acceptableContentTypes)) { res.set('Content-Type', req.accepts(acceptableContentTypes) as string) } else { - return res.status(HttpStatusCode.NOT_ACCEPTABLE_406) - .json({ - message: `You should accept at least one of the following content-types: ${acceptableContentTypes.join(', ')}` - }) + return res.fail({ + status: HttpStatusCode.NOT_ACCEPTABLE_406, + message: `You should accept at least one of the following content-types: ${acceptableContentTypes.join(', ')}` + }) } return next() @@ -106,10 +106,7 @@ const videoCommentsFeedsValidator = [ if (areValidationErrors(req, res)) return if (req.query.videoId && (req.query.videoChannelId || req.query.videoChannelName)) { - return res.status(HttpStatusCode.BAD_REQUEST_400) - .json({ - message: 'videoId cannot be mixed with a channel filter' - }) + return res.fail({ message: 'videoId cannot be mixed with a channel filter' }) } if (req.query.videoId && !await doesVideoExist(req.query.videoId, res)) return diff --git a/server/middlewares/validators/follows.ts b/server/middlewares/validators/follows.ts index 1d18de8cd..733be379b 100644 --- a/server/middlewares/validators/follows.ts +++ b/server/middlewares/validators/follows.ts @@ -63,11 +63,10 @@ const removeFollowingValidator = [ const follow = await ActorFollowModel.loadByActorAndTargetNameAndHostForAPI(serverActor.id, SERVER_ACTOR_NAME, req.params.host) if (!follow) { - return res - .status(HttpStatusCode.NOT_FOUND_404) - .json({ - error: `Following ${req.params.host} not found.` - }) + return res.fail({ + status: HttpStatusCode.NOT_FOUND_404, + message: `Following ${req.params.host} not found.` + }) } res.locals.follow = follow @@ -95,12 +94,10 @@ const getFollowerValidator = [ } if (!follow) { - return res - .status(HttpStatusCode.NOT_FOUND_404) - .json({ - error: `Follower ${req.params.nameWithHost} not found.` - }) - .end() + return res.fail({ + status: HttpStatusCode.NOT_FOUND_404, + message: `Follower ${req.params.nameWithHost} not found.` + }) } res.locals.follow = follow @@ -114,12 +111,7 @@ const acceptOrRejectFollowerValidator = [ const follow = res.locals.follow if (follow.state !== 'pending') { - return res - .status(HttpStatusCode.BAD_REQUEST_400) - .json({ - error: 'Follow is not in pending state.' - }) - .end() + return res.fail({ message: 'Follow is not in pending state.' }) } return next() diff --git a/server/middlewares/validators/oembed.ts b/server/middlewares/validators/oembed.ts index 165eda6d5..b1d763fbe 100644 --- a/server/middlewares/validators/oembed.ts +++ b/server/middlewares/validators/oembed.ts @@ -51,8 +51,13 @@ const oembedValidator = [ if (areValidationErrors(req, res)) return if (req.query.format !== undefined && req.query.format !== 'json') { - return res.status(HttpStatusCode.NOT_IMPLEMENTED_501) - .json({ error: 'Requested format is not implemented on server.' }) + return res.fail({ + status: HttpStatusCode.NOT_IMPLEMENTED_501, + message: 'Requested format is not implemented on server.', + data: { + format: req.query.format + } + }) } const url = req.query.url as string @@ -65,27 +70,35 @@ const oembedValidator = [ const matches = watchRegex.exec(url) if (startIsOk === false || matches === null) { - return res.status(HttpStatusCode.BAD_REQUEST_400) - .json({ error: 'Invalid url.' }) + return res.fail({ + status: HttpStatusCode.BAD_REQUEST_400, + message: 'Invalid url.', + data: { + url + } + }) } const elementId = matches[1] if (isIdOrUUIDValid(elementId) === false) { - return res.status(HttpStatusCode.BAD_REQUEST_400) - .json({ error: 'Invalid video or playlist id.' }) + return res.fail({ message: 'Invalid video or playlist id.' }) } if (isVideo) { const video = await fetchVideo(elementId, 'all') if (!video) { - return res.status(HttpStatusCode.NOT_FOUND_404) - .json({ error: 'Video not found' }) + return res.fail({ + status: HttpStatusCode.NOT_FOUND_404, + message: 'Video not found' + }) } if (video.privacy !== VideoPrivacy.PUBLIC) { - return res.status(HttpStatusCode.FORBIDDEN_403) - .json({ error: 'Video is not public' }) + return res.fail({ + status: HttpStatusCode.FORBIDDEN_403, + message: 'Video is not public' + }) } res.locals.videoAll = video @@ -96,13 +109,17 @@ const oembedValidator = [ const videoPlaylist = await VideoPlaylistModel.loadWithAccountAndChannelSummary(elementId, undefined) if (!videoPlaylist) { - return res.status(HttpStatusCode.NOT_FOUND_404) - .json({ error: 'Video playlist not found' }) + return res.fail({ + status: HttpStatusCode.NOT_FOUND_404, + message: 'Video playlist not found' + }) } if (videoPlaylist.privacy !== VideoPlaylistPrivacy.PUBLIC) { - return res.status(HttpStatusCode.FORBIDDEN_403) - .json({ error: 'Playlist is not public' }) + return res.fail({ + status: HttpStatusCode.FORBIDDEN_403, + message: 'Playlist is not public' + }) } res.locals.videoPlaylistSummary = videoPlaylist diff --git a/server/middlewares/validators/plugins.ts b/server/middlewares/validators/plugins.ts index 2c47ec5bb..5934a28bc 100644 --- a/server/middlewares/validators/plugins.ts +++ b/server/middlewares/validators/plugins.ts @@ -31,8 +31,18 @@ const getPluginValidator = (pluginType: PluginType, withVersion = true) => { const npmName = PluginModel.buildNpmName(req.params.pluginName, pluginType) const plugin = PluginManager.Instance.getRegisteredPluginOrTheme(npmName) - if (!plugin) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) - if (withVersion && plugin.version !== req.params.pluginVersion) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) + if (!plugin) { + return res.fail({ + status: HttpStatusCode.NOT_FOUND_404, + message: 'No plugin found named ' + npmName + }) + } + if (withVersion && plugin.version !== req.params.pluginVersion) { + return res.fail({ + status: HttpStatusCode.NOT_FOUND_404, + message: 'No plugin found named ' + npmName + ' with version ' + req.params.pluginVersion + }) + } res.locals.registeredPlugin = plugin @@ -50,10 +60,20 @@ const getExternalAuthValidator = [ if (areValidationErrors(req, res)) return const plugin = res.locals.registeredPlugin - if (!plugin.registerHelpers) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) + if (!plugin.registerHelpers) { + return res.fail({ + status: HttpStatusCode.NOT_FOUND_404, + message: 'No registered helpers were found for this plugin' + }) + } const externalAuth = plugin.registerHelpers.getExternalAuths().find(a => a.authName === req.params.authName) - if (!externalAuth) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) + if (!externalAuth) { + return res.fail({ + status: HttpStatusCode.NOT_FOUND_404, + message: 'No external auths were found for this plugin' + }) + } res.locals.externalAuth = externalAuth @@ -107,8 +127,7 @@ const installOrUpdatePluginValidator = [ const body: InstallOrUpdatePlugin = req.body if (!body.path && !body.npmName) { - return res.status(HttpStatusCode.BAD_REQUEST_400) - .json({ error: 'Should have either a npmName or a path' }) + return res.fail({ message: 'Should have either a npmName or a path' }) } return next() @@ -137,12 +156,13 @@ const existingPluginValidator = [ const plugin = await PluginModel.loadByNpmName(req.params.npmName) if (!plugin) { - return res.status(HttpStatusCode.NOT_FOUND_404) - .json({ error: 'Plugin not found' }) + return res.fail({ + status: HttpStatusCode.NOT_FOUND_404, + message: 'Plugin not found' + }) } res.locals.plugin = plugin - return next() } ] @@ -177,9 +197,7 @@ const listAvailablePluginsValidator = [ if (areValidationErrors(req, res)) return if (CONFIG.PLUGINS.INDEX.ENABLED === false) { - return res.status(HttpStatusCode.BAD_REQUEST_400) - .json({ error: 'Plugin index is not enabled' }) - .end() + return res.fail({ message: 'Plugin index is not enabled' }) } return next() diff --git a/server/middlewares/validators/redundancy.ts b/server/middlewares/validators/redundancy.ts index c379aebe4..3d557048a 100644 --- a/server/middlewares/validators/redundancy.ts +++ b/server/middlewares/validators/redundancy.ts @@ -35,11 +35,21 @@ const videoFileRedundancyGetValidator = [ return f.resolution === paramResolution && (!req.params.fps || paramFPS) }) - if (!videoFile) return res.status(HttpStatusCode.NOT_FOUND_404).json({ error: 'Video file not found.' }) + if (!videoFile) { + return res.fail({ + status: HttpStatusCode.NOT_FOUND_404, + message: 'Video file not found.' + }) + } res.locals.videoFile = videoFile const videoRedundancy = await VideoRedundancyModel.loadLocalByFileId(videoFile.id) - if (!videoRedundancy) return res.status(HttpStatusCode.NOT_FOUND_404).json({ error: 'Video redundancy not found.' }) + if (!videoRedundancy) { + return res.fail({ + status: HttpStatusCode.NOT_FOUND_404, + message: 'Video redundancy not found.' + }) + } res.locals.videoRedundancy = videoRedundancy return next() @@ -65,11 +75,21 @@ const videoPlaylistRedundancyGetValidator = [ const paramPlaylistType = req.params.streamingPlaylistType as unknown as number // We casted to int above const videoStreamingPlaylist = video.VideoStreamingPlaylists.find(p => p.type === paramPlaylistType) - if (!videoStreamingPlaylist) return res.status(HttpStatusCode.NOT_FOUND_404).json({ error: 'Video playlist not found.' }) + if (!videoStreamingPlaylist) { + return res.fail({ + status: HttpStatusCode.NOT_FOUND_404, + message: 'Video playlist not found.' + }) + } res.locals.videoStreamingPlaylist = videoStreamingPlaylist const videoRedundancy = await VideoRedundancyModel.loadLocalByStreamingPlaylistId(videoStreamingPlaylist.id) - if (!videoRedundancy) return res.status(HttpStatusCode.NOT_FOUND_404).json({ error: 'Video redundancy not found.' }) + if (!videoRedundancy) { + return res.fail({ + status: HttpStatusCode.NOT_FOUND_404, + message: 'Video redundancy not found.' + }) + } res.locals.videoRedundancy = videoRedundancy return next() @@ -90,12 +110,10 @@ const updateServerRedundancyValidator = [ const server = await ServerModel.loadByHost(req.params.host) if (!server) { - return res - .status(HttpStatusCode.NOT_FOUND_404) - .json({ - error: `Server ${req.params.host} not found.` - }) - .end() + return res.fail({ + status: HttpStatusCode.NOT_FOUND_404, + message: `Server ${req.params.host} not found.` + }) } res.locals.server = server @@ -129,19 +147,19 @@ const addVideoRedundancyValidator = [ if (!await doesVideoExist(req.body.videoId, res, 'only-video')) return if (res.locals.onlyVideo.remote === false) { - return res.status(HttpStatusCode.BAD_REQUEST_400) - .json({ error: 'Cannot create a redundancy on a local video' }) + return res.fail({ message: 'Cannot create a redundancy on a local video' }) } if (res.locals.onlyVideo.isLive) { - return res.status(HttpStatusCode.BAD_REQUEST_400) - .json({ error: 'Cannot create a redundancy of a live video' }) + return res.fail({ message: 'Cannot create a redundancy of a live video' }) } const alreadyExists = await VideoRedundancyModel.isLocalByVideoUUIDExists(res.locals.onlyVideo.uuid) if (alreadyExists) { - return res.status(HttpStatusCode.CONFLICT_409) - .json({ error: 'This video is already duplicated by your instance.' }) + return res.fail({ + status: HttpStatusCode.CONFLICT_409, + message: 'This video is already duplicated by your instance.' + }) } return next() @@ -160,9 +178,10 @@ const removeVideoRedundancyValidator = [ const redundancy = await VideoRedundancyModel.loadByIdWithVideo(parseInt(req.params.redundancyId, 10)) if (!redundancy) { - return res.status(HttpStatusCode.NOT_FOUND_404) - .json({ error: 'Video redundancy not found' }) - .end() + return res.fail({ + status: HttpStatusCode.NOT_FOUND_404, + message: 'Video redundancy not found' + }) } res.locals.videoRedundancy = redundancy diff --git a/server/middlewares/validators/server.ts b/server/middlewares/validators/server.ts index fe6704716..2b34c4a76 100644 --- a/server/middlewares/validators/server.ts +++ b/server/middlewares/validators/server.ts @@ -19,9 +19,10 @@ const serverGetValidator = [ const server = await ServerModel.loadByHost(req.body.host) if (!server) { - return res.status(HttpStatusCode.NOT_FOUND_404) - .send({ error: 'Server host not found.' }) - .end() + return res.fail({ + status: HttpStatusCode.NOT_FOUND_404, + message: 'Server host not found.' + }) } res.locals.server = server @@ -44,26 +45,26 @@ const contactAdministratorValidator = [ if (areValidationErrors(req, res)) return if (CONFIG.CONTACT_FORM.ENABLED === false) { - return res - .status(HttpStatusCode.CONFLICT_409) - .send({ error: 'Contact form is not enabled on this instance.' }) - .end() + return res.fail({ + status: HttpStatusCode.CONFLICT_409, + message: 'Contact form is not enabled on this instance.' + }) } if (isEmailEnabled() === false) { - return res - .status(HttpStatusCode.CONFLICT_409) - .send({ error: 'Emailer is not enabled on this instance.' }) - .end() + return res.fail({ + status: HttpStatusCode.CONFLICT_409, + message: 'Emailer is not enabled on this instance.' + }) } if (await Redis.Instance.doesContactFormIpExist(req.ip)) { logger.info('Refusing a contact form by %s: already sent one recently.', req.ip) - return res - .status(HttpStatusCode.FORBIDDEN_403) - .send({ error: 'You already sent a contact form recently.' }) - .end() + return res.fail({ + status: HttpStatusCode.FORBIDDEN_403, + message: 'You already sent a contact form recently.' + }) } return next() diff --git a/server/middlewares/validators/themes.ts b/server/middlewares/validators/themes.ts index a726a567b..91ec0d7ac 100644 --- a/server/middlewares/validators/themes.ts +++ b/server/middlewares/validators/themes.ts @@ -20,11 +20,17 @@ const serveThemeCSSValidator = [ const theme = PluginManager.Instance.getRegisteredThemeByShortName(req.params.themeName) if (!theme || theme.version !== req.params.themeVersion) { - return res.sendStatus(HttpStatusCode.NOT_FOUND_404) + return res.fail({ + status: HttpStatusCode.NOT_FOUND_404, + message: 'No theme named ' + req.params.themeName + ' was found with version ' + req.params.themeVersion + }) } if (theme.css.includes(req.params.staticEndpoint) === false) { - return res.sendStatus(HttpStatusCode.NOT_FOUND_404) + return res.fail({ + status: HttpStatusCode.NOT_FOUND_404, + message: 'No static endpoint was found for this theme' + }) } res.locals.registeredPlugin = theme diff --git a/server/middlewares/validators/user-subscriptions.ts b/server/middlewares/validators/user-subscriptions.ts index 1823892b6..5f928b05b 100644 --- a/server/middlewares/validators/user-subscriptions.ts +++ b/server/middlewares/validators/user-subscriptions.ts @@ -61,11 +61,10 @@ const userSubscriptionGetValidator = [ const subscription = await ActorFollowModel.loadByActorAndTargetNameAndHostForAPI(user.Account.Actor.id, name, host) if (!subscription || !subscription.ActorFollowing.VideoChannel) { - return res - .status(HttpStatusCode.NOT_FOUND_404) - .json({ - error: `Subscription ${req.params.uri} not found.` - }) + return res.fail({ + status: HttpStatusCode.NOT_FOUND_404, + message: `Subscription ${req.params.uri} not found.` + }) } res.locals.subscription = subscription diff --git a/server/middlewares/validators/users.ts b/server/middlewares/validators/users.ts index 548d5df4d..0eb9172c4 100644 --- a/server/middlewares/validators/users.ts +++ b/server/middlewares/validators/users.ts @@ -73,23 +73,23 @@ const usersAddValidator = [ const authUser = res.locals.oauth.token.User if (authUser.role !== UserRole.ADMINISTRATOR && req.body.role !== UserRole.USER) { - return res - .status(HttpStatusCode.FORBIDDEN_403) - .json({ error: 'You can only create users (and not administrators or moderators)' }) + return res.fail({ + status: HttpStatusCode.FORBIDDEN_403, + message: 'You can only create users (and not administrators or moderators)' + }) } if (req.body.channelName) { if (req.body.channelName === req.body.username) { - return res - .status(HttpStatusCode.BAD_REQUEST_400) - .json({ error: 'Channel name cannot be the same as user username.' }) + return res.fail({ message: 'Channel name cannot be the same as user username.' }) } const existing = await ActorModel.loadLocalByName(req.body.channelName) if (existing) { - return res - .status(HttpStatusCode.CONFLICT_409) - .json({ error: `Channel with name ${req.body.channelName} already exists.` }) + return res.fail({ + status: HttpStatusCode.CONFLICT_409, + message: `Channel with name ${req.body.channelName} already exists.` + }) } } @@ -121,20 +121,19 @@ const usersRegisterValidator = [ const body: UserRegister = req.body if (body.channel) { if (!body.channel.name || !body.channel.displayName) { - return res - .status(HttpStatusCode.BAD_REQUEST_400) - .json({ error: 'Channel is optional but if you specify it, channel.name and channel.displayName are required.' }) + return res.fail({ message: 'Channel is optional but if you specify it, channel.name and channel.displayName are required.' }) } if (body.channel.name === body.username) { - return res.status(HttpStatusCode.BAD_REQUEST_400) - .json({ error: 'Channel name cannot be the same as user username.' }) + return res.fail({ message: 'Channel name cannot be the same as user username.' }) } const existing = await ActorModel.loadLocalByName(body.channel.name) if (existing) { - return res.status(HttpStatusCode.CONFLICT_409) - .json({ error: `Channel with name ${body.channel.name} already exists.` }) + return res.fail({ + status: HttpStatusCode.CONFLICT_409, + message: `Channel with name ${body.channel.name} already exists.` + }) } } @@ -153,8 +152,7 @@ const usersRemoveValidator = [ const user = res.locals.user if (user.username === 'root') { - return res.status(HttpStatusCode.BAD_REQUEST_400) - .json({ error: 'Cannot remove the root user' }) + return res.fail({ message: 'Cannot remove the root user' }) } return next() @@ -173,8 +171,7 @@ const usersBlockingValidator = [ const user = res.locals.user if (user.username === 'root') { - return res.status(HttpStatusCode.BAD_REQUEST_400) - .json({ error: 'Cannot block the root user' }) + return res.fail({ message: 'Cannot block the root user' }) } return next() @@ -185,9 +182,7 @@ const deleteMeValidator = [ (req: express.Request, res: express.Response, next: express.NextFunction) => { const user = res.locals.oauth.token.User if (user.username === 'root') { - return res.status(HttpStatusCode.BAD_REQUEST_400) - .json({ error: 'You cannot delete your root account.' }) - .end() + return res.fail({ message: 'You cannot delete your root account.' }) } return next() @@ -217,8 +212,7 @@ const usersUpdateValidator = [ const user = res.locals.user if (user.username === 'root' && req.body.role !== undefined && user.role !== req.body.role) { - return res.status(HttpStatusCode.BAD_REQUEST_400) - .json({ error: 'Cannot change root role.' }) + return res.fail({ message: 'Cannot change root role.' }) } return next() @@ -273,18 +267,18 @@ const usersUpdateMeValidator = [ if (req.body.password || req.body.email) { if (user.pluginAuth !== null) { - return res.status(HttpStatusCode.BAD_REQUEST_400) - .json({ error: 'You cannot update your email or password that is associated with an external auth system.' }) + return res.fail({ message: 'You cannot update your email or password that is associated with an external auth system.' }) } if (!req.body.currentPassword) { - return res.status(HttpStatusCode.BAD_REQUEST_400) - .json({ error: 'currentPassword parameter is missing.' }) + return res.fail({ message: 'currentPassword parameter is missing.' }) } if (await user.isPasswordMatch(req.body.currentPassword) !== true) { - return res.status(HttpStatusCode.UNAUTHORIZED_401) - .json({ error: 'currentPassword is invalid.' }) + return res.fail({ + status: HttpStatusCode.UNAUTHORIZED_401, + message: 'currentPassword is invalid.' + }) } } @@ -335,8 +329,10 @@ const ensureUserRegistrationAllowed = [ ) if (allowedResult.allowed === false) { - return res.status(HttpStatusCode.FORBIDDEN_403) - .json({ error: allowedResult.errorMessage || 'User registration is not enabled or user limit is reached.' }) + return res.fail({ + status: HttpStatusCode.FORBIDDEN_403, + message: allowedResult.errorMessage || 'User registration is not enabled or user limit is reached.' + }) } return next() @@ -348,8 +344,10 @@ const ensureUserRegistrationAllowedForIP = [ const allowed = isSignupAllowedForCurrentIP(req.ip) if (allowed === false) { - return res.status(HttpStatusCode.FORBIDDEN_403) - .json({ error: 'You are not on a network authorized for registration.' }) + return res.fail({ + status: HttpStatusCode.FORBIDDEN_403, + message: 'You are not on a network authorized for registration.' + }) } return next() @@ -390,9 +388,10 @@ const usersResetPasswordValidator = [ const redisVerificationString = await Redis.Instance.getResetPasswordLink(user.id) if (redisVerificationString !== req.body.verificationString) { - return res - .status(HttpStatusCode.FORBIDDEN_403) - .json({ error: 'Invalid verification string.' }) + return res.fail({ + status: HttpStatusCode.FORBIDDEN_403, + message: 'Invalid verification string.' + }) } return next() @@ -437,9 +436,10 @@ const usersVerifyEmailValidator = [ const redisVerificationString = await Redis.Instance.getVerifyEmailLink(user.id) if (redisVerificationString !== req.body.verificationString) { - return res - .status(HttpStatusCode.FORBIDDEN_403) - .json({ error: 'Invalid verification string.' }) + return res.fail({ + status: HttpStatusCode.FORBIDDEN_403, + message: 'Invalid verification string.' + }) } return next() @@ -455,8 +455,10 @@ const ensureAuthUserOwnsAccountValidator = [ const user = res.locals.oauth.token.User if (res.locals.account.id !== user.Account.id) { - return res.status(HttpStatusCode.FORBIDDEN_403) - .json({ error: 'Only owner can access ratings list.' }) + return res.fail({ + status: HttpStatusCode.FORBIDDEN_403, + message: 'Only owner can access ratings list.' + }) } return next() @@ -471,8 +473,10 @@ const ensureCanManageUser = [ if (authUser.role === UserRole.ADMINISTRATOR) return next() if (authUser.role === UserRole.MODERATOR && onUser.role === UserRole.USER) return next() - return res.status(HttpStatusCode.FORBIDDEN_403) - .json({ error: 'A moderator can only manager users.' }) + return res.fail({ + status: HttpStatusCode.FORBIDDEN_403, + message: 'A moderator can only manager users.' + }) } ] @@ -515,15 +519,19 @@ async function checkUserNameOrEmailDoesNotAlreadyExist (username: string, email: const user = await UserModel.loadByUsernameOrEmail(username, email) if (user) { - res.status(HttpStatusCode.CONFLICT_409) - .json({ error: 'User with this username or email already exists.' }) + res.fail({ + status: HttpStatusCode.CONFLICT_409, + message: 'User with this username or email already exists.' + }) return false } const actor = await ActorModel.loadLocalByName(username) if (actor) { - res.status(HttpStatusCode.CONFLICT_409) - .json({ error: 'Another actor (account/channel) with this name on this instance already exists or has already existed.' }) + res.fail({ + status: HttpStatusCode.CONFLICT_409, + message: 'Another actor (account/channel) with this name on this instance already exists or has already existed.' + }) return false } @@ -535,14 +543,15 @@ async function checkUserExist (finder: () => Promise, res: express if (!user) { if (abortResponse === true) { - res.status(HttpStatusCode.NOT_FOUND_404) - .json({ error: 'User not found' }) + res.fail({ + status: HttpStatusCode.NOT_FOUND_404, + message: 'User not found' + }) } return false } res.locals.user = user - return true } diff --git a/server/middlewares/validators/utils.ts b/server/middlewares/validators/utils.ts index 4167f6d43..e291f1b17 100644 --- a/server/middlewares/validators/utils.ts +++ b/server/middlewares/validators/utils.ts @@ -1,15 +1,19 @@ import * as express from 'express' import { query, validationResult } from 'express-validator' import { logger } from '../../helpers/logger' -import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' function areValidationErrors (req: express.Request, res: express.Response) { const errors = validationResult(req) if (!errors.isEmpty()) { logger.warn('Incorrect request parameters', { path: req.originalUrl, err: errors.mapped() }) - res.status(HttpStatusCode.BAD_REQUEST_400) - .json({ errors: errors.mapped() }) + res.fail({ + message: 'Incorrect request parameters: ' + Object.keys(errors.mapped()).join(', '), + instance: req.originalUrl, + data: { + 'invalid-params': errors.mapped() + } + }) return true } diff --git a/server/middlewares/validators/videos/video-blacklist.ts b/server/middlewares/validators/videos/video-blacklist.ts index 88c788a43..65132a09f 100644 --- a/server/middlewares/validators/videos/video-blacklist.ts +++ b/server/middlewares/validators/videos/video-blacklist.ts @@ -39,10 +39,10 @@ const videosBlacklistAddValidator = [ const video = res.locals.videoAll if (req.body.unfederate === true && video.remote === true) { - return res - .status(HttpStatusCode.CONFLICT_409) - .send({ error: 'You cannot unfederate a remote video.' }) - .end() + return res.fail({ + status: HttpStatusCode.CONFLICT_409, + message: 'You cannot unfederate a remote video.' + }) } return next() diff --git a/server/middlewares/validators/videos/video-channels.ts b/server/middlewares/validators/videos/video-channels.ts index e881f0d3e..331a51007 100644 --- a/server/middlewares/validators/videos/video-channels.ts +++ b/server/middlewares/validators/videos/video-channels.ts @@ -30,17 +30,16 @@ const videoChannelsAddValidator = [ const actor = await ActorModel.loadLocalByName(req.body.name) if (actor) { - res.status(HttpStatusCode.CONFLICT_409) - .send({ error: 'Another actor (account/channel) with this name on this instance already exists or has already existed.' }) - .end() + res.fail({ + status: HttpStatusCode.CONFLICT_409, + message: 'Another actor (account/channel) with this name on this instance already exists or has already existed.' + }) return false } const count = await VideoChannelModel.countByAccount(res.locals.oauth.token.User.Account.id) if (count >= VIDEO_CHANNELS.MAX_PER_USER) { - res.status(HttpStatusCode.BAD_REQUEST_400) - .send({ error: `You cannot create more than ${VIDEO_CHANNELS.MAX_PER_USER} channels` }) - .end() + res.fail({ message: `You cannot create more than ${VIDEO_CHANNELS.MAX_PER_USER} channels` }) return false } @@ -71,13 +70,17 @@ const videoChannelsUpdateValidator = [ // We need to make additional checks if (res.locals.videoChannel.Actor.isOwned() === false) { - return res.status(HttpStatusCode.FORBIDDEN_403) - .json({ error: 'Cannot update video channel of another server' }) + return res.fail({ + status: HttpStatusCode.FORBIDDEN_403, + message: 'Cannot update video channel of another server' + }) } if (res.locals.videoChannel.Account.userId !== res.locals.oauth.token.User.id) { - return res.status(HttpStatusCode.FORBIDDEN_403) - .json({ error: 'Cannot update video channel of another user' }) + return res.fail({ + status: HttpStatusCode.FORBIDDEN_403, + message: 'Cannot update video channel of another user' + }) } return next() @@ -154,10 +157,10 @@ export { function checkUserCanDeleteVideoChannel (user: MUser, videoChannel: MChannelAccountDefault, res: express.Response) { if (videoChannel.Actor.isOwned() === false) { - res.status(HttpStatusCode.FORBIDDEN_403) - .json({ error: 'Cannot remove video channel of another server.' }) - .end() - + res.fail({ + status: HttpStatusCode.FORBIDDEN_403, + message: 'Cannot remove video channel of another server.' + }) return false } @@ -165,10 +168,10 @@ function checkUserCanDeleteVideoChannel (user: MUser, videoChannel: MChannelAcco // The user can delete it if s/he is an admin // Or if s/he is the video channel's account if (user.hasRight(UserRight.REMOVE_ANY_VIDEO_CHANNEL) === false && videoChannel.Account.userId !== user.id) { - res.status(HttpStatusCode.FORBIDDEN_403) - .json({ error: 'Cannot remove video channel of another user' }) - .end() - + res.fail({ + status: HttpStatusCode.FORBIDDEN_403, + message: 'Cannot remove video channel of another user' + }) return false } @@ -179,10 +182,10 @@ async function checkVideoChannelIsNotTheLastOne (res: express.Response) { const count = await VideoChannelModel.countByAccount(res.locals.oauth.token.User.Account.id) if (count <= 1) { - res.status(HttpStatusCode.CONFLICT_409) - .json({ error: 'Cannot remove the last channel of this user' }) - .end() - + res.fail({ + status: HttpStatusCode.CONFLICT_409, + message: 'Cannot remove the last channel of this user' + }) return false } diff --git a/server/middlewares/validators/videos/video-comments.ts b/server/middlewares/validators/videos/video-comments.ts index 1afacfed8..aac25a787 100644 --- a/server/middlewares/validators/videos/video-comments.ts +++ b/server/middlewares/validators/videos/video-comments.ts @@ -155,9 +155,10 @@ export { function isVideoCommentsEnabled (video: MVideo, res: express.Response) { if (video.commentsEnabled !== true) { - res.status(HttpStatusCode.CONFLICT_409) - .json({ error: 'Video comments are disabled for this video.' }) - + res.fail({ + status: HttpStatusCode.CONFLICT_409, + message: 'Video comments are disabled for this video.' + }) return false } @@ -166,9 +167,10 @@ function isVideoCommentsEnabled (video: MVideo, res: express.Response) { function checkUserCanDeleteVideoComment (user: MUserAccountUrl, videoComment: MCommentOwnerVideoReply, res: express.Response) { if (videoComment.isDeleted()) { - res.status(HttpStatusCode.CONFLICT_409) - .json({ error: 'This comment is already deleted' }) - + res.fail({ + status: HttpStatusCode.CONFLICT_409, + message: 'This comment is already deleted' + }) return false } @@ -179,9 +181,10 @@ function checkUserCanDeleteVideoComment (user: MUserAccountUrl, videoComment: MC videoComment.accountId !== userAccount.id && // Not the comment owner videoComment.Video.VideoChannel.accountId !== userAccount.id // Not the video owner ) { - res.status(HttpStatusCode.FORBIDDEN_403) - .json({ error: 'Cannot remove video comment of another user' }) - + res.fail({ + status: HttpStatusCode.FORBIDDEN_403, + message: 'Cannot remove video comment of another user' + }) return false } @@ -215,9 +218,11 @@ async function isVideoCommentAccepted (req: express.Request, res: express.Respon if (!acceptedResult || acceptedResult.accepted !== true) { logger.info('Refused local comment.', { acceptedResult, acceptParameters }) - res.status(HttpStatusCode.FORBIDDEN_403) - .json({ error: acceptedResult?.errorMessage || 'Refused local comment' }) + res.fail({ + status: HttpStatusCode.FORBIDDEN_403, + message: acceptedResult?.errorMessage || 'Refused local comment' + }) return false } diff --git a/server/middlewares/validators/videos/video-imports.ts b/server/middlewares/validators/videos/video-imports.ts index a5e3ffbcd..55ff09124 100644 --- a/server/middlewares/validators/videos/video-imports.ts +++ b/server/middlewares/validators/videos/video-imports.ts @@ -47,14 +47,20 @@ const videoImportAddValidator = getCommonVideoEditAttributes().concat([ if (CONFIG.IMPORT.VIDEOS.HTTP.ENABLED !== true && req.body.targetUrl) { cleanUpReqFiles(req) - return res.status(HttpStatusCode.CONFLICT_409) - .json({ error: 'HTTP import is not enabled on this instance.' }) + + return res.fail({ + status: HttpStatusCode.CONFLICT_409, + message: 'HTTP import is not enabled on this instance.' + }) } if (CONFIG.IMPORT.VIDEOS.TORRENT.ENABLED !== true && (req.body.magnetUri || torrentFile)) { cleanUpReqFiles(req) - return res.status(HttpStatusCode.CONFLICT_409) - .json({ error: 'Torrent/magnet URI import is not enabled on this instance.' }) + + return res.fail({ + status: HttpStatusCode.CONFLICT_409, + message: 'Torrent/magnet URI import is not enabled on this instance.' + }) } if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req) @@ -63,8 +69,7 @@ const videoImportAddValidator = getCommonVideoEditAttributes().concat([ if (!req.body.targetUrl && !req.body.magnetUri && !torrentFile) { cleanUpReqFiles(req) - return res.status(HttpStatusCode.BAD_REQUEST_400) - .json({ error: 'Should have a magnetUri or a targetUrl or a torrent file.' }) + return res.fail({ message: 'Should have a magnetUri or a targetUrl or a torrent file.' }) } if (!await isImportAccepted(req, res)) return cleanUpReqFiles(req) @@ -100,9 +105,11 @@ async function isImportAccepted (req: express.Request, res: express.Response) { if (!acceptedResult || acceptedResult.accepted !== true) { logger.info('Refused to import video.', { acceptedResult, acceptParameters }) - res.status(HttpStatusCode.FORBIDDEN_403) - .json({ error: acceptedResult.errorMessage || 'Refused to import video' }) + res.fail({ + status: HttpStatusCode.FORBIDDEN_403, + message: acceptedResult.errorMessage || 'Refused to import video' + }) return false } diff --git a/server/middlewares/validators/videos/video-live.ts b/server/middlewares/validators/videos/video-live.ts index ec4c7f32f..9544fa4f5 100644 --- a/server/middlewares/validators/videos/video-live.ts +++ b/server/middlewares/validators/videos/video-live.ts @@ -30,7 +30,12 @@ const videoLiveGetValidator = [ if (!checkUserCanManageVideo(user, res.locals.videoAll, UserRight.GET_ANY_LIVE, res, false)) return const videoLive = await VideoLiveModel.loadByVideoId(res.locals.videoAll.id) - if (!videoLive) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) + if (!videoLive) { + return res.fail({ + status: HttpStatusCode.NOT_FOUND_404, + message: 'Live video not found' + }) + } res.locals.videoLive = videoLive @@ -66,22 +71,25 @@ const videoLiveAddValidator = getCommonVideoEditAttributes().concat([ if (CONFIG.LIVE.ENABLED !== true) { cleanUpReqFiles(req) - return res.status(HttpStatusCode.FORBIDDEN_403) - .json({ error: 'Live is not enabled on this instance' }) + return res.fail({ + status: HttpStatusCode.FORBIDDEN_403, + message: 'Live is not enabled on this instance' + }) } if (CONFIG.LIVE.ALLOW_REPLAY !== true && req.body.saveReplay === true) { cleanUpReqFiles(req) - return res.status(HttpStatusCode.FORBIDDEN_403) - .json({ error: 'Saving live replay is not allowed instance' }) + return res.fail({ + status: HttpStatusCode.FORBIDDEN_403, + message: 'Saving live replay is not allowed instance' + }) } if (req.body.permanentLive && req.body.saveReplay) { cleanUpReqFiles(req) - return res.status(HttpStatusCode.BAD_REQUEST_400) - .json({ error: 'Cannot set this live as permanent while saving its replay' }) + return res.fail({ message: 'Cannot set this live as permanent while saving its replay' }) } const user = res.locals.oauth.token.User @@ -93,11 +101,11 @@ const videoLiveAddValidator = getCommonVideoEditAttributes().concat([ if (totalInstanceLives >= CONFIG.LIVE.MAX_INSTANCE_LIVES) { cleanUpReqFiles(req) - return res.status(HttpStatusCode.FORBIDDEN_403) - .json({ - code: ServerErrorCode.MAX_INSTANCE_LIVES_LIMIT_REACHED, - error: 'Cannot create this live because the max instance lives limit is reached.' - }) + return res.fail({ + status: HttpStatusCode.FORBIDDEN_403, + message: 'Cannot create this live because the max instance lives limit is reached.', + type: ServerErrorCode.MAX_INSTANCE_LIVES_LIMIT_REACHED.toString() + }) } } @@ -107,11 +115,11 @@ const videoLiveAddValidator = getCommonVideoEditAttributes().concat([ if (totalUserLives >= CONFIG.LIVE.MAX_USER_LIVES) { cleanUpReqFiles(req) - return res.status(HttpStatusCode.FORBIDDEN_403) - .json({ - code: ServerErrorCode.MAX_USER_LIVES_LIMIT_REACHED, - error: 'Cannot create this live because the max user lives limit is reached.' - }) + return res.fail({ + status: HttpStatusCode.FORBIDDEN_403, + type: ServerErrorCode.MAX_USER_LIVES_LIMIT_REACHED.toString(), + message: 'Cannot create this live because the max user lives limit is reached.' + }) } } @@ -133,18 +141,18 @@ const videoLiveUpdateValidator = [ if (areValidationErrors(req, res)) return if (req.body.permanentLive && req.body.saveReplay) { - return res.status(HttpStatusCode.BAD_REQUEST_400) - .json({ error: 'Cannot set this live as permanent while saving its replay' }) + return res.fail({ message: 'Cannot set this live as permanent while saving its replay' }) } if (CONFIG.LIVE.ALLOW_REPLAY !== true && req.body.saveReplay === true) { - return res.status(HttpStatusCode.FORBIDDEN_403) - .json({ error: 'Saving live replay is not allowed instance' }) + return res.fail({ + status: HttpStatusCode.FORBIDDEN_403, + message: 'Saving live replay is not allowed instance' + }) } if (res.locals.videoAll.state !== VideoState.WAITING_FOR_LIVE) { - return res.status(HttpStatusCode.BAD_REQUEST_400) - .json({ error: 'Cannot update a live that has already started' }) + return res.fail({ message: 'Cannot update a live that has already started' }) } // Check the user can manage the live @@ -180,9 +188,10 @@ async function isLiveVideoAccepted (req: express.Request, res: express.Response) if (!acceptedResult || acceptedResult.accepted !== true) { logger.info('Refused local live video.', { acceptedResult, acceptParameters }) - res.status(HttpStatusCode.FORBIDDEN_403) - .json({ error: acceptedResult.errorMessage || 'Refused local live video' }) - + res.fail({ + status: HttpStatusCode.FORBIDDEN_403, + message: acceptedResult.errorMessage || 'Refused local live video' + }) return false } diff --git a/server/middlewares/validators/videos/video-playlists.ts b/server/middlewares/validators/videos/video-playlists.ts index c872d045e..90815dd3a 100644 --- a/server/middlewares/validators/videos/video-playlists.ts +++ b/server/middlewares/validators/videos/video-playlists.ts @@ -46,8 +46,8 @@ const videoPlaylistsAddValidator = getCommonPlaylistEditAttributes().concat([ if (body.privacy === VideoPlaylistPrivacy.PUBLIC && !body.videoChannelId) { cleanUpReqFiles(req) - return res.status(HttpStatusCode.BAD_REQUEST_400) - .json({ error: 'Cannot set "public" a playlist that is not assigned to a channel.' }) + + return res.fail({ message: 'Cannot set "public" a playlist that is not assigned to a channel.' }) } return next() @@ -85,14 +85,14 @@ const videoPlaylistsUpdateValidator = getCommonPlaylistEditAttributes().concat([ ) ) { cleanUpReqFiles(req) - return res.status(HttpStatusCode.BAD_REQUEST_400) - .json({ error: 'Cannot set "public" a playlist that is not assigned to a channel.' }) + + return res.fail({ message: 'Cannot set "public" a playlist that is not assigned to a channel.' }) } if (videoPlaylist.type === VideoPlaylistType.WATCH_LATER) { cleanUpReqFiles(req) - return res.status(HttpStatusCode.BAD_REQUEST_400) - .json({ error: 'Cannot update a watch later playlist.' }) + + return res.fail({ message: 'Cannot update a watch later playlist.' }) } if (body.videoChannelId && !await doesVideoChannelIdExist(body.videoChannelId, res)) return cleanUpReqFiles(req) @@ -114,8 +114,7 @@ const videoPlaylistsDeleteValidator = [ const videoPlaylist = getPlaylist(res) if (videoPlaylist.type === VideoPlaylistType.WATCH_LATER) { - return res.status(HttpStatusCode.BAD_REQUEST_400) - .json({ error: 'Cannot delete a watch later playlist.' }) + return res.fail({ message: 'Cannot delete a watch later playlist.' }) } if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, videoPlaylist, UserRight.REMOVE_ANY_VIDEO_PLAYLIST, res)) { @@ -144,7 +143,10 @@ const videoPlaylistsGetValidator = (fetchType: VideoPlaylistFetchType) => { if (videoPlaylist.privacy === VideoPlaylistPrivacy.UNLISTED) { if (isUUIDValid(req.params.playlistId)) return next() - return res.status(HttpStatusCode.NOT_FOUND_404).end() + return res.fail({ + status: HttpStatusCode.NOT_FOUND_404, + message: 'Playlist not found' + }) } if (videoPlaylist.privacy === VideoPlaylistPrivacy.PRIVATE) { @@ -156,8 +158,10 @@ const videoPlaylistsGetValidator = (fetchType: VideoPlaylistFetchType) => { !user || (videoPlaylist.OwnerAccount.id !== user.Account.id && !user.hasRight(UserRight.UPDATE_ANY_VIDEO_PLAYLIST)) ) { - return res.status(HttpStatusCode.FORBIDDEN_403) - .json({ error: 'Cannot get this private video playlist.' }) + return res.fail({ + status: HttpStatusCode.FORBIDDEN_403, + message: 'Cannot get this private video playlist.' + }) } return next() @@ -233,10 +237,10 @@ const videoPlaylistsUpdateOrRemoveVideoValidator = [ const videoPlaylistElement = await VideoPlaylistElementModel.loadById(req.params.playlistElementId) if (!videoPlaylistElement) { - res.status(HttpStatusCode.NOT_FOUND_404) - .json({ error: 'Video playlist element not found' }) - .end() - + res.fail({ + status: HttpStatusCode.NOT_FOUND_404, + message: 'Video playlist element not found' + }) return } res.locals.videoPlaylistElement = videoPlaylistElement @@ -263,15 +267,18 @@ const videoPlaylistElementAPGetValidator = [ const videoPlaylistElement = await VideoPlaylistElementModel.loadByPlaylistAndElementIdForAP(playlistId, playlistElementId) if (!videoPlaylistElement) { - res.status(HttpStatusCode.NOT_FOUND_404) - .json({ error: 'Video playlist element not found' }) - .end() - + res.fail({ + status: HttpStatusCode.NOT_FOUND_404, + message: 'Video playlist element not found' + }) return } if (videoPlaylistElement.VideoPlaylist.privacy === VideoPlaylistPrivacy.PRIVATE) { - return res.status(HttpStatusCode.FORBIDDEN_403).end() + return res.fail({ + status: HttpStatusCode.FORBIDDEN_403, + message: 'Cannot get this private video playlist.' + }) } res.locals.videoPlaylistElementAP = videoPlaylistElement @@ -307,18 +314,12 @@ const videoPlaylistsReorderVideosValidator = [ const reorderLength: number = req.body.reorderLength if (startPosition >= nextPosition || insertAfterPosition >= nextPosition) { - res.status(HttpStatusCode.BAD_REQUEST_400) - .json({ error: `Start position or insert after position exceed the playlist limits (max: ${nextPosition - 1})` }) - .end() - + res.fail({ message: `Start position or insert after position exceed the playlist limits (max: ${nextPosition - 1})` }) return } if (reorderLength && reorderLength + startPosition > nextPosition) { - res.status(HttpStatusCode.BAD_REQUEST_400) - .json({ error: `Reorder length with this start position exceeds the playlist limits (max: ${nextPosition - startPosition})` }) - .end() - + res.fail({ message: `Reorder length with this start position exceeds the playlist limits (max: ${nextPosition - startPosition})` }) return } @@ -401,10 +402,10 @@ function getCommonPlaylistEditAttributes () { function checkUserCanManageVideoPlaylist (user: MUserAccountId, videoPlaylist: MVideoPlaylist, right: UserRight, res: express.Response) { if (videoPlaylist.isOwned() === false) { - res.status(HttpStatusCode.FORBIDDEN_403) - .json({ error: 'Cannot manage video playlist of another server.' }) - .end() - + res.fail({ + status: HttpStatusCode.FORBIDDEN_403, + message: 'Cannot manage video playlist of another server.' + }) return false } @@ -412,10 +413,10 @@ function checkUserCanManageVideoPlaylist (user: MUserAccountId, videoPlaylist: M // The user can delete it if s/he is an admin // Or if s/he is the video playlist's owner if (user.hasRight(right) === false && videoPlaylist.ownerAccountId !== user.Account.id) { - res.status(HttpStatusCode.FORBIDDEN_403) - .json({ error: 'Cannot manage video playlist of another user' }) - .end() - + res.fail({ + status: HttpStatusCode.FORBIDDEN_403, + message: 'Cannot manage video playlist of another user' + }) return false } diff --git a/server/middlewares/validators/videos/video-rates.ts b/server/middlewares/validators/videos/video-rates.ts index 01bdef25f..5c4176f54 100644 --- a/server/middlewares/validators/videos/video-rates.ts +++ b/server/middlewares/validators/videos/video-rates.ts @@ -37,8 +37,10 @@ const getAccountVideoRateValidatorFactory = function (rateType: VideoRateType) { const rate = await AccountVideoRateModel.loadLocalAndPopulateVideo(rateType, req.params.name, +req.params.videoId) if (!rate) { - return res.status(HttpStatusCode.NOT_FOUND_404) - .json({ error: 'Video rate not found' }) + return res.fail({ + status: HttpStatusCode.NOT_FOUND_404, + message: 'Video rate not found' + }) } res.locals.accountVideoRate = rate diff --git a/server/middlewares/validators/videos/video-watch.ts b/server/middlewares/validators/videos/video-watch.ts index 29ce0dab6..00c739d31 100644 --- a/server/middlewares/validators/videos/video-watch.ts +++ b/server/middlewares/validators/videos/video-watch.ts @@ -21,7 +21,10 @@ const videoWatchingValidator = [ const user = res.locals.oauth.token.User if (user.videosHistoryEnabled === false) { logger.warn('Cannot set videos to watch by user %d: videos history is disabled.', user.id) - return res.status(HttpStatusCode.CONFLICT_409).end() + return res.fail({ + status: HttpStatusCode.CONFLICT_409, + message: 'Video history is disabled' + }) } return next() diff --git a/server/middlewares/validators/videos/videos.ts b/server/middlewares/validators/videos/videos.ts index 8864be269..dfd472400 100644 --- a/server/middlewares/validators/videos/videos.ts +++ b/server/middlewares/validators/videos/videos.ts @@ -73,6 +73,7 @@ const videosAddLegacyValidator = getCommonVideoEditAttributes().concat([ .custom(isIdValid).withMessage('Should have correct video channel id'), async (req: express.Request, res: express.Response, next: express.NextFunction) => { + res.docs = "https://docs.joinpeertube.org/api-rest-reference.html#operation/uploadLegacy" logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files }) if (areValidationErrors(req, res)) return cleanUpReqFiles(req) @@ -88,9 +89,11 @@ const videosAddLegacyValidator = getCommonVideoEditAttributes().concat([ if (!videoFile.duration) await addDurationToVideo(videoFile) } catch (err) { logger.error('Invalid input file in videosAddLegacyValidator.', { err }) - res.status(HttpStatusCode.UNPROCESSABLE_ENTITY_422) - .json({ error: 'Video file unreadable.' }) + res.fail({ + status: HttpStatusCode.UNPROCESSABLE_ENTITY_422, + message: 'Video file unreadable.' + }) return cleanUpReqFiles(req) } @@ -105,6 +108,7 @@ const videosAddLegacyValidator = getCommonVideoEditAttributes().concat([ */ const videosAddResumableValidator = [ async (req: express.Request, res: express.Response, next: express.NextFunction) => { + res.docs = "https://docs.joinpeertube.org/api-rest-reference.html#operation/uploadResumable" const user = res.locals.oauth.token.User const body: express.CustomUploadXFile = req.body @@ -118,9 +122,11 @@ const videosAddResumableValidator = [ if (!file.duration) await addDurationToVideo(file) } catch (err) { logger.error('Invalid input file in videosAddResumableValidator.', { err }) - res.status(HttpStatusCode.UNPROCESSABLE_ENTITY_422) - .json({ error: 'Video file unreadable.' }) + res.fail({ + status: HttpStatusCode.UNPROCESSABLE_ENTITY_422, + message: 'Video file unreadable.' + }) return cleanup() } @@ -164,6 +170,7 @@ const videosAddResumableInitValidator = getCommonVideoEditAttributes().concat([ .withMessage('Should specify the file mimetype'), async (req: express.Request, res: express.Response, next: express.NextFunction) => { + res.docs = "https://docs.joinpeertube.org/api-rest-reference.html#operation/uploadResumableInit" const videoFileMetadata = { mimetype: req.headers['x-upload-content-type'] as string, size: +req.headers['x-upload-content-length'], @@ -207,6 +214,7 @@ const videosUpdateValidator = getCommonVideoEditAttributes().concat([ .custom(isIdValid).withMessage('Should have correct video channel id'), async (req: express.Request, res: express.Response, next: express.NextFunction) => { + res.docs = 'https://docs.joinpeertube.org/api-rest-reference.html#operation/putVideo' logger.debug('Checking videosUpdate parameters', { parameters: req.body }) if (areValidationErrors(req, res)) return cleanUpReqFiles(req) @@ -242,12 +250,14 @@ async function checkVideoFollowConstraints (req: express.Request, res: express.R const serverActor = await getServerActor() if (await VideoModel.checkVideoHasInstanceFollow(video.id, serverActor.id) === true) return next() - return res.status(HttpStatusCode.FORBIDDEN_403) - .json({ - errorCode: ServerErrorCode.DOES_NOT_RESPECT_FOLLOW_CONSTRAINTS, - error: 'Cannot get this video regarding follow constraints.', - originUrl: video.url - }) + return res.fail({ + status: HttpStatusCode.FORBIDDEN_403, + message: 'Cannot get this video regarding follow constraints.', + type: ServerErrorCode.DOES_NOT_RESPECT_FOLLOW_CONSTRAINTS.toString(), + data: { + originUrl: video.url + } + }) } const videosCustomGetValidator = ( @@ -258,6 +268,7 @@ const videosCustomGetValidator = ( param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), async (req: express.Request, res: express.Response, next: express.NextFunction) => { + res.docs = 'https://docs.joinpeertube.org/api-rest-reference.html#operation/getVideo' logger.debug('Checking videosGet parameters', { parameters: req.params }) if (areValidationErrors(req, res)) return @@ -276,8 +287,10 @@ const videosCustomGetValidator = ( // Only the owner or a user that have blacklist rights can see the video if (!user || !user.canGetVideo(video)) { - return res.status(HttpStatusCode.FORBIDDEN_403) - .json({ error: 'Cannot get this private/internal or blacklisted video.' }) + return res.fail({ + status: HttpStatusCode.FORBIDDEN_403, + message: 'Cannot get this private/internal or blacklisted video.' + }) } return next() @@ -291,7 +304,10 @@ const videosCustomGetValidator = ( if (isUUIDValid(req.params.id)) return next() // Don't leak this unlisted video - return res.status(HttpStatusCode.NOT_FOUND_404).end() + return res.fail({ + status: HttpStatusCode.NOT_FOUND_404, + message: 'Video not found' + }) } } ] @@ -318,6 +334,7 @@ const videosRemoveValidator = [ param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), async (req: express.Request, res: express.Response, next: express.NextFunction) => { + res.docs = "https://docs.joinpeertube.org/api-rest-reference.html#operation/delVideo" logger.debug('Checking videosRemove parameters', { parameters: req.params }) if (areValidationErrors(req, res)) return @@ -344,13 +361,11 @@ const videosChangeOwnershipValidator = [ const nextOwner = await AccountModel.loadLocalByName(req.body.username) if (!nextOwner) { - res.status(HttpStatusCode.BAD_REQUEST_400) - .json({ error: 'Changing video ownership to a remote account is not supported yet' }) - + res.fail({ message: 'Changing video ownership to a remote account is not supported yet' }) return } - res.locals.nextOwner = nextOwner + res.locals.nextOwner = nextOwner return next() } ] @@ -370,8 +385,10 @@ const videosTerminateChangeOwnershipValidator = [ const videoChangeOwnership = res.locals.videoChangeOwnership if (videoChangeOwnership.status !== VideoChangeOwnershipStatus.WAITING) { - res.status(HttpStatusCode.FORBIDDEN_403) - .json({ error: 'Ownership already accepted or refused' }) + res.fail({ + status: HttpStatusCode.FORBIDDEN_403, + message: 'Ownership already accepted or refused' + }) return } @@ -388,9 +405,10 @@ const videosAcceptChangeOwnershipValidator = [ const videoChangeOwnership = res.locals.videoChangeOwnership const isAble = await isAbleToUploadVideo(user.id, videoChangeOwnership.Video.getMaxQualityFile().size) if (isAble === false) { - res.status(HttpStatusCode.PAYLOAD_TOO_LARGE_413) - .json({ error: 'The user video quota is exceeded with this video.' }) - + res.fail({ + status: HttpStatusCode.PAYLOAD_TOO_LARGE_413, + message: 'The user video quota is exceeded with this video.' + }) return } @@ -538,9 +556,10 @@ const commonVideosFiltersValidator = [ (req.query.filter === 'all-local' || req.query.filter === 'all') && (!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) === false) ) { - res.status(HttpStatusCode.UNAUTHORIZED_401) - .json({ error: 'You are not allowed to see all local videos.' }) - + res.fail({ + status: HttpStatusCode.UNAUTHORIZED_401, + message: 'You are not allowed to see all local videos.' + }) return } @@ -581,9 +600,7 @@ function areErrorsInScheduleUpdate (req: express.Request, res: express.Response) if (!req.body.scheduleUpdate.updateAt) { logger.warn('Invalid parameters: scheduleUpdate.updateAt is mandatory.') - res.status(HttpStatusCode.BAD_REQUEST_400) - .json({ error: 'Schedule update at is mandatory.' }) - + res.fail({ message: 'Schedule update at is mandatory.' }) return true } } @@ -605,26 +622,27 @@ async function commonVideoChecksPass (parameters: { if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return false if (!isVideoFileMimeTypeValid(files)) { - res.status(HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415) - .json({ - error: 'This file is not supported. Please, make sure it is of the following type: ' + - CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ') - }) - + res.fail({ + status: HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415, + message: 'This file is not supported. Please, make sure it is of the following type: ' + + CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ') + }) return false } if (!isVideoFileSizeValid(videoFileSize.toString())) { - res.status(HttpStatusCode.PAYLOAD_TOO_LARGE_413) - .json({ error: 'This file is too large. It exceeds the maximum file size authorized.' }) - + res.fail({ + status: HttpStatusCode.PAYLOAD_TOO_LARGE_413, + message: 'This file is too large. It exceeds the maximum file size authorized.' + }) return false } if (await isAbleToUploadVideo(user.id, videoFileSize) === false) { - res.status(HttpStatusCode.PAYLOAD_TOO_LARGE_413) - .json({ error: 'The user video quota is exceeded with this video.' }) - + res.fail({ + status: HttpStatusCode.PAYLOAD_TOO_LARGE_413, + message: 'The user video quota is exceeded with this video.' + }) return false } @@ -650,9 +668,10 @@ export async function isVideoAccepted ( if (!acceptedResult || acceptedResult.accepted !== true) { logger.info('Refused local video.', { acceptedResult, acceptParameters }) - res.status(HttpStatusCode.FORBIDDEN_403) - .json({ error: acceptedResult.errorMessage || 'Refused local video' }) - + res.fail({ + status: HttpStatusCode.FORBIDDEN_403, + message: acceptedResult.errorMessage || 'Refused local video' + }) return false } diff --git a/server/middlewares/validators/webfinger.ts b/server/middlewares/validators/webfinger.ts index c2dfccc96..097a5ece1 100644 --- a/server/middlewares/validators/webfinger.ts +++ b/server/middlewares/validators/webfinger.ts @@ -21,9 +21,10 @@ const webfingerValidator = [ const actor = await ActorModel.loadLocalUrlByName(name) if (!actor) { - return res.status(HttpStatusCode.NOT_FOUND_404) - .send({ error: 'Actor not found' }) - .end() + return res.fail({ + status: HttpStatusCode.NOT_FOUND_404, + message: 'Actor not found' + }) } res.locals.actorUrl = actor diff --git a/server/tests/api/users/users-verification.ts b/server/tests/api/users/users-verification.ts index 1a9a519a0..e0f2f2112 100644 --- a/server/tests/api/users/users-verification.ts +++ b/server/tests/api/users/users-verification.ts @@ -19,6 +19,7 @@ import { setAccessTokensToServers } from '../../../../shared/extra-utils/users/l import { MockSmtpServer } from '../../../../shared/extra-utils/miscs/email' import { waitJobs } from '../../../../shared/extra-utils/server/jobs' import { User } from '../../../../shared/models/users' +import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' const expect = chai.expect @@ -89,8 +90,8 @@ describe('Test users account verification', function () { }) it('Should not allow login for user with unverified email', async function () { - const resLogin = await login(server.url, server.client, user1, 400) - expect(resLogin.body.error).to.contain('User email is not verified.') + const resLogin = await login(server.url, server.client, user1, HttpStatusCode.BAD_REQUEST_400) + expect(resLogin.body.detail).to.contain('User email is not verified.') }) it('Should verify the user via email and allow login', async function () { diff --git a/server/tests/api/users/users.ts b/server/tests/api/users/users.ts index cea98aac7..363236b62 100644 --- a/server/tests/api/users/users.ts +++ b/server/tests/api/users/users.ts @@ -93,16 +93,16 @@ describe('Test users', function () { const client = { id: 'client', secret: server.client.secret } const res = await login(server.url, client, server.user, HttpStatusCode.BAD_REQUEST_400) - expect(res.body.code).to.equal('invalid_client') - expect(res.body.error).to.contain('client is invalid') + expect(res.body.type).to.equal('invalid_client') + expect(res.body.detail).to.contain('client is invalid') }) it('Should not login with an invalid client secret', async function () { const client = { id: server.client.id, secret: 'coucou' } const res = await login(server.url, client, server.user, HttpStatusCode.BAD_REQUEST_400) - expect(res.body.code).to.equal('invalid_client') - expect(res.body.error).to.contain('client is invalid') + expect(res.body.type).to.equal('invalid_client') + expect(res.body.detail).to.contain('client is invalid') }) }) @@ -112,16 +112,16 @@ describe('Test users', function () { const user = { username: 'captain crochet', password: server.user.password } const res = await login(server.url, server.client, user, HttpStatusCode.BAD_REQUEST_400) - expect(res.body.code).to.equal('invalid_grant') - expect(res.body.error).to.contain('credentials are invalid') + expect(res.body.type).to.equal('invalid_grant') + expect(res.body.detail).to.contain('credentials are invalid') }) it('Should not login with an invalid password', async function () { const user = { username: server.user.username, password: 'mew_three' } const res = await login(server.url, server.client, user, HttpStatusCode.BAD_REQUEST_400) - expect(res.body.code).to.equal('invalid_grant') - expect(res.body.error).to.contain('credentials are invalid') + expect(res.body.type).to.equal('invalid_grant') + expect(res.body.detail).to.contain('credentials are invalid') }) it('Should not be able to upload a video', async function () { diff --git a/server/typings/express/index.d.ts b/server/typings/express/index.d.ts index 55b6e0039..f58436ce1 100644 --- a/server/typings/express/index.d.ts +++ b/server/typings/express/index.d.ts @@ -22,6 +22,7 @@ import { MAccountVideoRateAccountVideo } from '@server/types/models/video/video- import { HttpMethod } from '@shared/core-utils/miscs/http-methods' import { VideoCreate } from '@shared/models' import { File as UploadXFile, Metadata } from '@uploadx/core' +import { ProblemDocumentOptions } from 'http-problem-details/dist/ProblemDocument' import { RegisteredPlugin } from '../../lib/plugins/plugin-manager' import { MAccountDefault, @@ -83,8 +84,15 @@ declare module 'express' { filename: string } - // Extends locals property from Response + // Extends Response with added functions and potential variables passed by middlewares interface Response { + docs?: string + fail: (options: { + data?: Record + docs?: string + message: string + } & ProblemDocumentOptions) => void + locals: { videoAll?: MVideoFullLight onlyImmutableVideo?: MVideoImmutable diff --git a/support/doc/api/openapi.yaml b/support/doc/api/openapi.yaml index 34bf9c411..52a834056 100644 --- a/support/doc/api/openapi.yaml +++ b/support/doc/api/openapi.yaml @@ -38,46 +38,53 @@ info: # Errors The API uses standard HTTP status codes to indicate the success or failure - of the API call. + of the API call, completed by a [RFC7807-compliant](https://tools.ietf.org/html/rfc7807) response body. ``` HTTP 1.1 404 Not Found - Content-Type: application/json + Content-Type: application/problem+json; charset=utf-8 { - "errorCode": 1 - "error": "Account not found" + "detail": "Video not found", + "status": 404, + "title": "Not Found", + "type": "about:blank" } ``` - We provide error codes for [a growing number of cases](https://github.com/Chocobozzz/PeerTube/blob/develop/shared/models/server/server-error-code.enum.ts), + We provide error types for [a growing number of cases](https://github.com/Chocobozzz/PeerTube/blob/develop/shared/models/server/server-error-code.enum.ts), but it is still optional. ### Validation errors Each parameter is evaluated on its own against a set of rules before the route validator - proceeds with potential testing involving parameter combinations. Errors coming from Validation + proceeds with potential testing involving parameter combinations. Errors coming from validation errors appear earlier and benefit from a more detailed error type: ``` HTTP 1.1 400 Bad Request - Content-Type: application/json + Content-Type: application/problem+json; charset=utf-8 { - "errors": { + "detail": "Incorrect request parameters: id", + "instance": "/api/v1/videos/9c9de5e8-0a1e-484a-b099-e80766180", + "invalid-params": { "id": { - "value": "a117eb-c6a9-4756-bb09-2a956239f", - "msg": "Should have a valid id", + "location": "params", + "msg": "Invalid value", "param": "id", - "location": "params" + "value": "9c9de5e8-0a1e-484a-b099-e80766180" } - } + }, + "status": 400, + "title": "Bad Request", + "type": "about:blank" } ``` Where `id` is the name of the field concerned by the error, within the route definition. - `errors..location` can be either 'params', 'body', 'header', 'query' or 'cookies', and - `errors..value` reports the value that didn't pass validation whose `errors..msg` + `invalid-params..location` can be either 'params', 'body', 'header', 'query' or 'cookies', and + `invalid-params..value` reports the value that didn't pass validation whose `invalid-params..msg` is about. # Rate limits diff --git a/yarn.lock b/yarn.lock index adfb8c912..4731b61f4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4136,6 +4136,11 @@ http-parser-js@^0.5.2: resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.3.tgz#01d2709c79d41698bb01d4decc5e9da4e4a033d9" integrity sha512-t7hjvef/5HEK7RWTdUzVUhl8zkEu+LlaE0IYzdMuvbSDipxBRpOn4Uhw8ZyECEa808iVT8XCjzo6xmYt4CiLZg== +http-problem-details@^0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/http-problem-details/-/http-problem-details-0.1.5.tgz#f8f94f4ab9d4050749e9f8566fb85bb8caa2be56" + integrity sha512-GHxfQZ0POP4FWbAM0guOyZyJNWwbLUXp+4XOJdmitS2tp3gHVSatrSX59Yyq/dCkhk4KiGtTWIlXZC83yCkBkA== + http-signature@1.3.5: version "1.3.5" resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.3.5.tgz#9f19496ffbf3227298d7b5f156e0e1a948678683"