aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorRigel Kent <sendmemail@rigelk.eu>2021-06-01 01:36:53 +0200
committerChocobozzz <chocobozzz@cpy.re>2021-06-02 16:57:07 +0200
commit76148b27f7501bac061992136852be4303370c8d (patch)
treefc0559253e833c9252fa14ebaec5321d88bfb4e8
parent5ed25fb76e920dac364cb9ef46f14ec4bd372949 (diff)
downloadPeerTube-76148b27f7501bac061992136852be4303370c8d.tar.gz
PeerTube-76148b27f7501bac061992136852be4303370c8d.tar.zst
PeerTube-76148b27f7501bac061992136852be4303370c8d.zip
refactor API errors to standard error format
-rw-r--r--client/src/app/+videos/+video-watch/video-watch.component.ts2
-rw-r--r--client/src/app/core/rest/rest-extractor.service.ts2
-rw-r--r--package.json1
-rw-r--r--server.ts10
-rw-r--r--server/controllers/api/abuse.ts6
-rw-r--r--server/controllers/api/bulk.ts2
-rw-r--r--server/controllers/api/custom-page.ts9
-rw-r--r--server/controllers/api/oauth-clients.ts5
-rw-r--r--server/controllers/api/plugins.ts14
-rw-r--r--server/controllers/api/search.ts10
-rw-r--r--server/controllers/api/server/debug.ts3
-rw-r--r--server/controllers/api/server/redundancy.ts6
-rw-r--r--server/controllers/api/users/index.ts4
-rw-r--r--server/controllers/api/users/me.ts6
-rw-r--r--server/controllers/api/users/token.ts7
-rw-r--r--server/controllers/api/video-channel.ts4
-rw-r--r--server/controllers/api/videos/blacklist.ts6
-rw-r--r--server/controllers/api/videos/comment.ts5
-rw-r--r--server/controllers/api/videos/import.ts21
-rw-r--r--server/controllers/api/videos/index.ts4
-rw-r--r--server/controllers/api/videos/live.ts2
-rw-r--r--server/controllers/api/videos/ownership.ts4
-rw-r--r--server/controllers/api/videos/upload.ts7
-rw-r--r--server/controllers/client.ts4
-rw-r--r--server/controllers/download.ts27
-rw-r--r--server/controllers/lazy-static.ts12
-rw-r--r--server/controllers/live.ts2
-rw-r--r--server/controllers/plugins.ts10
-rw-r--r--server/controllers/static.ts13
-rw-r--r--server/helpers/custom-validators/video-comments.ts39
-rw-r--r--server/helpers/custom-validators/video-imports.ts8
-rw-r--r--server/helpers/custom-validators/video-ownership.ts15
-rw-r--r--server/helpers/express-utils.ts32
-rw-r--r--server/helpers/middlewares/abuses.ts6
-rw-r--r--server/helpers/middlewares/accounts.ts16
-rw-r--r--server/helpers/middlewares/video-blacklists.ts8
-rw-r--r--server/helpers/middlewares/video-captions.ts7
-rw-r--r--server/helpers/middlewares/video-channels.ts7
-rw-r--r--server/helpers/middlewares/video-playlists.ts8
-rw-r--r--server/helpers/middlewares/videos.ts40
-rw-r--r--server/lib/client-html.ts4
-rw-r--r--server/middlewares/activitypub.ts40
-rw-r--r--server/middlewares/auth.ts17
-rw-r--r--server/middlewares/servers.ts5
-rw-r--r--server/middlewares/user-right.ts6
-rw-r--r--server/middlewares/validators/abuse.ts27
-rw-r--r--server/middlewares/validators/activitypub/activity.ts2
-rw-r--r--server/middlewares/validators/blocklist.ts29
-rw-r--r--server/middlewares/validators/bulk.ts6
-rw-r--r--server/middlewares/validators/config.ts13
-rw-r--r--server/middlewares/validators/feeds.ts13
-rw-r--r--server/middlewares/validators/follows.ts26
-rw-r--r--server/middlewares/validators/oembed.ts45
-rw-r--r--server/middlewares/validators/plugins.ts42
-rw-r--r--server/middlewares/validators/redundancy.ts57
-rw-r--r--server/middlewares/validators/server.ts31
-rw-r--r--server/middlewares/validators/themes.ts10
-rw-r--r--server/middlewares/validators/user-subscriptions.ts9
-rw-r--r--server/middlewares/validators/users.ts113
-rw-r--r--server/middlewares/validators/utils.ts10
-rw-r--r--server/middlewares/validators/videos/video-blacklist.ts8
-rw-r--r--server/middlewares/validators/videos/video-channels.ts47
-rw-r--r--server/middlewares/validators/videos/video-comments.ts27
-rw-r--r--server/middlewares/validators/videos/video-imports.ts23
-rw-r--r--server/middlewares/validators/videos/video-live.ts61
-rw-r--r--server/middlewares/validators/videos/video-playlists.ts73
-rw-r--r--server/middlewares/validators/videos/video-rates.ts6
-rw-r--r--server/middlewares/validators/videos/video-watch.ts5
-rw-r--r--server/middlewares/validators/videos/videos.ts105
-rw-r--r--server/middlewares/validators/webfinger.ts7
-rw-r--r--server/tests/api/users/users-verification.ts5
-rw-r--r--server/tests/api/users/users.ts16
-rw-r--r--server/typings/express/index.d.ts10
-rw-r--r--support/doc/api/openapi.yaml35
-rw-r--r--yarn.lock5
75 files changed, 785 insertions, 547 deletions
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 {
431 .pipe( 431 .pipe(
432 // If 400, 403 or 404, the video is private or blocked so redirect to 404 432 // If 400, 403 or 404, the video is private or blocked so redirect to 404
433 catchError(err => { 433 catchError(err => {
434 if (err.body.errorCode === ServerErrorCode.DOES_NOT_RESPECT_FOLLOW_CONSTRAINTS && err.body.originUrl) { 434 if (err.body.type === ServerErrorCode.DOES_NOT_RESPECT_FOLLOW_CONSTRAINTS && err.body.originUrl) {
435 const search = window.location.search 435 const search = window.location.search
436 let originUrl = err.body.originUrl 436 let originUrl = err.body.originUrl
437 if (search) originUrl += search 437 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 {
41 41
42 if (err.error instanceof Error) { 42 if (err.error instanceof Error) {
43 // A client-side or network error occurred. Handle it accordingly. 43 // A client-side or network error occurred. Handle it accordingly.
44 errorMessage = err.error.message 44 errorMessage = err.error.detail || err.error.title
45 console.error('An error occurred:', errorMessage) 45 console.error('An error occurred:', errorMessage)
46 } else if (typeof err.error === 'string') { 46 } else if (typeof err.error === 'string') {
47 errorMessage = err.error 47 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 @@
100 "fs-extra": "^10.0.0", 100 "fs-extra": "^10.0.0",
101 "got": "^11.8.2", 101 "got": "^11.8.2",
102 "helmet": "^4.1.0", 102 "helmet": "^4.1.0",
103 "http-problem-details": "^0.1.5",
103 "http-signature": "1.3.5", 104 "http-signature": "1.3.5",
104 "ip-anonymize": "^0.1.0", 105 "ip-anonymize": "^0.1.0",
105 "ipaddr.js": "2.0.0", 106 "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'
128import { HttpStatusCode } from './shared/core-utils/miscs/http-error-codes' 128import { HttpStatusCode } from './shared/core-utils/miscs/http-error-codes'
129import { VideosTorrentCache } from '@server/lib/files-cache/videos-torrent-cache' 129import { VideosTorrentCache } from '@server/lib/files-cache/videos-torrent-cache'
130import { ServerConfigManager } from '@server/lib/server-config-manager' 130import { ServerConfigManager } from '@server/lib/server-config-manager'
131import { apiResponseHelpers } from '@server/helpers/express-utils'
131 132
132// ----------- Command line ----------- 133// ----------- Command line -----------
133 134
@@ -186,6 +187,9 @@ app.use(cookieParser())
186// W3C DNT Tracking Status 187// W3C DNT Tracking Status
187app.use(advertiseDoNotTrack) 188app.use(advertiseDoNotTrack)
188 189
190// Response helpers used in developement
191app.use(apiResponseHelpers)
192
189// ----------- Views, routes and static files ----------- 193// ----------- Views, routes and static files -----------
190 194
191// API 195// API
@@ -235,7 +239,11 @@ app.use(function (err, req, res, next) {
235 const sql = err.parent ? err.parent.sql : undefined 239 const sql = err.parent ? err.parent.sql : undefined
236 240
237 logger.error('Error in controller.', { err: error, sql }) 241 logger.error('Error in controller.', { err: error, sql })
238 return res.status(err.status || HttpStatusCode.INTERNAL_SERVER_ERROR_500).end() 242 return res.fail({
243 status: err.status || HttpStatusCode.INTERNAL_SERVER_ERROR_500,
244 message: err.message,
245 type: err.name
246 })
239}) 247})
240 248
241const server = createWebsocketTrackerServer(app) 249const 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) {
142 142
143 // Do not send the delete to other instances, we updated OUR copy of this abuse 143 // Do not send the delete to other instances, we updated OUR copy of this abuse
144 144
145 return res.sendStatus(HttpStatusCode.NO_CONTENT_204) 145 return res.status(HttpStatusCode.NO_CONTENT_204).end()
146} 146}
147 147
148async function deleteAbuse (req: express.Request, res: express.Response) { 148async function deleteAbuse (req: express.Request, res: express.Response) {
@@ -154,7 +154,7 @@ async function deleteAbuse (req: express.Request, res: express.Response) {
154 154
155 // Do not send the delete to other instances, we delete OUR copy of this abuse 155 // Do not send the delete to other instances, we delete OUR copy of this abuse
156 156
157 return res.sendStatus(HttpStatusCode.NO_CONTENT_204) 157 return res.status(HttpStatusCode.NO_CONTENT_204).end()
158} 158}
159 159
160async function reportAbuse (req: express.Request, res: express.Response) { 160async function reportAbuse (req: express.Request, res: express.Response) {
@@ -244,5 +244,5 @@ async function deleteAbuseMessage (req: express.Request, res: express.Response)
244 return abuseMessage.destroy({ transaction: t }) 244 return abuseMessage.destroy({ transaction: t })
245 }) 245 })
246 246
247 return res.sendStatus(HttpStatusCode.NO_CONTENT_204) 247 return res.status(HttpStatusCode.NO_CONTENT_204).end()
248} 248}
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
34 const comments = await VideoCommentModel.listForBulkDelete(account, filter) 34 const comments = await VideoCommentModel.listForBulkDelete(account, filter)
35 35
36 // Don't wait result 36 // Don't wait result
37 res.sendStatus(HttpStatusCode.NO_CONTENT_204) 37 res.status(HttpStatusCode.NO_CONTENT_204).end()
38 38
39 for (const comment of comments) { 39 for (const comment of comments) {
40 await removeComment(comment) 40 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 {
27 27
28async function getInstanceHomepage (req: express.Request, res: express.Response) { 28async function getInstanceHomepage (req: express.Request, res: express.Response) {
29 const page = await ActorCustomPageModel.loadInstanceHomepage() 29 const page = await ActorCustomPageModel.loadInstanceHomepage()
30 if (!page) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) 30 if (!page) {
31 return res.fail({
32 status: HttpStatusCode.NOT_FOUND_404,
33 message: 'Instance homepage could not be found'
34 })
35 }
31 36
32 return res.json(page.toFormattedJSON()) 37 return res.json(page.toFormattedJSON())
33} 38}
@@ -38,5 +43,5 @@ async function updateInstanceHomepage (req: express.Request, res: express.Respon
38 await ActorCustomPageModel.updateInstanceHomepage(content) 43 await ActorCustomPageModel.updateInstanceHomepage(content)
39 ServerConfigManager.Instance.updateHomepageState(content) 44 ServerConfigManager.Instance.updateHomepageState(content)
40 45
41 return res.sendStatus(HttpStatusCode.NO_CONTENT_204) 46 return res.status(HttpStatusCode.NO_CONTENT_204).end()
42} 47}
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
24 // Don't make this check if this is a test instance 24 // Don't make this check if this is a test instance
25 if (process.env.NODE_ENV !== 'test' && req.get('host') !== headerHostShouldBe) { 25 if (process.env.NODE_ENV !== 'test' && req.get('host') !== headerHostShouldBe) {
26 logger.info('Getting client tokens for host %s is forbidden (expected %s).', req.get('host'), headerHostShouldBe) 26 logger.info('Getting client tokens for host %s is forbidden (expected %s).', req.get('host'), headerHostShouldBe)
27 return res.type('json').status(HttpStatusCode.FORBIDDEN_403).end() 27 return res.fail({
28 status: HttpStatusCode.FORBIDDEN_403,
29 message: `Getting client tokens for host ${req.get('host')} is forbidden`
30 })
28 } 31 }
29 32
30 const client = await OAuthClientModel.loadFirstClient() 33 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) {
144 return res.json(plugin.toFormattedJSON()) 144 return res.json(plugin.toFormattedJSON())
145 } catch (err) { 145 } catch (err) {
146 logger.warn('Cannot install plugin %s.', toInstall, { err }) 146 logger.warn('Cannot install plugin %s.', toInstall, { err })
147 return res.sendStatus(HttpStatusCode.BAD_REQUEST_400) 147 return res.fail({ message: 'Cannot install plugin ' + toInstall })
148 } 148 }
149} 149}
150 150
@@ -159,7 +159,7 @@ async function updatePlugin (req: express.Request, res: express.Response) {
159 return res.json(plugin.toFormattedJSON()) 159 return res.json(plugin.toFormattedJSON())
160 } catch (err) { 160 } catch (err) {
161 logger.warn('Cannot update plugin %s.', toUpdate, { err }) 161 logger.warn('Cannot update plugin %s.', toUpdate, { err })
162 return res.sendStatus(HttpStatusCode.BAD_REQUEST_400) 162 return res.fail({ message: 'Cannot update plugin ' + toUpdate })
163 } 163 }
164} 164}
165 165
@@ -168,7 +168,7 @@ async function uninstallPlugin (req: express.Request, res: express.Response) {
168 168
169 await PluginManager.Instance.uninstall(body.npmName) 169 await PluginManager.Instance.uninstall(body.npmName)
170 170
171 return res.sendStatus(HttpStatusCode.NO_CONTENT_204) 171 return res.status(HttpStatusCode.NO_CONTENT_204).end()
172} 172}
173 173
174function getPublicPluginSettings (req: express.Request, res: express.Response) { 174function getPublicPluginSettings (req: express.Request, res: express.Response) {
@@ -197,7 +197,7 @@ async function updatePluginSettings (req: express.Request, res: express.Response
197 197
198 await PluginManager.Instance.onSettingsChanged(plugin.name, plugin.settings) 198 await PluginManager.Instance.onSettingsChanged(plugin.name, plugin.settings)
199 199
200 return res.sendStatus(HttpStatusCode.NO_CONTENT_204) 200 return res.status(HttpStatusCode.NO_CONTENT_204).end()
201} 201}
202 202
203async function listAvailablePlugins (req: express.Request, res: express.Response) { 203async function listAvailablePlugins (req: express.Request, res: express.Response) {
@@ -206,8 +206,10 @@ async function listAvailablePlugins (req: express.Request, res: express.Response
206 const resultList = await listAvailablePluginsFromIndex(query) 206 const resultList = await listAvailablePluginsFromIndex(query)
207 207
208 if (!resultList) { 208 if (!resultList) {
209 return res.status(HttpStatusCode.SERVICE_UNAVAILABLE_503) 209 return res.fail({
210 .json({ error: 'Plugin index unavailable. Please retry later' }) 210 status: HttpStatusCode.SERVICE_UNAVAILABLE_503,
211 message: 'Plugin index unavailable. Please retry later'
212 })
211 } 213 }
212 214
213 return res.json(resultList) 215 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
102 } catch (err) { 102 } catch (err) {
103 logger.warn('Cannot use search index to make video channels search.', { err }) 103 logger.warn('Cannot use search index to make video channels search.', { err })
104 104
105 return res.sendStatus(HttpStatusCode.INTERNAL_SERVER_ERROR_500) 105 return res.fail({
106 status: HttpStatusCode.INTERNAL_SERVER_ERROR_500,
107 message: 'Cannot use search index to make video channels search'
108 })
106 } 109 }
107} 110}
108 111
@@ -202,7 +205,10 @@ async function searchVideosIndex (query: VideosSearchQuery, res: express.Respons
202 } catch (err) { 205 } catch (err) {
203 logger.warn('Cannot use search index to make video search.', { err }) 206 logger.warn('Cannot use search index to make video search.', { err })
204 207
205 return res.sendStatus(HttpStatusCode.INTERNAL_SERVER_ERROR_500) 208 return res.fail({
209 status: HttpStatusCode.INTERNAL_SERVER_ERROR_500,
210 message: 'Cannot use search index to make video search'
211 })
206 } 212 }
207} 213}
208 214
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 @@
1import { InboxManager } from '@server/lib/activitypub/inbox-manager' 1import { InboxManager } from '@server/lib/activitypub/inbox-manager'
2import { RemoveDanglingResumableUploadsScheduler } from '@server/lib/schedulers/remove-dangling-resumable-uploads-scheduler' 2import { RemoveDanglingResumableUploadsScheduler } from '@server/lib/schedulers/remove-dangling-resumable-uploads-scheduler'
3import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
3import { SendDebugCommand } from '@shared/models' 4import { SendDebugCommand } from '@shared/models'
4import * as express from 'express' 5import * as express from 'express'
5import { UserRight } from '../../../../shared/models/users' 6import { UserRight } from '../../../../shared/models/users'
@@ -41,5 +42,5 @@ async function runCommand (req: express.Request, res: express.Response) {
41 await RemoveDanglingResumableUploadsScheduler.Instance.execute() 42 await RemoveDanglingResumableUploadsScheduler.Instance.execute()
42 } 43 }
43 44
44 return res.sendStatus(204) 45 return res.status(HttpStatusCode.NO_CONTENT_204).end()
45} 46}
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)
90 payload 90 payload
91 }) 91 })
92 92
93 return res.sendStatus(HttpStatusCode.NO_CONTENT_204) 93 return res.status(HttpStatusCode.NO_CONTENT_204).end()
94} 94}
95 95
96async function removeVideoRedundancyController (req: express.Request, res: express.Response) { 96async function removeVideoRedundancyController (req: express.Request, res: express.Response) {
97 await removeVideoRedundancy(res.locals.videoRedundancy) 97 await removeVideoRedundancy(res.locals.videoRedundancy)
98 98
99 return res.sendStatus(HttpStatusCode.NO_CONTENT_204) 99 return res.status(HttpStatusCode.NO_CONTENT_204).end()
100} 100}
101 101
102async function updateRedundancy (req: express.Request, res: express.Response) { 102async function updateRedundancy (req: express.Request, res: express.Response) {
@@ -110,5 +110,5 @@ async function updateRedundancy (req: express.Request, res: express.Response) {
110 removeRedundanciesOfServer(server.id) 110 removeRedundanciesOfServer(server.id)
111 .catch(err => logger.error('Cannot remove redundancy of %s.', server.host, { err })) 111 .catch(err => logger.error('Cannot remove redundancy of %s.', server.host, { err }))
112 112
113 return res.sendStatus(HttpStatusCode.NO_CONTENT_204) 113 return res.status(HttpStatusCode.NO_CONTENT_204).end()
114} 114}
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) {
314 314
315 Hooks.runAction('action:api.user.deleted', { user }) 315 Hooks.runAction('action:api.user.deleted', { user })
316 316
317 return res.sendStatus(HttpStatusCode.NO_CONTENT_204) 317 return res.status(HttpStatusCode.NO_CONTENT_204).end()
318} 318}
319 319
320async function updateUser (req: express.Request, res: express.Response) { 320async function updateUser (req: express.Request, res: express.Response) {
@@ -349,7 +349,7 @@ async function updateUser (req: express.Request, res: express.Response) {
349 349
350 // Don't need to send this update to followers, these attributes are not federated 350 // Don't need to send this update to followers, these attributes are not federated
351 351
352 return res.sendStatus(HttpStatusCode.NO_CONTENT_204) 352 return res.status(HttpStatusCode.NO_CONTENT_204).end()
353} 353}
354 354
355async function askResetUserPassword (req: express.Request, res: express.Response) { 355async 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) {
183 183
184 await user.destroy() 184 await user.destroy()
185 185
186 return res.sendStatus(HttpStatusCode.NO_CONTENT_204) 186 return res.status(HttpStatusCode.NO_CONTENT_204).end()
187} 187}
188 188
189async function updateMe (req: express.Request, res: express.Response) { 189async function updateMe (req: express.Request, res: express.Response) {
@@ -237,7 +237,7 @@ async function updateMe (req: express.Request, res: express.Response) {
237 await sendVerifyUserEmail(user, true) 237 await sendVerifyUserEmail(user, true)
238 } 238 }
239 239
240 return res.sendStatus(HttpStatusCode.NO_CONTENT_204) 240 return res.status(HttpStatusCode.NO_CONTENT_204).end()
241} 241}
242 242
243async function updateMyAvatar (req: express.Request, res: express.Response) { 243async function updateMyAvatar (req: express.Request, res: express.Response) {
@@ -257,5 +257,5 @@ async function deleteMyAvatar (req: express.Request, res: express.Response) {
257 const userAccount = await AccountModel.load(user.Account.id) 257 const userAccount = await AccountModel.load(user.Account.id)
258 await deleteLocalActorImageFile(userAccount, ActorImageType.AVATAR) 258 await deleteLocalActorImageFile(userAccount, ActorImageType.AVATAR)
259 259
260 return res.sendStatus(HttpStatusCode.NO_CONTENT_204) 260 return res.status(HttpStatusCode.NO_CONTENT_204).end()
261} 261}
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
78 } catch (err) { 78 } catch (err) {
79 logger.warn('Login error', { err }) 79 logger.warn('Login error', { err })
80 80
81 return res.status(err.code || 400).json({ 81 return res.fail({
82 code: err.name, 82 status: err.code,
83 error: err.message 83 message: err.message,
84 type: err.name
84 }) 85 })
85 } 86 }
86} 87}
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
180 180
181 await deleteLocalActorImageFile(videoChannel, ActorImageType.AVATAR) 181 await deleteLocalActorImageFile(videoChannel, ActorImageType.AVATAR)
182 182
183 return res.sendStatus(HttpStatusCode.NO_CONTENT_204) 183 return res.status(HttpStatusCode.NO_CONTENT_204).end()
184} 184}
185 185
186async function deleteVideoChannelBanner (req: express.Request, res: express.Response) { 186async function deleteVideoChannelBanner (req: express.Request, res: express.Response) {
@@ -188,7 +188,7 @@ async function deleteVideoChannelBanner (req: express.Request, res: express.Resp
188 188
189 await deleteLocalActorImageFile(videoChannel, ActorImageType.BANNER) 189 await deleteLocalActorImageFile(videoChannel, ActorImageType.BANNER)
190 190
191 return res.sendStatus(HttpStatusCode.NO_CONTENT_204) 191 return res.status(HttpStatusCode.NO_CONTENT_204).end()
192} 192}
193 193
194async function addVideoChannel (req: express.Request, res: express.Response) { 194async 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
70 70
71 logger.info('Video %s blacklisted.', videoInstance.uuid) 71 logger.info('Video %s blacklisted.', videoInstance.uuid)
72 72
73 return res.type('json').sendStatus(HttpStatusCode.NO_CONTENT_204) 73 return res.type('json').status(HttpStatusCode.NO_CONTENT_204).end()
74} 74}
75 75
76async function updateVideoBlacklistController (req: express.Request, res: express.Response) { 76async function updateVideoBlacklistController (req: express.Request, res: express.Response) {
@@ -82,7 +82,7 @@ async function updateVideoBlacklistController (req: express.Request, res: expres
82 return videoBlacklist.save({ transaction: t }) 82 return videoBlacklist.save({ transaction: t })
83 }) 83 })
84 84
85 return res.type('json').sendStatus(HttpStatusCode.NO_CONTENT_204) 85 return res.type('json').status(HttpStatusCode.NO_CONTENT_204).end()
86} 86}
87 87
88async function listBlacklist (req: express.Request, res: express.Response) { 88async function listBlacklist (req: express.Request, res: express.Response) {
@@ -105,5 +105,5 @@ async function removeVideoFromBlacklistController (req: express.Request, res: ex
105 105
106 logger.info('Video %s removed from blacklist.', video.uuid) 106 logger.info('Video %s removed from blacklist.', video.uuid)
107 107
108 return res.type('json').sendStatus(HttpStatusCode.NO_CONTENT_204) 108 return res.type('json').status(HttpStatusCode.NO_CONTENT_204).end()
109} 109}
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
166 } 166 }
167 167
168 if (resultList.data.length === 0) { 168 if (resultList.data.length === 0) {
169 return res.sendStatus(HttpStatusCode.NOT_FOUND_404) 169 return res.fail({
170 status: HttpStatusCode.NOT_FOUND_404,
171 message: 'No comments were found'
172 })
170 } 173 }
171 174
172 return res.json(buildFormattedCommentTree(resultList)) 175 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 {
18} from '@server/types/models' 18} from '@server/types/models'
19import { MVideoImportFormattable } from '@server/types/models/video/video-import' 19import { MVideoImportFormattable } from '@server/types/models/video/video-import'
20import { ServerErrorCode, VideoImportCreate, VideoImportState, VideoPrivacy, VideoState } from '../../../../shared' 20import { ServerErrorCode, VideoImportCreate, VideoImportState, VideoPrivacy, VideoState } from '../../../../shared'
21import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
22import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type' 21import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type'
23import { auditLoggerFactory, getAuditIdFromRes, VideoImportAuditView } from '../../../helpers/audit-logger' 22import { auditLoggerFactory, getAuditIdFromRes, VideoImportAuditView } from '../../../helpers/audit-logger'
24import { moveAndProcessCaptionFile } from '../../../helpers/captions-utils' 23import { moveAndProcessCaptionFile } from '../../../helpers/captions-utils'
@@ -143,10 +142,12 @@ async function addYoutubeDLImport (req: express.Request, res: express.Response)
143 } catch (err) { 142 } catch (err) {
144 logger.info('Cannot fetch information from import for URL %s.', targetUrl, { err }) 143 logger.info('Cannot fetch information from import for URL %s.', targetUrl, { err })
145 144
146 return res.status(HttpStatusCode.BAD_REQUEST_400) 145 return res.fail({
147 .json({ 146 message: 'Cannot fetch remote information of this URL.',
148 error: 'Cannot fetch remote information of this URL.' 147 data: {
149 }) 148 targetUrl
149 }
150 })
150 } 151 }
151 152
152 const video = buildVideo(res.locals.videoChannel.id, body, youtubeDLInfo) 153 const video = buildVideo(res.locals.videoChannel.id, body, youtubeDLInfo)
@@ -333,12 +334,10 @@ async function processTorrentOrAbortRequest (req: express.Request, res: express.
333 if (parsedTorrent.files.length !== 1) { 334 if (parsedTorrent.files.length !== 1) {
334 cleanUpReqFiles(req) 335 cleanUpReqFiles(req)
335 336
336 res.status(HttpStatusCode.BAD_REQUEST_400) 337 res.fail({
337 .json({ 338 type: ServerErrorCode.INCORRECT_FILES_IN_TORRENT.toString(),
338 code: ServerErrorCode.INCORRECT_FILES_IN_TORRENT, 339 message: 'Torrents with only 1 file are supported.'
339 error: 'Torrents with only 1 file are supported.' 340 })
340 })
341
342 return undefined 341 return undefined
343 } 342 }
344 343
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) {
146 const exists = await Redis.Instance.doesVideoIPViewExist(ip, immutableVideoAttrs.uuid) 146 const exists = await Redis.Instance.doesVideoIPViewExist(ip, immutableVideoAttrs.uuid)
147 if (exists) { 147 if (exists) {
148 logger.debug('View for ip %s and video %s already exists.', ip, immutableVideoAttrs.uuid) 148 logger.debug('View for ip %s and video %s already exists.', ip, immutableVideoAttrs.uuid)
149 return res.sendStatus(HttpStatusCode.NO_CONTENT_204) 149 return res.status(HttpStatusCode.NO_CONTENT_204).end()
150 } 150 }
151 151
152 const video = await VideoModel.load(immutableVideoAttrs.id) 152 const video = await VideoModel.load(immutableVideoAttrs.id)
@@ -179,7 +179,7 @@ async function viewVideo (req: express.Request, res: express.Response) {
179 179
180 Hooks.runAction('action:api.video.viewed', { video, ip }) 180 Hooks.runAction('action:api.video.viewed', { video, ip })
181 181
182 return res.sendStatus(HttpStatusCode.NO_CONTENT_204) 182 return res.status(HttpStatusCode.NO_CONTENT_204).end()
183} 183}
184 184
185async function getVideoDescription (req: express.Request, res: express.Response) { 185async 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) {
76 76
77 await federateVideoIfNeeded(video, false) 77 await federateVideoIfNeeded(video, false)
78 78
79 return res.sendStatus(HttpStatusCode.NO_CONTENT_204) 79 return res.status(HttpStatusCode.NO_CONTENT_204).end()
80} 80}
81 81
82async function addLiveVideo (req: express.Request, res: express.Response) { 82async 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) {
122 videoChangeOwnership.status = VideoChangeOwnershipStatus.ACCEPTED 122 videoChangeOwnership.status = VideoChangeOwnershipStatus.ACCEPTED
123 await videoChangeOwnership.save({ transaction: t }) 123 await videoChangeOwnership.save({ transaction: t })
124 124
125 return res.sendStatus(HttpStatusCode.NO_CONTENT_204) 125 return res.status(HttpStatusCode.NO_CONTENT_204).end()
126 }) 126 })
127} 127}
128 128
@@ -133,6 +133,6 @@ function refuseOwnership (req: express.Request, res: express.Response) {
133 videoChangeOwnership.status = VideoChangeOwnershipStatus.REFUSED 133 videoChangeOwnership.status = VideoChangeOwnershipStatus.REFUSED
134 await videoChangeOwnership.save({ transaction: t }) 134 await videoChangeOwnership.save({ transaction: t })
135 135
136 return res.sendStatus(HttpStatusCode.NO_CONTENT_204) 136 return res.status(HttpStatusCode.NO_CONTENT_204).end()
137 }) 137 })
138} 138}
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
97 // Uploading the video could be long 97 // Uploading the video could be long
98 // Set timeout to 10 minutes, as Express's default is 2 minutes 98 // Set timeout to 10 minutes, as Express's default is 2 minutes
99 req.setTimeout(1000 * 60 * 10, () => { 99 req.setTimeout(1000 * 60 * 10, () => {
100 logger.error('Upload video has timed out.') 100 logger.error('Video upload has timed out.')
101 return res.sendStatus(HttpStatusCode.REQUEST_TIMEOUT_408) 101 return res.fail({
102 status: HttpStatusCode.REQUEST_TIMEOUT_408,
103 message: 'Video upload has timed out.'
104 })
102 }) 105 })
103 106
104 const videoPhysicalFile = req.files['videofile'][0] 107 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
78 78
79// 404 for static files not found 79// 404 for static files not found
80clientsRouter.use('/client/*', (req: express.Request, res: express.Response) => { 80clientsRouter.use('/client/*', (req: express.Request, res: express.Response) => {
81 res.sendStatus(HttpStatusCode.NOT_FOUND_404) 81 res.status(HttpStatusCode.NOT_FOUND_404).end()
82}) 82})
83 83
84// Always serve index client page (the client is a single page application, let it handle routing) 84// 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) {
105 return res.sendFile(path, { maxAge: STATIC_MAX_AGE.SERVER }) 105 return res.sendFile(path, { maxAge: STATIC_MAX_AGE.SERVER })
106 } 106 }
107 107
108 return res.sendStatus(HttpStatusCode.NOT_FOUND_404) 108 return res.status(HttpStatusCode.NOT_FOUND_404).end()
109} 109}
110 110
111async function generateEmbedHtmlPage (req: express.Request, res: express.Response) { 111async 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 {
41 41
42async function downloadTorrent (req: express.Request, res: express.Response) { 42async function downloadTorrent (req: express.Request, res: express.Response) {
43 const result = await VideosTorrentCache.Instance.getFilePath(req.params.filename) 43 const result = await VideosTorrentCache.Instance.getFilePath(req.params.filename)
44 if (!result) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) 44 if (!result) {
45 return res.fail({
46 status: HttpStatusCode.NOT_FOUND_404,
47 message: 'Torrent file not found'
48 })
49 }
45 50
46 const allowParameters = { torrentPath: result.path, downloadName: result.downloadName } 51 const allowParameters = { torrentPath: result.path, downloadName: result.downloadName }
47 52
@@ -60,7 +65,12 @@ async function downloadVideoFile (req: express.Request, res: express.Response) {
60 const video = res.locals.videoAll 65 const video = res.locals.videoAll
61 66
62 const videoFile = getVideoFile(req, video.VideoFiles) 67 const videoFile = getVideoFile(req, video.VideoFiles)
63 if (!videoFile) return res.status(HttpStatusCode.NOT_FOUND_404).end() 68 if (!videoFile) {
69 return res.fail({
70 status: HttpStatusCode.NOT_FOUND_404,
71 message: 'Video file not found'
72 })
73 }
64 74
65 const allowParameters = { video, videoFile } 75 const allowParameters = { video, videoFile }
66 76
@@ -81,7 +91,12 @@ async function downloadHLSVideoFile (req: express.Request, res: express.Response
81 if (!streamingPlaylist) return res.status(HttpStatusCode.NOT_FOUND_404).end 91 if (!streamingPlaylist) return res.status(HttpStatusCode.NOT_FOUND_404).end
82 92
83 const videoFile = getVideoFile(req, streamingPlaylist.VideoFiles) 93 const videoFile = getVideoFile(req, streamingPlaylist.VideoFiles)
84 if (!videoFile) return res.status(HttpStatusCode.NOT_FOUND_404).end() 94 if (!videoFile) {
95 return res.fail({
96 status: HttpStatusCode.NOT_FOUND_404,
97 message: 'Video file not found'
98 })
99 }
85 100
86 const allowParameters = { video, streamingPlaylist, videoFile } 101 const allowParameters = { video, streamingPlaylist, videoFile }
87 102
@@ -131,9 +146,11 @@ function isVideoDownloadAllowed (_object: {
131function checkAllowResult (res: express.Response, allowParameters: any, result?: AllowedResult) { 146function checkAllowResult (res: express.Response, allowParameters: any, result?: AllowedResult) {
132 if (!result || result.allowed !== true) { 147 if (!result || result.allowed !== true) {
133 logger.info('Download is not allowed.', { result, allowParameters }) 148 logger.info('Download is not allowed.', { result, allowParameters })
134 res.status(HttpStatusCode.FORBIDDEN_403)
135 .json({ error: result?.errorMessage || 'Refused download' })
136 149
150 res.fail({
151 status: HttpStatusCode.FORBIDDEN_403,
152 message: result?.errorMessage || 'Refused download'
153 })
137 return false 154 return false
138 } 155 }
139 156
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) {
56 } 56 }
57 57
58 const image = await ActorImageModel.loadByName(filename) 58 const image = await ActorImageModel.loadByName(filename)
59 if (!image) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) 59 if (!image) return res.status(HttpStatusCode.NOT_FOUND_404).end()
60 60
61 if (image.onDisk === false) { 61 if (image.onDisk === false) {
62 if (!image.fileUrl) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) 62 if (!image.fileUrl) return res.status(HttpStatusCode.NOT_FOUND_404).end()
63 63
64 logger.info('Lazy serve remote actor image %s.', image.fileUrl) 64 logger.info('Lazy serve remote actor image %s.', image.fileUrl)
65 65
@@ -67,7 +67,7 @@ async function getActorImage (req: express.Request, res: express.Response) {
67 await pushActorImageProcessInQueue({ filename: image.filename, fileUrl: image.fileUrl, type: image.type }) 67 await pushActorImageProcessInQueue({ filename: image.filename, fileUrl: image.fileUrl, type: image.type })
68 } catch (err) { 68 } catch (err) {
69 logger.warn('Cannot process remote actor image %s.', image.fileUrl, { err }) 69 logger.warn('Cannot process remote actor image %s.', image.fileUrl, { err })
70 return res.sendStatus(HttpStatusCode.NOT_FOUND_404) 70 return res.status(HttpStatusCode.NOT_FOUND_404).end()
71 } 71 }
72 72
73 image.onDisk = true 73 image.onDisk = true
@@ -83,21 +83,21 @@ async function getActorImage (req: express.Request, res: express.Response) {
83 83
84async function getPreview (req: express.Request, res: express.Response) { 84async function getPreview (req: express.Request, res: express.Response) {
85 const result = await VideosPreviewCache.Instance.getFilePath(req.params.filename) 85 const result = await VideosPreviewCache.Instance.getFilePath(req.params.filename)
86 if (!result) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) 86 if (!result) return res.status(HttpStatusCode.NOT_FOUND_404).end()
87 87
88 return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE.LAZY_SERVER }) 88 return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE.LAZY_SERVER })
89} 89}
90 90
91async function getVideoCaption (req: express.Request, res: express.Response) { 91async function getVideoCaption (req: express.Request, res: express.Response) {
92 const result = await VideosCaptionCache.Instance.getFilePath(req.params.filename) 92 const result = await VideosCaptionCache.Instance.getFilePath(req.params.filename)
93 if (!result) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) 93 if (!result) return res.status(HttpStatusCode.NOT_FOUND_404).end()
94 94
95 return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE.LAZY_SERVER }) 95 return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE.LAZY_SERVER })
96} 96}
97 97
98async function getTorrent (req: express.Request, res: express.Response) { 98async function getTorrent (req: express.Request, res: express.Response) {
99 const result = await VideosTorrentCache.Instance.getFilePath(req.params.filename) 99 const result = await VideosTorrentCache.Instance.getFilePath(req.params.filename)
100 if (!result) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) 100 if (!result) return res.status(HttpStatusCode.NOT_FOUND_404).end()
101 101
102 // Torrents still use the old naming convention (video uuid + .torrent) 102 // Torrents still use the old naming convention (video uuid + .torrent)
103 return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE.SERVER }) 103 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) {
25 const result = LiveManager.Instance.getSegmentsSha256(videoUUID) 25 const result = LiveManager.Instance.getSegmentsSha256(videoUUID)
26 26
27 if (!result) { 27 if (!result) {
28 return res.sendStatus(HttpStatusCode.NOT_FOUND_404) 28 return res.status(HttpStatusCode.NOT_FOUND_404).end()
29 } 29 }
30 30
31 return res.json(mapToJSON(result)) 31 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) {
100 return res.json(json) 100 return res.json(json)
101 } 101 }
102 102
103 return res.sendStatus(HttpStatusCode.NOT_FOUND_404) 103 return res.status(HttpStatusCode.NOT_FOUND_404).end()
104} 104}
105 105
106function servePluginStaticDirectory (req: express.Request, res: express.Response) { 106function servePluginStaticDirectory (req: express.Request, res: express.Response) {
@@ -110,7 +110,7 @@ function servePluginStaticDirectory (req: express.Request, res: express.Response
110 const [ directory, ...file ] = staticEndpoint.split('/') 110 const [ directory, ...file ] = staticEndpoint.split('/')
111 111
112 const staticPath = plugin.staticDirs[directory] 112 const staticPath = plugin.staticDirs[directory]
113 if (!staticPath) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) 113 if (!staticPath) return res.status(HttpStatusCode.NOT_FOUND_404).end()
114 114
115 const filepath = file.join('/') 115 const filepath = file.join('/')
116 return res.sendFile(join(plugin.path, staticPath, filepath), sendFileOptions) 116 return res.sendFile(join(plugin.path, staticPath, filepath), sendFileOptions)
@@ -120,7 +120,7 @@ function servePluginCustomRoutes (req: express.Request, res: express.Response, n
120 const plugin: RegisteredPlugin = res.locals.registeredPlugin 120 const plugin: RegisteredPlugin = res.locals.registeredPlugin
121 const router = PluginManager.Instance.getRouter(plugin.npmName) 121 const router = PluginManager.Instance.getRouter(plugin.npmName)
122 122
123 if (!router) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) 123 if (!router) return res.status(HttpStatusCode.NOT_FOUND_404).end()
124 124
125 return router(req, res, next) 125 return router(req, res, next)
126} 126}
@@ -130,7 +130,7 @@ function servePluginClientScripts (req: express.Request, res: express.Response)
130 const staticEndpoint = req.params.staticEndpoint 130 const staticEndpoint = req.params.staticEndpoint
131 131
132 const file = plugin.clientScripts[staticEndpoint] 132 const file = plugin.clientScripts[staticEndpoint]
133 if (!file) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) 133 if (!file) return res.status(HttpStatusCode.NOT_FOUND_404).end()
134 134
135 return res.sendFile(join(plugin.path, staticEndpoint), sendFileOptions) 135 return res.sendFile(join(plugin.path, staticEndpoint), sendFileOptions)
136} 136}
@@ -140,7 +140,7 @@ function serveThemeCSSDirectory (req: express.Request, res: express.Response) {
140 const staticEndpoint = req.params.staticEndpoint 140 const staticEndpoint = req.params.staticEndpoint
141 141
142 if (plugin.css.includes(staticEndpoint) === false) { 142 if (plugin.css.includes(staticEndpoint) === false) {
143 return res.sendStatus(HttpStatusCode.NOT_FOUND_404) 143 return res.status(HttpStatusCode.NOT_FOUND_404).end()
144 } 144 }
145 145
146 return res.sendFile(join(plugin.path, staticEndpoint), sendFileOptions) 146 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) {
160 const { totalVideos } = await VideoModel.getStats() 160 const { totalVideos } = await VideoModel.getStats()
161 const { totalLocalVideoComments } = await VideoCommentModel.getStats() 161 const { totalLocalVideoComments } = await VideoCommentModel.getStats()
162 const { totalUsers, totalMonthlyActiveUsers, totalHalfYearActiveUsers } = await UserModel.getStats() 162 const { totalUsers, totalMonthlyActiveUsers, totalHalfYearActiveUsers } = await UserModel.getStats()
163 let json = {}
164 163
165 if (req.params.version && (req.params.version === '2.0')) { 164 if (req.params.version && (req.params.version === '2.0')) {
166 json = { 165 const json = {
167 version: '2.0', 166 version: '2.0',
168 software: { 167 software: {
169 name: 'peertube', 168 name: 'peertube',
@@ -291,12 +290,14 @@ async function generateNodeinfo (req: express.Request, res: express.Response) {
291 } 290 }
292 } as HttpNodeinfoDiasporaSoftwareNsSchema20 291 } as HttpNodeinfoDiasporaSoftwareNsSchema20
293 res.contentType('application/json; profile="http://nodeinfo.diaspora.software/ns/schema/2.0#"') 292 res.contentType('application/json; profile="http://nodeinfo.diaspora.software/ns/schema/2.0#"')
294 } else { 293 .send(json)
295 json = { error: 'Nodeinfo schema version not handled' } 294 .end()
296 res.status(HttpStatusCode.NOT_FOUND_404)
297 } 295 }
298 296
299 return res.send(json).end() 297 return res.fail({
298 status: HttpStatusCode.NOT_FOUND_404,
299 message: 'Nodeinfo schema version not handled'
300 })
300} 301}
301 302
302function getCup (req: express.Request, res: express.Response, next: express.NextFunction) { 303function 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
16 const videoComment = await VideoCommentModel.loadById(id) 16 const videoComment = await VideoCommentModel.loadById(id)
17 17
18 if (!videoComment) { 18 if (!videoComment) {
19 res.status(HttpStatusCode.NOT_FOUND_404) 19 res.fail({
20 .json({ error: 'Video comment thread not found' }) 20 status: HttpStatusCode.NOT_FOUND_404,
21 .end() 21 message: 'Video comment thread not found'
22 22 })
23 return false 23 return false
24 } 24 }
25 25
26 if (videoComment.videoId !== video.id) { 26 if (videoComment.videoId !== video.id) {
27 res.status(HttpStatusCode.BAD_REQUEST_400) 27 res.fail({ message: 'Video comment is not associated to this video.' })
28 .json({ error: 'Video comment is not associated to this video.' })
29 .end()
30
31 return false 28 return false
32 } 29 }
33 30
34 if (videoComment.inReplyToCommentId !== null) { 31 if (videoComment.inReplyToCommentId !== null) {
35 res.status(HttpStatusCode.BAD_REQUEST_400) 32 res.fail({ message: 'Video comment is not a thread.' })
36 .json({ error: 'Video comment is not a thread.' })
37 .end()
38
39 return false 33 return false
40 } 34 }
41 35
@@ -48,18 +42,15 @@ async function doesVideoCommentExist (idArg: number | string, video: MVideoId, r
48 const videoComment = await VideoCommentModel.loadByIdAndPopulateVideoAndAccountAndReply(id) 42 const videoComment = await VideoCommentModel.loadByIdAndPopulateVideoAndAccountAndReply(id)
49 43
50 if (!videoComment) { 44 if (!videoComment) {
51 res.status(HttpStatusCode.NOT_FOUND_404) 45 res.fail({
52 .json({ error: 'Video comment thread not found' }) 46 status: HttpStatusCode.NOT_FOUND_404,
53 .end() 47 message: 'Video comment thread not found'
54 48 })
55 return false 49 return false
56 } 50 }
57 51
58 if (videoComment.videoId !== video.id) { 52 if (videoComment.videoId !== video.id) {
59 res.status(HttpStatusCode.BAD_REQUEST_400) 53 res.fail({ message: 'Video comment is not associated to this video.' })
60 .json({ error: 'Video comment is not associated to this video.' })
61 .end()
62
63 return false 54 return false
64 } 55 }
65 56
@@ -72,14 +63,14 @@ async function doesCommentIdExist (idArg: number | string, res: express.Response
72 const videoComment = await VideoCommentModel.loadByIdAndPopulateVideoAndAccountAndReply(id) 63 const videoComment = await VideoCommentModel.loadByIdAndPopulateVideoAndAccountAndReply(id)
73 64
74 if (!videoComment) { 65 if (!videoComment) {
75 res.status(HttpStatusCode.NOT_FOUND_404) 66 res.fail({
76 .json({ error: 'Video comment thread not found' }) 67 status: HttpStatusCode.NOT_FOUND_404,
77 68 message: 'Video comment thread not found'
69 })
78 return false 70 return false
79 } 71 }
80 72
81 res.locals.videoCommentFull = videoComment 73 res.locals.videoCommentFull = videoComment
82
83 return true 74 return true
84} 75}
85 76
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) {
36 const videoImport = await VideoImportModel.loadAndPopulateVideo(id) 36 const videoImport = await VideoImportModel.loadAndPopulateVideo(id)
37 37
38 if (!videoImport) { 38 if (!videoImport) {
39 res.status(HttpStatusCode.NOT_FOUND_404) 39 res.fail({
40 .json({ error: 'Video import not found' }) 40 status: HttpStatusCode.NOT_FOUND_404,
41 .end() 41 message: 'Video import not found'
42 42 })
43 return false 43 return false
44 } 44 }
45 45
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
9 const videoChangeOwnership = await VideoChangeOwnershipModel.load(id) 9 const videoChangeOwnership = await VideoChangeOwnershipModel.load(id)
10 10
11 if (!videoChangeOwnership) { 11 if (!videoChangeOwnership) {
12 res.status(HttpStatusCode.NOT_FOUND_404) 12 res.fail({
13 .json({ error: 'Video change ownership not found' }) 13 status: HttpStatusCode.NOT_FOUND_404,
14 .end() 14 message: 'Video change ownership not found'
15 15 })
16 return false 16 return false
17 } 17 }
18 18
@@ -25,8 +25,9 @@ export function checkUserCanTerminateOwnershipChange (user: MUserId, videoChange
25 return true 25 return true
26 } 26 }
27 27
28 res.status(HttpStatusCode.FORBIDDEN_403) 28 res.fail({
29 .json({ error: 'Cannot terminate an ownership change of another user' }) 29 status: HttpStatusCode.FORBIDDEN_403,
30 .end() 30 message: 'Cannot terminate an ownership change of another user'
31 })
31 return false 32 return false
32} 33}
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'
8import { logger } from './logger' 8import { logger } from './logger'
9import { deleteFileAndCatch, generateRandomString } from './utils' 9import { deleteFileAndCatch, generateRandomString } from './utils'
10import { getExtFromMimetype } from './video' 10import { getExtFromMimetype } from './video'
11import { ProblemDocument, ProblemDocumentExtension } from 'http-problem-details'
11 12
12function buildNSFWFilter (res?: express.Response, paramNSFW?: string) { 13function buildNSFWFilter (res?: express.Response, paramNSFW?: string) {
13 if (paramNSFW === 'true') return true 14 if (paramNSFW === 'true') return true
@@ -125,6 +126,34 @@ function getCountVideos (req: express.Request) {
125 return req.query.skipCount !== true 126 return req.query.skipCount !== true
126} 127}
127 128
129// helpers added in server.ts and used in subsequent controllers used
130const apiResponseHelpers = (req, res: express.Response, next = null) => {
131 res.fail = (options) => {
132 const { data, status, message, title, type, docs, instance } = {
133 data: null,
134 status: HttpStatusCode.BAD_REQUEST_400,
135 ...options
136 }
137
138 const extension = new ProblemDocumentExtension({
139 ...data,
140 docs: docs || res.docs
141 })
142
143 res.status(status)
144 res.setHeader('Content-Type', 'application/problem+json')
145 res.json(new ProblemDocument({
146 status,
147 title,
148 instance,
149 type: type && '' + type,
150 detail: message
151 }, extension))
152 }
153
154 if (next !== null) next()
155}
156
128// --------------------------------------------------------------------------- 157// ---------------------------------------------------------------------------
129 158
130export { 159export {
@@ -134,5 +163,6 @@ export {
134 badRequest, 163 badRequest,
135 createReqFiles, 164 createReqFiles,
136 cleanUpReqFiles, 165 cleanUpReqFiles,
137 getCountVideos 166 getCountVideos,
167 apiResponseHelpers
138} 168}
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) {
6 const abuse = await AbuseModel.loadByIdWithReporter(parseInt(abuseId + '', 10)) 6 const abuse = await AbuseModel.loadByIdWithReporter(parseInt(abuseId + '', 10))
7 7
8 if (!abuse) { 8 if (!abuse) {
9 res.status(HttpStatusCode.NOT_FOUND_404) 9 res.fail({
10 .json({ error: 'Abuse not found' }) 10 status: HttpStatusCode.NOT_FOUND_404,
11 message: 'Abuse not found'
12 })
11 13
12 return false 14 return false
13 } 15 }
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<MAccountDefault>, res: Response, sen
27 27
28 if (!account) { 28 if (!account) {
29 if (sendNotFound === true) { 29 if (sendNotFound === true) {
30 res.status(HttpStatusCode.NOT_FOUND_404) 30 res.fail({
31 .json({ error: 'Account not found' }) 31 status: HttpStatusCode.NOT_FOUND_404,
32 message: 'Account not found'
33 })
32 } 34 }
33
34 return false 35 return false
35 } 36 }
36 37
37 res.locals.account = account 38 res.locals.account = account
38
39 return true 39 return true
40} 40}
41 41
@@ -43,14 +43,14 @@ async function doesUserFeedTokenCorrespond (id: number, token: string, res: Resp
43 const user = await UserModel.loadByIdWithChannels(parseInt(id + '', 10)) 43 const user = await UserModel.loadByIdWithChannels(parseInt(id + '', 10))
44 44
45 if (token !== user.feedToken) { 45 if (token !== user.feedToken) {
46 res.status(HttpStatusCode.FORBIDDEN_403) 46 res.fail({
47 .json({ error: 'User and token mismatch' }) 47 status: HttpStatusCode.FORBIDDEN_403,
48 48 message: 'User and token mismatch'
49 })
49 return false 50 return false
50 } 51 }
51 52
52 res.locals.user = user 53 res.locals.user = user
53
54 return true 54 return true
55} 55}
56 56
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) {
6 const videoBlacklist = await VideoBlacklistModel.loadByVideoId(videoId) 6 const videoBlacklist = await VideoBlacklistModel.loadByVideoId(videoId)
7 7
8 if (videoBlacklist === null) { 8 if (videoBlacklist === null) {
9 res.status(HttpStatusCode.NOT_FOUND_404) 9 res.fail({
10 .json({ error: 'Blacklisted video not found' }) 10 status: HttpStatusCode.NOT_FOUND_404,
11 .end() 11 message: 'Blacklisted video not found'
12 12 })
13 return false 13 return false
14 } 14 }
15 15
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
7 const videoCaption = await VideoCaptionModel.loadByVideoIdAndLanguage(video.id, language) 7 const videoCaption = await VideoCaptionModel.loadByVideoIdAndLanguage(video.id, language)
8 8
9 if (!videoCaption) { 9 if (!videoCaption) {
10 res.status(HttpStatusCode.NOT_FOUND_404) 10 res.fail({
11 .json({ error: 'Video caption not found' }) 11 status: HttpStatusCode.NOT_FOUND_404,
12 12 message: 'Video caption not found'
13 })
13 return false 14 return false
14 } 15 }
15 16
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 {
31 31
32function processVideoChannelExist (videoChannel: MChannelBannerAccountDefault, res: express.Response) { 32function processVideoChannelExist (videoChannel: MChannelBannerAccountDefault, res: express.Response) {
33 if (!videoChannel) { 33 if (!videoChannel) {
34 res.status(HttpStatusCode.NOT_FOUND_404) 34 res.fail({
35 .json({ error: 'Video channel not found' }) 35 status: HttpStatusCode.NOT_FOUND_404,
36 36 message: 'Video channel not found'
37 })
37 return false 38 return false
38 } 39 }
39 40
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 {
28 28
29function handleVideoPlaylist (videoPlaylist: MVideoPlaylist, res: express.Response) { 29function handleVideoPlaylist (videoPlaylist: MVideoPlaylist, res: express.Response) {
30 if (!videoPlaylist) { 30 if (!videoPlaylist) {
31 res.status(HttpStatusCode.NOT_FOUND_404) 31 res.fail({
32 .json({ error: 'Video playlist not found' }) 32 status: HttpStatusCode.NOT_FOUND_404,
33 .end() 33 message: 'Video playlist not found'
34 34 })
35 return false 35 return false
36 } 36 }
37 37
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
21 const video = await fetchVideo(id, fetchType, userId) 21 const video = await fetchVideo(id, fetchType, userId)
22 22
23 if (video === null) { 23 if (video === null) {
24 res.status(HttpStatusCode.NOT_FOUND_404) 24 res.fail({
25 .json({ error: 'Video not found' }) 25 status: HttpStatusCode.NOT_FOUND_404,
26 .end() 26 message: 'Video not found'
27 27 })
28 return false 28 return false
29 } 29 }
30 30
@@ -55,10 +55,10 @@ async function doesVideoExist (id: number | string, res: Response, fetchType: Vi
55 55
56async function doesVideoFileOfVideoExist (id: number, videoIdOrUUID: number | string, res: Response) { 56async function doesVideoFileOfVideoExist (id: number, videoIdOrUUID: number | string, res: Response) {
57 if (!await VideoFileModel.doesVideoExistForVideoFile(id, videoIdOrUUID)) { 57 if (!await VideoFileModel.doesVideoExistForVideoFile(id, videoIdOrUUID)) {
58 res.status(HttpStatusCode.NOT_FOUND_404) 58 res.fail({
59 .json({ error: 'VideoFile matching Video not found' }) 59 status: HttpStatusCode.NOT_FOUND_404,
60 .end() 60 message: 'VideoFile matching Video not found'
61 61 })
62 return false 62 return false
63 } 63 }
64 64
@@ -69,9 +69,7 @@ async function doesVideoChannelOfAccountExist (channelId: number, user: MUserAcc
69 const videoChannel = await VideoChannelModel.loadAndPopulateAccount(channelId) 69 const videoChannel = await VideoChannelModel.loadAndPopulateAccount(channelId)
70 70
71 if (videoChannel === null) { 71 if (videoChannel === null) {
72 res.status(HttpStatusCode.BAD_REQUEST_400) 72 res.fail({ message: 'Unknown video "video channel" for this instance.' })
73 .json({ error: 'Unknown video "video channel" for this instance.' })
74
75 return false 73 return false
76 } 74 }
77 75
@@ -82,9 +80,9 @@ async function doesVideoChannelOfAccountExist (channelId: number, user: MUserAcc
82 } 80 }
83 81
84 if (videoChannel.Account.id !== user.Account.id) { 82 if (videoChannel.Account.id !== user.Account.id) {
85 res.status(HttpStatusCode.BAD_REQUEST_400) 83 res.fail({
86 .json({ error: 'Unknown video "video channel" for this account.' }) 84 message: 'Unknown video "video channel" for this account.'
87 85 })
88 return false 86 return false
89 } 87 }
90 88
@@ -95,9 +93,10 @@ async function doesVideoChannelOfAccountExist (channelId: number, user: MUserAcc
95function checkUserCanManageVideo (user: MUser, video: MVideoAccountLight, right: UserRight, res: Response, onlyOwned = true) { 93function checkUserCanManageVideo (user: MUser, video: MVideoAccountLight, right: UserRight, res: Response, onlyOwned = true) {
96 // Retrieve the user who did the request 94 // Retrieve the user who did the request
97 if (onlyOwned && video.isOwned() === false) { 95 if (onlyOwned && video.isOwned() === false) {
98 res.status(HttpStatusCode.FORBIDDEN_403) 96 res.fail({
99 .json({ error: 'Cannot manage a video of another server.' }) 97 status: HttpStatusCode.FORBIDDEN_403,
100 .end() 98 message: 'Cannot manage a video of another server.'
99 })
101 return false 100 return false
102 } 101 }
103 102
@@ -106,9 +105,10 @@ function checkUserCanManageVideo (user: MUser, video: MVideoAccountLight, right:
106 // Or if s/he is the video's account 105 // Or if s/he is the video's account
107 const account = video.VideoChannel.Account 106 const account = video.VideoChannel.Account
108 if (user.hasRight(right) === false && account.userId !== user.id) { 107 if (user.hasRight(right) === false && account.userId !== user.id) {
109 res.status(HttpStatusCode.FORBIDDEN_403) 108 res.fail({
110 .json({ error: 'Cannot manage a video of another user.' }) 109 status: HttpStatusCode.FORBIDDEN_403,
111 .end() 110 message: 'Cannot manage a video of another user.'
111 })
112 return false 112 return false
113 } 113 }
114 114
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) {
549 return 549 return
550 } catch (err) { 550 } catch (err) {
551 logger.error('Cannot generate HTML page.', err) 551 logger.error('Cannot generate HTML page.', err)
552 return res.sendStatus(HttpStatusCode.INTERNAL_SERVER_ERROR_500) 552 return res.status(HttpStatusCode.INTERNAL_SERVER_ERROR_500).end()
553 } 553 }
554 } 554 }
555 555
556 return res.sendStatus(HttpStatusCode.NOT_ACCEPTABLE_406) 556 return res.status(HttpStatusCode.NOT_ACCEPTABLE_406).end()
557} 557}
558 558
559// --------------------------------------------------------------------------- 559// ---------------------------------------------------------------------------
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)
29 const activity: ActivityDelete = req.body 29 const activity: ActivityDelete = req.body
30 if (isActorDeleteActivityValid(activity) && activity.object === activity.actor) { 30 if (isActorDeleteActivityValid(activity) && activity.object === activity.actor) {
31 logger.debug('Handling signature error on actor delete activity', { err }) 31 logger.debug('Handling signature error on actor delete activity', { err })
32 return res.sendStatus(HttpStatusCode.NO_CONTENT_204) 32 return res.status(HttpStatusCode.NO_CONTENT_204).end()
33 } 33 }
34 34
35 logger.warn('Error in ActivityPub signature checker.', { err }) 35 logger.warn('Error in ActivityPub signature checker.', { err })
36 return res.sendStatus(HttpStatusCode.FORBIDDEN_403) 36 return res.fail({
37 status: HttpStatusCode.FORBIDDEN_403,
38 message: 'ActivityPub signature could not be checked'
39 })
37 } 40 }
38} 41}
39 42
@@ -71,13 +74,22 @@ async function checkHttpSignature (req: Request, res: Response) {
71 } catch (err) { 74 } catch (err) {
72 logger.warn('Invalid signature because of exception in signature parser', { reqBody: req.body, err }) 75 logger.warn('Invalid signature because of exception in signature parser', { reqBody: req.body, err })
73 76
74 res.status(HttpStatusCode.FORBIDDEN_403).json({ error: err.message }) 77 res.fail({
78 status: HttpStatusCode.FORBIDDEN_403,
79 message: err.message
80 })
75 return false 81 return false
76 } 82 }
77 83
78 const keyId = parsed.keyId 84 const keyId = parsed.keyId
79 if (!keyId) { 85 if (!keyId) {
80 res.sendStatus(HttpStatusCode.FORBIDDEN_403) 86 res.fail({
87 status: HttpStatusCode.FORBIDDEN_403,
88 message: 'Invalid key ID',
89 data: {
90 keyId
91 }
92 })
81 return false 93 return false
82 } 94 }
83 95
@@ -94,12 +106,17 @@ async function checkHttpSignature (req: Request, res: Response) {
94 if (verified !== true) { 106 if (verified !== true) {
95 logger.warn('Signature from %s is invalid', actorUrl, { parsed }) 107 logger.warn('Signature from %s is invalid', actorUrl, { parsed })
96 108
97 res.sendStatus(HttpStatusCode.FORBIDDEN_403) 109 res.fail({
110 status: HttpStatusCode.FORBIDDEN_403,
111 message: 'Invalid signature',
112 data: {
113 actorUrl
114 }
115 })
98 return false 116 return false
99 } 117 }
100 118
101 res.locals.signature = { actor } 119 res.locals.signature = { actor }
102
103 return true 120 return true
104} 121}
105 122
@@ -107,7 +124,10 @@ async function checkJsonLDSignature (req: Request, res: Response) {
107 const signatureObject: ActivityPubSignature = req.body.signature 124 const signatureObject: ActivityPubSignature = req.body.signature
108 125
109 if (!signatureObject || !signatureObject.creator) { 126 if (!signatureObject || !signatureObject.creator) {
110 res.sendStatus(HttpStatusCode.FORBIDDEN_403) 127 res.fail({
128 status: HttpStatusCode.FORBIDDEN_403,
129 message: 'Object and creator signature do not match'
130 })
111 return false 131 return false
112 } 132 }
113 133
@@ -121,11 +141,13 @@ async function checkJsonLDSignature (req: Request, res: Response) {
121 if (verified !== true) { 141 if (verified !== true) {
122 logger.warn('Signature not verified.', req.body) 142 logger.warn('Signature not verified.', req.body)
123 143
124 res.sendStatus(HttpStatusCode.FORBIDDEN_403) 144 res.fail({
145 status: HttpStatusCode.FORBIDDEN_403,
146 message: 'Signature could not be verified'
147 })
125 return false 148 return false
126 } 149 }
127 150
128 res.locals.signature = { actor } 151 res.locals.signature = { actor }
129
130 return true 152 return true
131} 153}
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
16 .catch(err => { 16 .catch(err => {
17 logger.warn('Cannot authenticate.', { err }) 17 logger.warn('Cannot authenticate.', { err })
18 18
19 return res.status(err.status) 19 return res.fail({
20 .json({ 20 status: err.status,
21 error: 'Token is invalid.', 21 message: 'Token is invalid',
22 code: err.name 22 type: err.name
23 }) 23 })
24 }) 24 })
25} 25}
26 26
@@ -52,7 +52,12 @@ function authenticatePromiseIfNeeded (req: express.Request, res: express.Respons
52 // Already authenticated? (or tried to) 52 // Already authenticated? (or tried to)
53 if (res.locals.oauth?.token.User) return resolve() 53 if (res.locals.oauth?.token.User) return resolve()
54 54
55 if (res.locals.authenticated === false) return res.sendStatus(HttpStatusCode.UNAUTHORIZED_401) 55 if (res.locals.authenticated === false) {
56 return res.fail({
57 status: HttpStatusCode.UNAUTHORIZED_401,
58 message: 'Not authenticated'
59 })
60 }
56 61
57 authenticate(req, res, () => resolve(), authenticateInQuery) 62 authenticate(req, res, () => resolve(), authenticateInQuery)
58 }) 63 })
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
10 10
11 // Problem with the url parsing? 11 // Problem with the url parsing?
12 if (hostWithPort === null) { 12 if (hostWithPort === null) {
13 return res.sendStatus(HttpStatusCode.INTERNAL_SERVER_ERROR_500) 13 return res.fail({
14 status: HttpStatusCode.INTERNAL_SERVER_ERROR_500,
15 message: 'Could not parse hosts'
16 })
14 } 17 }
15 18
16 req.body.hosts[i] = hostWithPort 19 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) {
10 const message = `User ${user.username} does not have right ${userRight} to access to ${req.path}.` 10 const message = `User ${user.username} does not have right ${userRight} to access to ${req.path}.`
11 logger.info(message) 11 logger.info(message)
12 12
13 return res.status(HttpStatusCode.FORBIDDEN_403) 13 return res.fail({
14 .json({ error: message }) 14 status: HttpStatusCode.FORBIDDEN_403,
15 message
16 })
15 } 17 }
16 18
17 return next() 19 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 = [
71 if (body.comment?.id && !await doesCommentIdExist(body.comment.id, res)) return 71 if (body.comment?.id && !await doesCommentIdExist(body.comment.id, res)) return
72 72
73 if (!body.video?.id && !body.account?.id && !body.comment?.id) { 73 if (!body.video?.id && !body.account?.id && !body.comment?.id) {
74 res.status(HttpStatusCode.BAD_REQUEST_400) 74 res.fail({ message: 'video id or account id or comment id is required.' })
75 .json({ error: 'video id or account id or comment id is required.' })
76
77 return 75 return
78 } 76 }
79 77
@@ -195,8 +193,10 @@ const getAbuseValidator = [
195 const message = `User ${user.username} does not have right to get abuse ${abuse.id}` 193 const message = `User ${user.username} does not have right to get abuse ${abuse.id}`
196 logger.warn(message) 194 logger.warn(message)
197 195
198 return res.status(HttpStatusCode.FORBIDDEN_403) 196 return res.fail({
199 .json({ error: message }) 197 status: HttpStatusCode.FORBIDDEN_403,
198 message
199 })
200 } 200 }
201 201
202 return next() 202 return next()
@@ -209,10 +209,7 @@ const checkAbuseValidForMessagesValidator = [
209 209
210 const abuse = res.locals.abuse 210 const abuse = res.locals.abuse
211 if (abuse.ReporterAccount.isOwned() === false) { 211 if (abuse.ReporterAccount.isOwned() === false) {
212 return res.status(HttpStatusCode.BAD_REQUEST_400) 212 return res.fail({ message: 'This abuse was created by a user of your instance.' })
213 .json({
214 error: 'This abuse was created by a user of your instance.'
215 })
216 } 213 }
217 214
218 return next() 215 return next()
@@ -246,13 +243,17 @@ const deleteAbuseMessageValidator = [
246 const abuseMessage = await AbuseMessageModel.loadByIdAndAbuseId(messageId, abuse.id) 243 const abuseMessage = await AbuseMessageModel.loadByIdAndAbuseId(messageId, abuse.id)
247 244
248 if (!abuseMessage) { 245 if (!abuseMessage) {
249 return res.status(HttpStatusCode.NOT_FOUND_404) 246 return res.fail({
250 .json({ error: 'Abuse message not found' }) 247 status: HttpStatusCode.NOT_FOUND_404,
248 message: 'Abuse message not found'
249 })
251 } 250 }
252 251
253 if (user.hasRight(UserRight.MANAGE_ABUSES) !== true && abuseMessage.accountId !== user.Account.id) { 252 if (user.hasRight(UserRight.MANAGE_ABUSES) !== true && abuseMessage.accountId !== user.Account.id) {
254 return res.status(HttpStatusCode.FORBIDDEN_403) 253 return res.fail({
255 .json({ error: 'Cannot delete this abuse message' }) 254 status: HttpStatusCode.FORBIDDEN_403,
255 message: 'Cannot delete this abuse message'
256 })
256 } 257 }
257 258
258 res.locals.abuseMessage = abuseMessage 259 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
10 if (!isRootActivityValid(req.body)) { 10 if (!isRootActivityValid(req.body)) {
11 logger.warn('Incorrect activity parameters.', { activity: req.body }) 11 logger.warn('Incorrect activity parameters.', { activity: req.body })
12 return res.status(HttpStatusCode.BAD_REQUEST_400) 12 return res.status(HttpStatusCode.BAD_REQUEST_400)
13 .json({ error: 'Incorrect activity.' }) 13 .end()
14 } 14 }
15 15
16 const serverActor = await getServerActor() 16 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 = [
24 const accountToBlock = res.locals.account 24 const accountToBlock = res.locals.account
25 25
26 if (user.Account.id === accountToBlock.id) { 26 if (user.Account.id === accountToBlock.id) {
27 res.status(HttpStatusCode.CONFLICT_409) 27 res.fail({
28 .json({ error: 'You cannot block yourself.' }) 28 status: HttpStatusCode.CONFLICT_409,
29 29 message: 'You cannot block yourself.'
30 })
30 return 31 return
31 } 32 }
32 33
@@ -79,8 +80,10 @@ const blockServerValidator = [
79 const host: string = req.body.host 80 const host: string = req.body.host
80 81
81 if (host === WEBSERVER.HOST) { 82 if (host === WEBSERVER.HOST) {
82 return res.status(HttpStatusCode.CONFLICT_409) 83 return res.fail({
83 .json({ error: 'You cannot block your own server.' }) 84 status: HttpStatusCode.CONFLICT_409,
85 message: 'You cannot block your own server.'
86 })
84 } 87 }
85 88
86 const server = await ServerModel.loadOrCreateByHost(host) 89 const server = await ServerModel.loadOrCreateByHost(host)
@@ -137,27 +140,27 @@ export {
137async function doesUnblockAccountExist (accountId: number, targetAccountId: number, res: express.Response) { 140async function doesUnblockAccountExist (accountId: number, targetAccountId: number, res: express.Response) {
138 const accountBlock = await AccountBlocklistModel.loadByAccountAndTarget(accountId, targetAccountId) 141 const accountBlock = await AccountBlocklistModel.loadByAccountAndTarget(accountId, targetAccountId)
139 if (!accountBlock) { 142 if (!accountBlock) {
140 res.status(HttpStatusCode.NOT_FOUND_404) 143 res.fail({
141 .json({ error: 'Account block entry not found.' }) 144 status: HttpStatusCode.NOT_FOUND_404,
142 145 message: 'Account block entry not found.'
146 })
143 return false 147 return false
144 } 148 }
145 149
146 res.locals.accountBlock = accountBlock 150 res.locals.accountBlock = accountBlock
147
148 return true 151 return true
149} 152}
150 153
151async function doesUnblockServerExist (accountId: number, host: string, res: express.Response) { 154async function doesUnblockServerExist (accountId: number, host: string, res: express.Response) {
152 const serverBlock = await ServerBlocklistModel.loadByAccountAndHost(accountId, host) 155 const serverBlock = await ServerBlocklistModel.loadByAccountAndHost(accountId, host)
153 if (!serverBlock) { 156 if (!serverBlock) {
154 res.status(HttpStatusCode.NOT_FOUND_404) 157 res.fail({
155 .json({ error: 'Server block entry not found.' }) 158 status: HttpStatusCode.NOT_FOUND_404,
156 159 message: 'Server block entry not found.'
160 })
157 return false 161 return false
158 } 162 }
159 163
160 res.locals.serverBlock = serverBlock 164 res.locals.serverBlock = serverBlock
161
162 return true 165 return true
163} 166}
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 = [
23 const body = req.body as BulkRemoveCommentsOfBody 23 const body = req.body as BulkRemoveCommentsOfBody
24 24
25 if (body.scope === 'instance' && user.hasRight(UserRight.REMOVE_ANY_VIDEO_COMMENT) !== true) { 25 if (body.scope === 'instance' && user.hasRight(UserRight.REMOVE_ANY_VIDEO_COMMENT) !== true) {
26 return res.status(HttpStatusCode.FORBIDDEN_403) 26 return res.fail({
27 .json({ 27 status: HttpStatusCode.FORBIDDEN_403,
28 error: 'User cannot remove any comments of this instance.' 28 message: 'User cannot remove any comments of this instance.'
29 }) 29 })
30 } 30 }
31 31
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'
2import { body } from 'express-validator' 2import { body } from 'express-validator'
3import { isIntOrNull } from '@server/helpers/custom-validators/misc' 3import { isIntOrNull } from '@server/helpers/custom-validators/misc'
4import { isEmailEnabled } from '@server/initializers/config' 4import { isEmailEnabled } from '@server/initializers/config'
5import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
6import { CustomConfig } from '../../../shared/models/server/custom-config.model' 5import { CustomConfig } from '../../../shared/models/server/custom-config.model'
7import { isThemeNameValid } from '../../helpers/custom-validators/plugins' 6import { isThemeNameValid } from '../../helpers/custom-validators/plugins'
8import { isUserNSFWPolicyValid, isUserVideoQuotaDailyValid, isUserVideoQuotaValid } from '../../helpers/custom-validators/users' 7import { isUserNSFWPolicyValid, isUserVideoQuotaDailyValid, isUserVideoQuotaValid } from '../../helpers/custom-validators/users'
@@ -115,9 +114,7 @@ function checkInvalidConfigIfEmailDisabled (customConfig: CustomConfig, res: exp
115 if (isEmailEnabled()) return true 114 if (isEmailEnabled()) return true
116 115
117 if (customConfig.signup.requiresEmailVerification === true) { 116 if (customConfig.signup.requiresEmailVerification === true) {
118 res.status(HttpStatusCode.BAD_REQUEST_400) 117 res.fail({ message: 'Emailer is disabled but you require signup email verification.' })
119 .send({ error: 'Emailer is disabled but you require signup email verification.' })
120 .end()
121 return false 118 return false
122 } 119 }
123 120
@@ -128,9 +125,7 @@ function checkInvalidTranscodingConfig (customConfig: CustomConfig, res: express
128 if (customConfig.transcoding.enabled === false) return true 125 if (customConfig.transcoding.enabled === false) return true
129 126
130 if (customConfig.transcoding.webtorrent.enabled === false && customConfig.transcoding.hls.enabled === false) { 127 if (customConfig.transcoding.webtorrent.enabled === false && customConfig.transcoding.hls.enabled === false) {
131 res.status(HttpStatusCode.BAD_REQUEST_400) 128 res.fail({ message: 'You need to enable at least webtorrent transcoding or hls transcoding' })
132 .send({ error: 'You need to enable at least webtorrent transcoding or hls transcoding' })
133 .end()
134 return false 129 return false
135 } 130 }
136 131
@@ -141,9 +136,7 @@ function checkInvalidLiveConfig (customConfig: CustomConfig, res: express.Respon
141 if (customConfig.live.enabled === false) return true 136 if (customConfig.live.enabled === false) return true
142 137
143 if (customConfig.live.allowReplay === true && customConfig.transcoding.enabled === false) { 138 if (customConfig.live.allowReplay === true && customConfig.transcoding.enabled === false) {
144 res.status(HttpStatusCode.BAD_REQUEST_400) 139 res.fail({ message: 'You cannot allow live replay if transcoding is not enabled' })
145 .send({ error: 'You cannot allow live replay if transcoding is not enabled' })
146 .end()
147 return false 140 return false
148 } 141 }
149 142
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,
36 if (req.accepts(acceptableContentTypes)) { 36 if (req.accepts(acceptableContentTypes)) {
37 res.set('Content-Type', req.accepts(acceptableContentTypes) as string) 37 res.set('Content-Type', req.accepts(acceptableContentTypes) as string)
38 } else { 38 } else {
39 return res.status(HttpStatusCode.NOT_ACCEPTABLE_406) 39 return res.fail({
40 .json({ 40 status: HttpStatusCode.NOT_ACCEPTABLE_406,
41 message: `You should accept at least one of the following content-types: ${acceptableContentTypes.join(', ')}` 41 message: `You should accept at least one of the following content-types: ${acceptableContentTypes.join(', ')}`
42 }) 42 })
43 } 43 }
44 44
45 return next() 45 return next()
@@ -106,10 +106,7 @@ const videoCommentsFeedsValidator = [
106 if (areValidationErrors(req, res)) return 106 if (areValidationErrors(req, res)) return
107 107
108 if (req.query.videoId && (req.query.videoChannelId || req.query.videoChannelName)) { 108 if (req.query.videoId && (req.query.videoChannelId || req.query.videoChannelName)) {
109 return res.status(HttpStatusCode.BAD_REQUEST_400) 109 return res.fail({ message: 'videoId cannot be mixed with a channel filter' })
110 .json({
111 message: 'videoId cannot be mixed with a channel filter'
112 })
113 } 110 }
114 111
115 if (req.query.videoId && !await doesVideoExist(req.query.videoId, res)) return 112 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 = [
63 const follow = await ActorFollowModel.loadByActorAndTargetNameAndHostForAPI(serverActor.id, SERVER_ACTOR_NAME, req.params.host) 63 const follow = await ActorFollowModel.loadByActorAndTargetNameAndHostForAPI(serverActor.id, SERVER_ACTOR_NAME, req.params.host)
64 64
65 if (!follow) { 65 if (!follow) {
66 return res 66 return res.fail({
67 .status(HttpStatusCode.NOT_FOUND_404) 67 status: HttpStatusCode.NOT_FOUND_404,
68 .json({ 68 message: `Following ${req.params.host} not found.`
69 error: `Following ${req.params.host} not found.` 69 })
70 })
71 } 70 }
72 71
73 res.locals.follow = follow 72 res.locals.follow = follow
@@ -95,12 +94,10 @@ const getFollowerValidator = [
95 } 94 }
96 95
97 if (!follow) { 96 if (!follow) {
98 return res 97 return res.fail({
99 .status(HttpStatusCode.NOT_FOUND_404) 98 status: HttpStatusCode.NOT_FOUND_404,
100 .json({ 99 message: `Follower ${req.params.nameWithHost} not found.`
101 error: `Follower ${req.params.nameWithHost} not found.` 100 })
102 })
103 .end()
104 } 101 }
105 102
106 res.locals.follow = follow 103 res.locals.follow = follow
@@ -114,12 +111,7 @@ const acceptOrRejectFollowerValidator = [
114 111
115 const follow = res.locals.follow 112 const follow = res.locals.follow
116 if (follow.state !== 'pending') { 113 if (follow.state !== 'pending') {
117 return res 114 return res.fail({ message: 'Follow is not in pending state.' })
118 .status(HttpStatusCode.BAD_REQUEST_400)
119 .json({
120 error: 'Follow is not in pending state.'
121 })
122 .end()
123 } 115 }
124 116
125 return next() 117 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 = [
51 if (areValidationErrors(req, res)) return 51 if (areValidationErrors(req, res)) return
52 52
53 if (req.query.format !== undefined && req.query.format !== 'json') { 53 if (req.query.format !== undefined && req.query.format !== 'json') {
54 return res.status(HttpStatusCode.NOT_IMPLEMENTED_501) 54 return res.fail({
55 .json({ error: 'Requested format is not implemented on server.' }) 55 status: HttpStatusCode.NOT_IMPLEMENTED_501,
56 message: 'Requested format is not implemented on server.',
57 data: {
58 format: req.query.format
59 }
60 })
56 } 61 }
57 62
58 const url = req.query.url as string 63 const url = req.query.url as string
@@ -65,27 +70,35 @@ const oembedValidator = [
65 const matches = watchRegex.exec(url) 70 const matches = watchRegex.exec(url)
66 71
67 if (startIsOk === false || matches === null) { 72 if (startIsOk === false || matches === null) {
68 return res.status(HttpStatusCode.BAD_REQUEST_400) 73 return res.fail({
69 .json({ error: 'Invalid url.' }) 74 status: HttpStatusCode.BAD_REQUEST_400,
75 message: 'Invalid url.',
76 data: {
77 url
78 }
79 })
70 } 80 }
71 81
72 const elementId = matches[1] 82 const elementId = matches[1]
73 if (isIdOrUUIDValid(elementId) === false) { 83 if (isIdOrUUIDValid(elementId) === false) {
74 return res.status(HttpStatusCode.BAD_REQUEST_400) 84 return res.fail({ message: 'Invalid video or playlist id.' })
75 .json({ error: 'Invalid video or playlist id.' })
76 } 85 }
77 86
78 if (isVideo) { 87 if (isVideo) {
79 const video = await fetchVideo(elementId, 'all') 88 const video = await fetchVideo(elementId, 'all')
80 89
81 if (!video) { 90 if (!video) {
82 return res.status(HttpStatusCode.NOT_FOUND_404) 91 return res.fail({
83 .json({ error: 'Video not found' }) 92 status: HttpStatusCode.NOT_FOUND_404,
93 message: 'Video not found'
94 })
84 } 95 }
85 96
86 if (video.privacy !== VideoPrivacy.PUBLIC) { 97 if (video.privacy !== VideoPrivacy.PUBLIC) {
87 return res.status(HttpStatusCode.FORBIDDEN_403) 98 return res.fail({
88 .json({ error: 'Video is not public' }) 99 status: HttpStatusCode.FORBIDDEN_403,
100 message: 'Video is not public'
101 })
89 } 102 }
90 103
91 res.locals.videoAll = video 104 res.locals.videoAll = video
@@ -96,13 +109,17 @@ const oembedValidator = [
96 109
97 const videoPlaylist = await VideoPlaylistModel.loadWithAccountAndChannelSummary(elementId, undefined) 110 const videoPlaylist = await VideoPlaylistModel.loadWithAccountAndChannelSummary(elementId, undefined)
98 if (!videoPlaylist) { 111 if (!videoPlaylist) {
99 return res.status(HttpStatusCode.NOT_FOUND_404) 112 return res.fail({
100 .json({ error: 'Video playlist not found' }) 113 status: HttpStatusCode.NOT_FOUND_404,
114 message: 'Video playlist not found'
115 })
101 } 116 }
102 117
103 if (videoPlaylist.privacy !== VideoPlaylistPrivacy.PUBLIC) { 118 if (videoPlaylist.privacy !== VideoPlaylistPrivacy.PUBLIC) {
104 return res.status(HttpStatusCode.FORBIDDEN_403) 119 return res.fail({
105 .json({ error: 'Playlist is not public' }) 120 status: HttpStatusCode.FORBIDDEN_403,
121 message: 'Playlist is not public'
122 })
106 } 123 }
107 124
108 res.locals.videoPlaylistSummary = videoPlaylist 125 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) => {
31 const npmName = PluginModel.buildNpmName(req.params.pluginName, pluginType) 31 const npmName = PluginModel.buildNpmName(req.params.pluginName, pluginType)
32 const plugin = PluginManager.Instance.getRegisteredPluginOrTheme(npmName) 32 const plugin = PluginManager.Instance.getRegisteredPluginOrTheme(npmName)
33 33
34 if (!plugin) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) 34 if (!plugin) {
35 if (withVersion && plugin.version !== req.params.pluginVersion) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) 35 return res.fail({
36 status: HttpStatusCode.NOT_FOUND_404,
37 message: 'No plugin found named ' + npmName
38 })
39 }
40 if (withVersion && plugin.version !== req.params.pluginVersion) {
41 return res.fail({
42 status: HttpStatusCode.NOT_FOUND_404,
43 message: 'No plugin found named ' + npmName + ' with version ' + req.params.pluginVersion
44 })
45 }
36 46
37 res.locals.registeredPlugin = plugin 47 res.locals.registeredPlugin = plugin
38 48
@@ -50,10 +60,20 @@ const getExternalAuthValidator = [
50 if (areValidationErrors(req, res)) return 60 if (areValidationErrors(req, res)) return
51 61
52 const plugin = res.locals.registeredPlugin 62 const plugin = res.locals.registeredPlugin
53 if (!plugin.registerHelpers) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) 63 if (!plugin.registerHelpers) {
64 return res.fail({
65 status: HttpStatusCode.NOT_FOUND_404,
66 message: 'No registered helpers were found for this plugin'
67 })
68 }
54 69
55 const externalAuth = plugin.registerHelpers.getExternalAuths().find(a => a.authName === req.params.authName) 70 const externalAuth = plugin.registerHelpers.getExternalAuths().find(a => a.authName === req.params.authName)
56 if (!externalAuth) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) 71 if (!externalAuth) {
72 return res.fail({
73 status: HttpStatusCode.NOT_FOUND_404,
74 message: 'No external auths were found for this plugin'
75 })
76 }
57 77
58 res.locals.externalAuth = externalAuth 78 res.locals.externalAuth = externalAuth
59 79
@@ -107,8 +127,7 @@ const installOrUpdatePluginValidator = [
107 127
108 const body: InstallOrUpdatePlugin = req.body 128 const body: InstallOrUpdatePlugin = req.body
109 if (!body.path && !body.npmName) { 129 if (!body.path && !body.npmName) {
110 return res.status(HttpStatusCode.BAD_REQUEST_400) 130 return res.fail({ message: 'Should have either a npmName or a path' })
111 .json({ error: 'Should have either a npmName or a path' })
112 } 131 }
113 132
114 return next() 133 return next()
@@ -137,12 +156,13 @@ const existingPluginValidator = [
137 156
138 const plugin = await PluginModel.loadByNpmName(req.params.npmName) 157 const plugin = await PluginModel.loadByNpmName(req.params.npmName)
139 if (!plugin) { 158 if (!plugin) {
140 return res.status(HttpStatusCode.NOT_FOUND_404) 159 return res.fail({
141 .json({ error: 'Plugin not found' }) 160 status: HttpStatusCode.NOT_FOUND_404,
161 message: 'Plugin not found'
162 })
142 } 163 }
143 164
144 res.locals.plugin = plugin 165 res.locals.plugin = plugin
145
146 return next() 166 return next()
147 } 167 }
148] 168]
@@ -177,9 +197,7 @@ const listAvailablePluginsValidator = [
177 if (areValidationErrors(req, res)) return 197 if (areValidationErrors(req, res)) return
178 198
179 if (CONFIG.PLUGINS.INDEX.ENABLED === false) { 199 if (CONFIG.PLUGINS.INDEX.ENABLED === false) {
180 return res.status(HttpStatusCode.BAD_REQUEST_400) 200 return res.fail({ message: 'Plugin index is not enabled' })
181 .json({ error: 'Plugin index is not enabled' })
182 .end()
183 } 201 }
184 202
185 return next() 203 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 = [
35 return f.resolution === paramResolution && (!req.params.fps || paramFPS) 35 return f.resolution === paramResolution && (!req.params.fps || paramFPS)
36 }) 36 })
37 37
38 if (!videoFile) return res.status(HttpStatusCode.NOT_FOUND_404).json({ error: 'Video file not found.' }) 38 if (!videoFile) {
39 return res.fail({
40 status: HttpStatusCode.NOT_FOUND_404,
41 message: 'Video file not found.'
42 })
43 }
39 res.locals.videoFile = videoFile 44 res.locals.videoFile = videoFile
40 45
41 const videoRedundancy = await VideoRedundancyModel.loadLocalByFileId(videoFile.id) 46 const videoRedundancy = await VideoRedundancyModel.loadLocalByFileId(videoFile.id)
42 if (!videoRedundancy) return res.status(HttpStatusCode.NOT_FOUND_404).json({ error: 'Video redundancy not found.' }) 47 if (!videoRedundancy) {
48 return res.fail({
49 status: HttpStatusCode.NOT_FOUND_404,
50 message: 'Video redundancy not found.'
51 })
52 }
43 res.locals.videoRedundancy = videoRedundancy 53 res.locals.videoRedundancy = videoRedundancy
44 54
45 return next() 55 return next()
@@ -65,11 +75,21 @@ const videoPlaylistRedundancyGetValidator = [
65 const paramPlaylistType = req.params.streamingPlaylistType as unknown as number // We casted to int above 75 const paramPlaylistType = req.params.streamingPlaylistType as unknown as number // We casted to int above
66 const videoStreamingPlaylist = video.VideoStreamingPlaylists.find(p => p.type === paramPlaylistType) 76 const videoStreamingPlaylist = video.VideoStreamingPlaylists.find(p => p.type === paramPlaylistType)
67 77
68 if (!videoStreamingPlaylist) return res.status(HttpStatusCode.NOT_FOUND_404).json({ error: 'Video playlist not found.' }) 78 if (!videoStreamingPlaylist) {
79 return res.fail({
80 status: HttpStatusCode.NOT_FOUND_404,
81 message: 'Video playlist not found.'
82 })
83 }
69 res.locals.videoStreamingPlaylist = videoStreamingPlaylist 84 res.locals.videoStreamingPlaylist = videoStreamingPlaylist
70 85
71 const videoRedundancy = await VideoRedundancyModel.loadLocalByStreamingPlaylistId(videoStreamingPlaylist.id) 86 const videoRedundancy = await VideoRedundancyModel.loadLocalByStreamingPlaylistId(videoStreamingPlaylist.id)
72 if (!videoRedundancy) return res.status(HttpStatusCode.NOT_FOUND_404).json({ error: 'Video redundancy not found.' }) 87 if (!videoRedundancy) {
88 return res.fail({
89 status: HttpStatusCode.NOT_FOUND_404,
90 message: 'Video redundancy not found.'
91 })
92 }
73 res.locals.videoRedundancy = videoRedundancy 93 res.locals.videoRedundancy = videoRedundancy
74 94
75 return next() 95 return next()
@@ -90,12 +110,10 @@ const updateServerRedundancyValidator = [
90 const server = await ServerModel.loadByHost(req.params.host) 110 const server = await ServerModel.loadByHost(req.params.host)
91 111
92 if (!server) { 112 if (!server) {
93 return res 113 return res.fail({
94 .status(HttpStatusCode.NOT_FOUND_404) 114 status: HttpStatusCode.NOT_FOUND_404,
95 .json({ 115 message: `Server ${req.params.host} not found.`
96 error: `Server ${req.params.host} not found.` 116 })
97 })
98 .end()
99 } 117 }
100 118
101 res.locals.server = server 119 res.locals.server = server
@@ -129,19 +147,19 @@ const addVideoRedundancyValidator = [
129 if (!await doesVideoExist(req.body.videoId, res, 'only-video')) return 147 if (!await doesVideoExist(req.body.videoId, res, 'only-video')) return
130 148
131 if (res.locals.onlyVideo.remote === false) { 149 if (res.locals.onlyVideo.remote === false) {
132 return res.status(HttpStatusCode.BAD_REQUEST_400) 150 return res.fail({ message: 'Cannot create a redundancy on a local video' })
133 .json({ error: 'Cannot create a redundancy on a local video' })
134 } 151 }
135 152
136 if (res.locals.onlyVideo.isLive) { 153 if (res.locals.onlyVideo.isLive) {
137 return res.status(HttpStatusCode.BAD_REQUEST_400) 154 return res.fail({ message: 'Cannot create a redundancy of a live video' })
138 .json({ error: 'Cannot create a redundancy of a live video' })
139 } 155 }
140 156
141 const alreadyExists = await VideoRedundancyModel.isLocalByVideoUUIDExists(res.locals.onlyVideo.uuid) 157 const alreadyExists = await VideoRedundancyModel.isLocalByVideoUUIDExists(res.locals.onlyVideo.uuid)
142 if (alreadyExists) { 158 if (alreadyExists) {
143 return res.status(HttpStatusCode.CONFLICT_409) 159 return res.fail({
144 .json({ error: 'This video is already duplicated by your instance.' }) 160 status: HttpStatusCode.CONFLICT_409,
161 message: 'This video is already duplicated by your instance.'
162 })
145 } 163 }
146 164
147 return next() 165 return next()
@@ -160,9 +178,10 @@ const removeVideoRedundancyValidator = [
160 178
161 const redundancy = await VideoRedundancyModel.loadByIdWithVideo(parseInt(req.params.redundancyId, 10)) 179 const redundancy = await VideoRedundancyModel.loadByIdWithVideo(parseInt(req.params.redundancyId, 10))
162 if (!redundancy) { 180 if (!redundancy) {
163 return res.status(HttpStatusCode.NOT_FOUND_404) 181 return res.fail({
164 .json({ error: 'Video redundancy not found' }) 182 status: HttpStatusCode.NOT_FOUND_404,
165 .end() 183 message: 'Video redundancy not found'
184 })
166 } 185 }
167 186
168 res.locals.videoRedundancy = redundancy 187 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 = [
19 19
20 const server = await ServerModel.loadByHost(req.body.host) 20 const server = await ServerModel.loadByHost(req.body.host)
21 if (!server) { 21 if (!server) {
22 return res.status(HttpStatusCode.NOT_FOUND_404) 22 return res.fail({
23 .send({ error: 'Server host not found.' }) 23 status: HttpStatusCode.NOT_FOUND_404,
24 .end() 24 message: 'Server host not found.'
25 })
25 } 26 }
26 27
27 res.locals.server = server 28 res.locals.server = server
@@ -44,26 +45,26 @@ const contactAdministratorValidator = [
44 if (areValidationErrors(req, res)) return 45 if (areValidationErrors(req, res)) return
45 46
46 if (CONFIG.CONTACT_FORM.ENABLED === false) { 47 if (CONFIG.CONTACT_FORM.ENABLED === false) {
47 return res 48 return res.fail({
48 .status(HttpStatusCode.CONFLICT_409) 49 status: HttpStatusCode.CONFLICT_409,
49 .send({ error: 'Contact form is not enabled on this instance.' }) 50 message: 'Contact form is not enabled on this instance.'
50 .end() 51 })
51 } 52 }
52 53
53 if (isEmailEnabled() === false) { 54 if (isEmailEnabled() === false) {
54 return res 55 return res.fail({
55 .status(HttpStatusCode.CONFLICT_409) 56 status: HttpStatusCode.CONFLICT_409,
56 .send({ error: 'Emailer is not enabled on this instance.' }) 57 message: 'Emailer is not enabled on this instance.'
57 .end() 58 })
58 } 59 }
59 60
60 if (await Redis.Instance.doesContactFormIpExist(req.ip)) { 61 if (await Redis.Instance.doesContactFormIpExist(req.ip)) {
61 logger.info('Refusing a contact form by %s: already sent one recently.', req.ip) 62 logger.info('Refusing a contact form by %s: already sent one recently.', req.ip)
62 63
63 return res 64 return res.fail({
64 .status(HttpStatusCode.FORBIDDEN_403) 65 status: HttpStatusCode.FORBIDDEN_403,
65 .send({ error: 'You already sent a contact form recently.' }) 66 message: 'You already sent a contact form recently.'
66 .end() 67 })
67 } 68 }
68 69
69 return next() 70 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 = [
20 const theme = PluginManager.Instance.getRegisteredThemeByShortName(req.params.themeName) 20 const theme = PluginManager.Instance.getRegisteredThemeByShortName(req.params.themeName)
21 21
22 if (!theme || theme.version !== req.params.themeVersion) { 22 if (!theme || theme.version !== req.params.themeVersion) {
23 return res.sendStatus(HttpStatusCode.NOT_FOUND_404) 23 return res.fail({
24 status: HttpStatusCode.NOT_FOUND_404,
25 message: 'No theme named ' + req.params.themeName + ' was found with version ' + req.params.themeVersion
26 })
24 } 27 }
25 28
26 if (theme.css.includes(req.params.staticEndpoint) === false) { 29 if (theme.css.includes(req.params.staticEndpoint) === false) {
27 return res.sendStatus(HttpStatusCode.NOT_FOUND_404) 30 return res.fail({
31 status: HttpStatusCode.NOT_FOUND_404,
32 message: 'No static endpoint was found for this theme'
33 })
28 } 34 }
29 35
30 res.locals.registeredPlugin = theme 36 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 = [
61 const subscription = await ActorFollowModel.loadByActorAndTargetNameAndHostForAPI(user.Account.Actor.id, name, host) 61 const subscription = await ActorFollowModel.loadByActorAndTargetNameAndHostForAPI(user.Account.Actor.id, name, host)
62 62
63 if (!subscription || !subscription.ActorFollowing.VideoChannel) { 63 if (!subscription || !subscription.ActorFollowing.VideoChannel) {
64 return res 64 return res.fail({
65 .status(HttpStatusCode.NOT_FOUND_404) 65 status: HttpStatusCode.NOT_FOUND_404,
66 .json({ 66 message: `Subscription ${req.params.uri} not found.`
67 error: `Subscription ${req.params.uri} not found.` 67 })
68 })
69 } 68 }
70 69
71 res.locals.subscription = subscription 70 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 = [
73 73
74 const authUser = res.locals.oauth.token.User 74 const authUser = res.locals.oauth.token.User
75 if (authUser.role !== UserRole.ADMINISTRATOR && req.body.role !== UserRole.USER) { 75 if (authUser.role !== UserRole.ADMINISTRATOR && req.body.role !== UserRole.USER) {
76 return res 76 return res.fail({
77 .status(HttpStatusCode.FORBIDDEN_403) 77 status: HttpStatusCode.FORBIDDEN_403,
78 .json({ error: 'You can only create users (and not administrators or moderators)' }) 78 message: 'You can only create users (and not administrators or moderators)'
79 })
79 } 80 }
80 81
81 if (req.body.channelName) { 82 if (req.body.channelName) {
82 if (req.body.channelName === req.body.username) { 83 if (req.body.channelName === req.body.username) {
83 return res 84 return res.fail({ message: 'Channel name cannot be the same as user username.' })
84 .status(HttpStatusCode.BAD_REQUEST_400)
85 .json({ error: 'Channel name cannot be the same as user username.' })
86 } 85 }
87 86
88 const existing = await ActorModel.loadLocalByName(req.body.channelName) 87 const existing = await ActorModel.loadLocalByName(req.body.channelName)
89 if (existing) { 88 if (existing) {
90 return res 89 return res.fail({
91 .status(HttpStatusCode.CONFLICT_409) 90 status: HttpStatusCode.CONFLICT_409,
92 .json({ error: `Channel with name ${req.body.channelName} already exists.` }) 91 message: `Channel with name ${req.body.channelName} already exists.`
92 })
93 } 93 }
94 } 94 }
95 95
@@ -121,20 +121,19 @@ const usersRegisterValidator = [
121 const body: UserRegister = req.body 121 const body: UserRegister = req.body
122 if (body.channel) { 122 if (body.channel) {
123 if (!body.channel.name || !body.channel.displayName) { 123 if (!body.channel.name || !body.channel.displayName) {
124 return res 124 return res.fail({ message: 'Channel is optional but if you specify it, channel.name and channel.displayName are required.' })
125 .status(HttpStatusCode.BAD_REQUEST_400)
126 .json({ error: 'Channel is optional but if you specify it, channel.name and channel.displayName are required.' })
127 } 125 }
128 126
129 if (body.channel.name === body.username) { 127 if (body.channel.name === body.username) {
130 return res.status(HttpStatusCode.BAD_REQUEST_400) 128 return res.fail({ message: 'Channel name cannot be the same as user username.' })
131 .json({ error: 'Channel name cannot be the same as user username.' })
132 } 129 }
133 130
134 const existing = await ActorModel.loadLocalByName(body.channel.name) 131 const existing = await ActorModel.loadLocalByName(body.channel.name)
135 if (existing) { 132 if (existing) {
136 return res.status(HttpStatusCode.CONFLICT_409) 133 return res.fail({
137 .json({ error: `Channel with name ${body.channel.name} already exists.` }) 134 status: HttpStatusCode.CONFLICT_409,
135 message: `Channel with name ${body.channel.name} already exists.`
136 })
138 } 137 }
139 } 138 }
140 139
@@ -153,8 +152,7 @@ const usersRemoveValidator = [
153 152
154 const user = res.locals.user 153 const user = res.locals.user
155 if (user.username === 'root') { 154 if (user.username === 'root') {
156 return res.status(HttpStatusCode.BAD_REQUEST_400) 155 return res.fail({ message: 'Cannot remove the root user' })
157 .json({ error: 'Cannot remove the root user' })
158 } 156 }
159 157
160 return next() 158 return next()
@@ -173,8 +171,7 @@ const usersBlockingValidator = [
173 171
174 const user = res.locals.user 172 const user = res.locals.user
175 if (user.username === 'root') { 173 if (user.username === 'root') {
176 return res.status(HttpStatusCode.BAD_REQUEST_400) 174 return res.fail({ message: 'Cannot block the root user' })
177 .json({ error: 'Cannot block the root user' })
178 } 175 }
179 176
180 return next() 177 return next()
@@ -185,9 +182,7 @@ const deleteMeValidator = [
185 (req: express.Request, res: express.Response, next: express.NextFunction) => { 182 (req: express.Request, res: express.Response, next: express.NextFunction) => {
186 const user = res.locals.oauth.token.User 183 const user = res.locals.oauth.token.User
187 if (user.username === 'root') { 184 if (user.username === 'root') {
188 return res.status(HttpStatusCode.BAD_REQUEST_400) 185 return res.fail({ message: 'You cannot delete your root account.' })
189 .json({ error: 'You cannot delete your root account.' })
190 .end()
191 } 186 }
192 187
193 return next() 188 return next()
@@ -217,8 +212,7 @@ const usersUpdateValidator = [
217 212
218 const user = res.locals.user 213 const user = res.locals.user
219 if (user.username === 'root' && req.body.role !== undefined && user.role !== req.body.role) { 214 if (user.username === 'root' && req.body.role !== undefined && user.role !== req.body.role) {
220 return res.status(HttpStatusCode.BAD_REQUEST_400) 215 return res.fail({ message: 'Cannot change root role.' })
221 .json({ error: 'Cannot change root role.' })
222 } 216 }
223 217
224 return next() 218 return next()
@@ -273,18 +267,18 @@ const usersUpdateMeValidator = [
273 267
274 if (req.body.password || req.body.email) { 268 if (req.body.password || req.body.email) {
275 if (user.pluginAuth !== null) { 269 if (user.pluginAuth !== null) {
276 return res.status(HttpStatusCode.BAD_REQUEST_400) 270 return res.fail({ message: 'You cannot update your email or password that is associated with an external auth system.' })
277 .json({ error: 'You cannot update your email or password that is associated with an external auth system.' })
278 } 271 }
279 272
280 if (!req.body.currentPassword) { 273 if (!req.body.currentPassword) {
281 return res.status(HttpStatusCode.BAD_REQUEST_400) 274 return res.fail({ message: 'currentPassword parameter is missing.' })
282 .json({ error: 'currentPassword parameter is missing.' })
283 } 275 }
284 276
285 if (await user.isPasswordMatch(req.body.currentPassword) !== true) { 277 if (await user.isPasswordMatch(req.body.currentPassword) !== true) {
286 return res.status(HttpStatusCode.UNAUTHORIZED_401) 278 return res.fail({
287 .json({ error: 'currentPassword is invalid.' }) 279 status: HttpStatusCode.UNAUTHORIZED_401,
280 message: 'currentPassword is invalid.'
281 })
288 } 282 }
289 } 283 }
290 284
@@ -335,8 +329,10 @@ const ensureUserRegistrationAllowed = [
335 ) 329 )
336 330
337 if (allowedResult.allowed === false) { 331 if (allowedResult.allowed === false) {
338 return res.status(HttpStatusCode.FORBIDDEN_403) 332 return res.fail({
339 .json({ error: allowedResult.errorMessage || 'User registration is not enabled or user limit is reached.' }) 333 status: HttpStatusCode.FORBIDDEN_403,
334 message: allowedResult.errorMessage || 'User registration is not enabled or user limit is reached.'
335 })
340 } 336 }
341 337
342 return next() 338 return next()
@@ -348,8 +344,10 @@ const ensureUserRegistrationAllowedForIP = [
348 const allowed = isSignupAllowedForCurrentIP(req.ip) 344 const allowed = isSignupAllowedForCurrentIP(req.ip)
349 345
350 if (allowed === false) { 346 if (allowed === false) {
351 return res.status(HttpStatusCode.FORBIDDEN_403) 347 return res.fail({
352 .json({ error: 'You are not on a network authorized for registration.' }) 348 status: HttpStatusCode.FORBIDDEN_403,
349 message: 'You are not on a network authorized for registration.'
350 })
353 } 351 }
354 352
355 return next() 353 return next()
@@ -390,9 +388,10 @@ const usersResetPasswordValidator = [
390 const redisVerificationString = await Redis.Instance.getResetPasswordLink(user.id) 388 const redisVerificationString = await Redis.Instance.getResetPasswordLink(user.id)
391 389
392 if (redisVerificationString !== req.body.verificationString) { 390 if (redisVerificationString !== req.body.verificationString) {
393 return res 391 return res.fail({
394 .status(HttpStatusCode.FORBIDDEN_403) 392 status: HttpStatusCode.FORBIDDEN_403,
395 .json({ error: 'Invalid verification string.' }) 393 message: 'Invalid verification string.'
394 })
396 } 395 }
397 396
398 return next() 397 return next()
@@ -437,9 +436,10 @@ const usersVerifyEmailValidator = [
437 const redisVerificationString = await Redis.Instance.getVerifyEmailLink(user.id) 436 const redisVerificationString = await Redis.Instance.getVerifyEmailLink(user.id)
438 437
439 if (redisVerificationString !== req.body.verificationString) { 438 if (redisVerificationString !== req.body.verificationString) {
440 return res 439 return res.fail({
441 .status(HttpStatusCode.FORBIDDEN_403) 440 status: HttpStatusCode.FORBIDDEN_403,
442 .json({ error: 'Invalid verification string.' }) 441 message: 'Invalid verification string.'
442 })
443 } 443 }
444 444
445 return next() 445 return next()
@@ -455,8 +455,10 @@ const ensureAuthUserOwnsAccountValidator = [
455 const user = res.locals.oauth.token.User 455 const user = res.locals.oauth.token.User
456 456
457 if (res.locals.account.id !== user.Account.id) { 457 if (res.locals.account.id !== user.Account.id) {
458 return res.status(HttpStatusCode.FORBIDDEN_403) 458 return res.fail({
459 .json({ error: 'Only owner can access ratings list.' }) 459 status: HttpStatusCode.FORBIDDEN_403,
460 message: 'Only owner can access ratings list.'
461 })
460 } 462 }
461 463
462 return next() 464 return next()
@@ -471,8 +473,10 @@ const ensureCanManageUser = [
471 if (authUser.role === UserRole.ADMINISTRATOR) return next() 473 if (authUser.role === UserRole.ADMINISTRATOR) return next()
472 if (authUser.role === UserRole.MODERATOR && onUser.role === UserRole.USER) return next() 474 if (authUser.role === UserRole.MODERATOR && onUser.role === UserRole.USER) return next()
473 475
474 return res.status(HttpStatusCode.FORBIDDEN_403) 476 return res.fail({
475 .json({ error: 'A moderator can only manager users.' }) 477 status: HttpStatusCode.FORBIDDEN_403,
478 message: 'A moderator can only manager users.'
479 })
476 } 480 }
477] 481]
478 482
@@ -515,15 +519,19 @@ async function checkUserNameOrEmailDoesNotAlreadyExist (username: string, email:
515 const user = await UserModel.loadByUsernameOrEmail(username, email) 519 const user = await UserModel.loadByUsernameOrEmail(username, email)
516 520
517 if (user) { 521 if (user) {
518 res.status(HttpStatusCode.CONFLICT_409) 522 res.fail({
519 .json({ error: 'User with this username or email already exists.' }) 523 status: HttpStatusCode.CONFLICT_409,
524 message: 'User with this username or email already exists.'
525 })
520 return false 526 return false
521 } 527 }
522 528
523 const actor = await ActorModel.loadLocalByName(username) 529 const actor = await ActorModel.loadLocalByName(username)
524 if (actor) { 530 if (actor) {
525 res.status(HttpStatusCode.CONFLICT_409) 531 res.fail({
526 .json({ error: 'Another actor (account/channel) with this name on this instance already exists or has already existed.' }) 532 status: HttpStatusCode.CONFLICT_409,
533 message: 'Another actor (account/channel) with this name on this instance already exists or has already existed.'
534 })
527 return false 535 return false
528 } 536 }
529 537
@@ -535,14 +543,15 @@ async function checkUserExist (finder: () => Promise<MUserDefault>, res: express
535 543
536 if (!user) { 544 if (!user) {
537 if (abortResponse === true) { 545 if (abortResponse === true) {
538 res.status(HttpStatusCode.NOT_FOUND_404) 546 res.fail({
539 .json({ error: 'User not found' }) 547 status: HttpStatusCode.NOT_FOUND_404,
548 message: 'User not found'
549 })
540 } 550 }
541 551
542 return false 552 return false
543 } 553 }
544 554
545 res.locals.user = user 555 res.locals.user = user
546
547 return true 556 return true
548} 557}
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 @@
1import * as express from 'express' 1import * as express from 'express'
2import { query, validationResult } from 'express-validator' 2import { query, validationResult } from 'express-validator'
3import { logger } from '../../helpers/logger' 3import { logger } from '../../helpers/logger'
4import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
5 4
6function areValidationErrors (req: express.Request, res: express.Response) { 5function areValidationErrors (req: express.Request, res: express.Response) {
7 const errors = validationResult(req) 6 const errors = validationResult(req)
8 7
9 if (!errors.isEmpty()) { 8 if (!errors.isEmpty()) {
10 logger.warn('Incorrect request parameters', { path: req.originalUrl, err: errors.mapped() }) 9 logger.warn('Incorrect request parameters', { path: req.originalUrl, err: errors.mapped() })
11 res.status(HttpStatusCode.BAD_REQUEST_400) 10 res.fail({
12 .json({ errors: errors.mapped() }) 11 message: 'Incorrect request parameters: ' + Object.keys(errors.mapped()).join(', '),
12 instance: req.originalUrl,
13 data: {
14 'invalid-params': errors.mapped()
15 }
16 })
13 17
14 return true 18 return true
15 } 19 }
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 = [
39 39
40 const video = res.locals.videoAll 40 const video = res.locals.videoAll
41 if (req.body.unfederate === true && video.remote === true) { 41 if (req.body.unfederate === true && video.remote === true) {
42 return res 42 return res.fail({
43 .status(HttpStatusCode.CONFLICT_409) 43 status: HttpStatusCode.CONFLICT_409,
44 .send({ error: 'You cannot unfederate a remote video.' }) 44 message: 'You cannot unfederate a remote video.'
45 .end() 45 })
46 } 46 }
47 47
48 return next() 48 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 = [
30 30
31 const actor = await ActorModel.loadLocalByName(req.body.name) 31 const actor = await ActorModel.loadLocalByName(req.body.name)
32 if (actor) { 32 if (actor) {
33 res.status(HttpStatusCode.CONFLICT_409) 33 res.fail({
34 .send({ error: 'Another actor (account/channel) with this name on this instance already exists or has already existed.' }) 34 status: HttpStatusCode.CONFLICT_409,
35 .end() 35 message: 'Another actor (account/channel) with this name on this instance already exists or has already existed.'
36 })
36 return false 37 return false
37 } 38 }
38 39
39 const count = await VideoChannelModel.countByAccount(res.locals.oauth.token.User.Account.id) 40 const count = await VideoChannelModel.countByAccount(res.locals.oauth.token.User.Account.id)
40 if (count >= VIDEO_CHANNELS.MAX_PER_USER) { 41 if (count >= VIDEO_CHANNELS.MAX_PER_USER) {
41 res.status(HttpStatusCode.BAD_REQUEST_400) 42 res.fail({ message: `You cannot create more than ${VIDEO_CHANNELS.MAX_PER_USER} channels` })
42 .send({ error: `You cannot create more than ${VIDEO_CHANNELS.MAX_PER_USER} channels` })
43 .end()
44 return false 43 return false
45 } 44 }
46 45
@@ -71,13 +70,17 @@ const videoChannelsUpdateValidator = [
71 70
72 // We need to make additional checks 71 // We need to make additional checks
73 if (res.locals.videoChannel.Actor.isOwned() === false) { 72 if (res.locals.videoChannel.Actor.isOwned() === false) {
74 return res.status(HttpStatusCode.FORBIDDEN_403) 73 return res.fail({
75 .json({ error: 'Cannot update video channel of another server' }) 74 status: HttpStatusCode.FORBIDDEN_403,
75 message: 'Cannot update video channel of another server'
76 })
76 } 77 }
77 78
78 if (res.locals.videoChannel.Account.userId !== res.locals.oauth.token.User.id) { 79 if (res.locals.videoChannel.Account.userId !== res.locals.oauth.token.User.id) {
79 return res.status(HttpStatusCode.FORBIDDEN_403) 80 return res.fail({
80 .json({ error: 'Cannot update video channel of another user' }) 81 status: HttpStatusCode.FORBIDDEN_403,
82 message: 'Cannot update video channel of another user'
83 })
81 } 84 }
82 85
83 return next() 86 return next()
@@ -154,10 +157,10 @@ export {
154 157
155function checkUserCanDeleteVideoChannel (user: MUser, videoChannel: MChannelAccountDefault, res: express.Response) { 158function checkUserCanDeleteVideoChannel (user: MUser, videoChannel: MChannelAccountDefault, res: express.Response) {
156 if (videoChannel.Actor.isOwned() === false) { 159 if (videoChannel.Actor.isOwned() === false) {
157 res.status(HttpStatusCode.FORBIDDEN_403) 160 res.fail({
158 .json({ error: 'Cannot remove video channel of another server.' }) 161 status: HttpStatusCode.FORBIDDEN_403,
159 .end() 162 message: 'Cannot remove video channel of another server.'
160 163 })
161 return false 164 return false
162 } 165 }
163 166
@@ -165,10 +168,10 @@ function checkUserCanDeleteVideoChannel (user: MUser, videoChannel: MChannelAcco
165 // The user can delete it if s/he is an admin 168 // The user can delete it if s/he is an admin
166 // Or if s/he is the video channel's account 169 // Or if s/he is the video channel's account
167 if (user.hasRight(UserRight.REMOVE_ANY_VIDEO_CHANNEL) === false && videoChannel.Account.userId !== user.id) { 170 if (user.hasRight(UserRight.REMOVE_ANY_VIDEO_CHANNEL) === false && videoChannel.Account.userId !== user.id) {
168 res.status(HttpStatusCode.FORBIDDEN_403) 171 res.fail({
169 .json({ error: 'Cannot remove video channel of another user' }) 172 status: HttpStatusCode.FORBIDDEN_403,
170 .end() 173 message: 'Cannot remove video channel of another user'
171 174 })
172 return false 175 return false
173 } 176 }
174 177
@@ -179,10 +182,10 @@ async function checkVideoChannelIsNotTheLastOne (res: express.Response) {
179 const count = await VideoChannelModel.countByAccount(res.locals.oauth.token.User.Account.id) 182 const count = await VideoChannelModel.countByAccount(res.locals.oauth.token.User.Account.id)
180 183
181 if (count <= 1) { 184 if (count <= 1) {
182 res.status(HttpStatusCode.CONFLICT_409) 185 res.fail({
183 .json({ error: 'Cannot remove the last channel of this user' }) 186 status: HttpStatusCode.CONFLICT_409,
184 .end() 187 message: 'Cannot remove the last channel of this user'
185 188 })
186 return false 189 return false
187 } 190 }
188 191
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 {
155 155
156function isVideoCommentsEnabled (video: MVideo, res: express.Response) { 156function isVideoCommentsEnabled (video: MVideo, res: express.Response) {
157 if (video.commentsEnabled !== true) { 157 if (video.commentsEnabled !== true) {
158 res.status(HttpStatusCode.CONFLICT_409) 158 res.fail({
159 .json({ error: 'Video comments are disabled for this video.' }) 159 status: HttpStatusCode.CONFLICT_409,
160 160 message: 'Video comments are disabled for this video.'
161 })
161 return false 162 return false
162 } 163 }
163 164
@@ -166,9 +167,10 @@ function isVideoCommentsEnabled (video: MVideo, res: express.Response) {
166 167
167function checkUserCanDeleteVideoComment (user: MUserAccountUrl, videoComment: MCommentOwnerVideoReply, res: express.Response) { 168function checkUserCanDeleteVideoComment (user: MUserAccountUrl, videoComment: MCommentOwnerVideoReply, res: express.Response) {
168 if (videoComment.isDeleted()) { 169 if (videoComment.isDeleted()) {
169 res.status(HttpStatusCode.CONFLICT_409) 170 res.fail({
170 .json({ error: 'This comment is already deleted' }) 171 status: HttpStatusCode.CONFLICT_409,
171 172 message: 'This comment is already deleted'
173 })
172 return false 174 return false
173 } 175 }
174 176
@@ -179,9 +181,10 @@ function checkUserCanDeleteVideoComment (user: MUserAccountUrl, videoComment: MC
179 videoComment.accountId !== userAccount.id && // Not the comment owner 181 videoComment.accountId !== userAccount.id && // Not the comment owner
180 videoComment.Video.VideoChannel.accountId !== userAccount.id // Not the video owner 182 videoComment.Video.VideoChannel.accountId !== userAccount.id // Not the video owner
181 ) { 183 ) {
182 res.status(HttpStatusCode.FORBIDDEN_403) 184 res.fail({
183 .json({ error: 'Cannot remove video comment of another user' }) 185 status: HttpStatusCode.FORBIDDEN_403,
184 186 message: 'Cannot remove video comment of another user'
187 })
185 return false 188 return false
186 } 189 }
187 190
@@ -215,9 +218,11 @@ async function isVideoCommentAccepted (req: express.Request, res: express.Respon
215 218
216 if (!acceptedResult || acceptedResult.accepted !== true) { 219 if (!acceptedResult || acceptedResult.accepted !== true) {
217 logger.info('Refused local comment.', { acceptedResult, acceptParameters }) 220 logger.info('Refused local comment.', { acceptedResult, acceptParameters })
218 res.status(HttpStatusCode.FORBIDDEN_403)
219 .json({ error: acceptedResult?.errorMessage || 'Refused local comment' })
220 221
222 res.fail({
223 status: HttpStatusCode.FORBIDDEN_403,
224 message: acceptedResult?.errorMessage || 'Refused local comment'
225 })
221 return false 226 return false
222 } 227 }
223 228
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([
47 47
48 if (CONFIG.IMPORT.VIDEOS.HTTP.ENABLED !== true && req.body.targetUrl) { 48 if (CONFIG.IMPORT.VIDEOS.HTTP.ENABLED !== true && req.body.targetUrl) {
49 cleanUpReqFiles(req) 49 cleanUpReqFiles(req)
50 return res.status(HttpStatusCode.CONFLICT_409) 50
51 .json({ error: 'HTTP import is not enabled on this instance.' }) 51 return res.fail({
52 status: HttpStatusCode.CONFLICT_409,
53 message: 'HTTP import is not enabled on this instance.'
54 })
52 } 55 }
53 56
54 if (CONFIG.IMPORT.VIDEOS.TORRENT.ENABLED !== true && (req.body.magnetUri || torrentFile)) { 57 if (CONFIG.IMPORT.VIDEOS.TORRENT.ENABLED !== true && (req.body.magnetUri || torrentFile)) {
55 cleanUpReqFiles(req) 58 cleanUpReqFiles(req)
56 return res.status(HttpStatusCode.CONFLICT_409) 59
57 .json({ error: 'Torrent/magnet URI import is not enabled on this instance.' }) 60 return res.fail({
61 status: HttpStatusCode.CONFLICT_409,
62 message: 'Torrent/magnet URI import is not enabled on this instance.'
63 })
58 } 64 }
59 65
60 if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req) 66 if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
@@ -63,8 +69,7 @@ const videoImportAddValidator = getCommonVideoEditAttributes().concat([
63 if (!req.body.targetUrl && !req.body.magnetUri && !torrentFile) { 69 if (!req.body.targetUrl && !req.body.magnetUri && !torrentFile) {
64 cleanUpReqFiles(req) 70 cleanUpReqFiles(req)
65 71
66 return res.status(HttpStatusCode.BAD_REQUEST_400) 72 return res.fail({ message: 'Should have a magnetUri or a targetUrl or a torrent file.' })
67 .json({ error: 'Should have a magnetUri or a targetUrl or a torrent file.' })
68 } 73 }
69 74
70 if (!await isImportAccepted(req, res)) return cleanUpReqFiles(req) 75 if (!await isImportAccepted(req, res)) return cleanUpReqFiles(req)
@@ -100,9 +105,11 @@ async function isImportAccepted (req: express.Request, res: express.Response) {
100 105
101 if (!acceptedResult || acceptedResult.accepted !== true) { 106 if (!acceptedResult || acceptedResult.accepted !== true) {
102 logger.info('Refused to import video.', { acceptedResult, acceptParameters }) 107 logger.info('Refused to import video.', { acceptedResult, acceptParameters })
103 res.status(HttpStatusCode.FORBIDDEN_403)
104 .json({ error: acceptedResult.errorMessage || 'Refused to import video' })
105 108
109 res.fail({
110 status: HttpStatusCode.FORBIDDEN_403,
111 message: acceptedResult.errorMessage || 'Refused to import video'
112 })
106 return false 113 return false
107 } 114 }
108 115
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 = [
30 if (!checkUserCanManageVideo(user, res.locals.videoAll, UserRight.GET_ANY_LIVE, res, false)) return 30 if (!checkUserCanManageVideo(user, res.locals.videoAll, UserRight.GET_ANY_LIVE, res, false)) return
31 31
32 const videoLive = await VideoLiveModel.loadByVideoId(res.locals.videoAll.id) 32 const videoLive = await VideoLiveModel.loadByVideoId(res.locals.videoAll.id)
33 if (!videoLive) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) 33 if (!videoLive) {
34 return res.fail({
35 status: HttpStatusCode.NOT_FOUND_404,
36 message: 'Live video not found'
37 })
38 }
34 39
35 res.locals.videoLive = videoLive 40 res.locals.videoLive = videoLive
36 41
@@ -66,22 +71,25 @@ const videoLiveAddValidator = getCommonVideoEditAttributes().concat([
66 if (CONFIG.LIVE.ENABLED !== true) { 71 if (CONFIG.LIVE.ENABLED !== true) {
67 cleanUpReqFiles(req) 72 cleanUpReqFiles(req)
68 73
69 return res.status(HttpStatusCode.FORBIDDEN_403) 74 return res.fail({
70 .json({ error: 'Live is not enabled on this instance' }) 75 status: HttpStatusCode.FORBIDDEN_403,
76 message: 'Live is not enabled on this instance'
77 })
71 } 78 }
72 79
73 if (CONFIG.LIVE.ALLOW_REPLAY !== true && req.body.saveReplay === true) { 80 if (CONFIG.LIVE.ALLOW_REPLAY !== true && req.body.saveReplay === true) {
74 cleanUpReqFiles(req) 81 cleanUpReqFiles(req)
75 82
76 return res.status(HttpStatusCode.FORBIDDEN_403) 83 return res.fail({
77 .json({ error: 'Saving live replay is not allowed instance' }) 84 status: HttpStatusCode.FORBIDDEN_403,
85 message: 'Saving live replay is not allowed instance'
86 })
78 } 87 }
79 88
80 if (req.body.permanentLive && req.body.saveReplay) { 89 if (req.body.permanentLive && req.body.saveReplay) {
81 cleanUpReqFiles(req) 90 cleanUpReqFiles(req)
82 91
83 return res.status(HttpStatusCode.BAD_REQUEST_400) 92 return res.fail({ message: 'Cannot set this live as permanent while saving its replay' })
84 .json({ error: 'Cannot set this live as permanent while saving its replay' })
85 } 93 }
86 94
87 const user = res.locals.oauth.token.User 95 const user = res.locals.oauth.token.User
@@ -93,11 +101,11 @@ const videoLiveAddValidator = getCommonVideoEditAttributes().concat([
93 if (totalInstanceLives >= CONFIG.LIVE.MAX_INSTANCE_LIVES) { 101 if (totalInstanceLives >= CONFIG.LIVE.MAX_INSTANCE_LIVES) {
94 cleanUpReqFiles(req) 102 cleanUpReqFiles(req)
95 103
96 return res.status(HttpStatusCode.FORBIDDEN_403) 104 return res.fail({
97 .json({ 105 status: HttpStatusCode.FORBIDDEN_403,
98 code: ServerErrorCode.MAX_INSTANCE_LIVES_LIMIT_REACHED, 106 message: 'Cannot create this live because the max instance lives limit is reached.',
99 error: 'Cannot create this live because the max instance lives limit is reached.' 107 type: ServerErrorCode.MAX_INSTANCE_LIVES_LIMIT_REACHED.toString()
100 }) 108 })
101 } 109 }
102 } 110 }
103 111
@@ -107,11 +115,11 @@ const videoLiveAddValidator = getCommonVideoEditAttributes().concat([
107 if (totalUserLives >= CONFIG.LIVE.MAX_USER_LIVES) { 115 if (totalUserLives >= CONFIG.LIVE.MAX_USER_LIVES) {
108 cleanUpReqFiles(req) 116 cleanUpReqFiles(req)
109 117
110 return res.status(HttpStatusCode.FORBIDDEN_403) 118 return res.fail({
111 .json({ 119 status: HttpStatusCode.FORBIDDEN_403,
112 code: ServerErrorCode.MAX_USER_LIVES_LIMIT_REACHED, 120 type: ServerErrorCode.MAX_USER_LIVES_LIMIT_REACHED.toString(),
113 error: 'Cannot create this live because the max user lives limit is reached.' 121 message: 'Cannot create this live because the max user lives limit is reached.'
114 }) 122 })
115 } 123 }
116 } 124 }
117 125
@@ -133,18 +141,18 @@ const videoLiveUpdateValidator = [
133 if (areValidationErrors(req, res)) return 141 if (areValidationErrors(req, res)) return
134 142
135 if (req.body.permanentLive && req.body.saveReplay) { 143 if (req.body.permanentLive && req.body.saveReplay) {
136 return res.status(HttpStatusCode.BAD_REQUEST_400) 144 return res.fail({ message: 'Cannot set this live as permanent while saving its replay' })
137 .json({ error: 'Cannot set this live as permanent while saving its replay' })
138 } 145 }
139 146
140 if (CONFIG.LIVE.ALLOW_REPLAY !== true && req.body.saveReplay === true) { 147 if (CONFIG.LIVE.ALLOW_REPLAY !== true && req.body.saveReplay === true) {
141 return res.status(HttpStatusCode.FORBIDDEN_403) 148 return res.fail({
142 .json({ error: 'Saving live replay is not allowed instance' }) 149 status: HttpStatusCode.FORBIDDEN_403,
150 message: 'Saving live replay is not allowed instance'
151 })
143 } 152 }
144 153
145 if (res.locals.videoAll.state !== VideoState.WAITING_FOR_LIVE) { 154 if (res.locals.videoAll.state !== VideoState.WAITING_FOR_LIVE) {
146 return res.status(HttpStatusCode.BAD_REQUEST_400) 155 return res.fail({ message: 'Cannot update a live that has already started' })
147 .json({ error: 'Cannot update a live that has already started' })
148 } 156 }
149 157
150 // Check the user can manage the live 158 // Check the user can manage the live
@@ -180,9 +188,10 @@ async function isLiveVideoAccepted (req: express.Request, res: express.Response)
180 if (!acceptedResult || acceptedResult.accepted !== true) { 188 if (!acceptedResult || acceptedResult.accepted !== true) {
181 logger.info('Refused local live video.', { acceptedResult, acceptParameters }) 189 logger.info('Refused local live video.', { acceptedResult, acceptParameters })
182 190
183 res.status(HttpStatusCode.FORBIDDEN_403) 191 res.fail({
184 .json({ error: acceptedResult.errorMessage || 'Refused local live video' }) 192 status: HttpStatusCode.FORBIDDEN_403,
185 193 message: acceptedResult.errorMessage || 'Refused local live video'
194 })
186 return false 195 return false
187 } 196 }
188 197
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([
46 46
47 if (body.privacy === VideoPlaylistPrivacy.PUBLIC && !body.videoChannelId) { 47 if (body.privacy === VideoPlaylistPrivacy.PUBLIC && !body.videoChannelId) {
48 cleanUpReqFiles(req) 48 cleanUpReqFiles(req)
49 return res.status(HttpStatusCode.BAD_REQUEST_400) 49
50 .json({ error: 'Cannot set "public" a playlist that is not assigned to a channel.' }) 50 return res.fail({ message: 'Cannot set "public" a playlist that is not assigned to a channel.' })
51 } 51 }
52 52
53 return next() 53 return next()
@@ -85,14 +85,14 @@ const videoPlaylistsUpdateValidator = getCommonPlaylistEditAttributes().concat([
85 ) 85 )
86 ) { 86 ) {
87 cleanUpReqFiles(req) 87 cleanUpReqFiles(req)
88 return res.status(HttpStatusCode.BAD_REQUEST_400) 88
89 .json({ error: 'Cannot set "public" a playlist that is not assigned to a channel.' }) 89 return res.fail({ message: 'Cannot set "public" a playlist that is not assigned to a channel.' })
90 } 90 }
91 91
92 if (videoPlaylist.type === VideoPlaylistType.WATCH_LATER) { 92 if (videoPlaylist.type === VideoPlaylistType.WATCH_LATER) {
93 cleanUpReqFiles(req) 93 cleanUpReqFiles(req)
94 return res.status(HttpStatusCode.BAD_REQUEST_400) 94
95 .json({ error: 'Cannot update a watch later playlist.' }) 95 return res.fail({ message: 'Cannot update a watch later playlist.' })
96 } 96 }
97 97
98 if (body.videoChannelId && !await doesVideoChannelIdExist(body.videoChannelId, res)) return cleanUpReqFiles(req) 98 if (body.videoChannelId && !await doesVideoChannelIdExist(body.videoChannelId, res)) return cleanUpReqFiles(req)
@@ -114,8 +114,7 @@ const videoPlaylistsDeleteValidator = [
114 114
115 const videoPlaylist = getPlaylist(res) 115 const videoPlaylist = getPlaylist(res)
116 if (videoPlaylist.type === VideoPlaylistType.WATCH_LATER) { 116 if (videoPlaylist.type === VideoPlaylistType.WATCH_LATER) {
117 return res.status(HttpStatusCode.BAD_REQUEST_400) 117 return res.fail({ message: 'Cannot delete a watch later playlist.' })
118 .json({ error: 'Cannot delete a watch later playlist.' })
119 } 118 }
120 119
121 if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, videoPlaylist, UserRight.REMOVE_ANY_VIDEO_PLAYLIST, res)) { 120 if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, videoPlaylist, UserRight.REMOVE_ANY_VIDEO_PLAYLIST, res)) {
@@ -144,7 +143,10 @@ const videoPlaylistsGetValidator = (fetchType: VideoPlaylistFetchType) => {
144 if (videoPlaylist.privacy === VideoPlaylistPrivacy.UNLISTED) { 143 if (videoPlaylist.privacy === VideoPlaylistPrivacy.UNLISTED) {
145 if (isUUIDValid(req.params.playlistId)) return next() 144 if (isUUIDValid(req.params.playlistId)) return next()
146 145
147 return res.status(HttpStatusCode.NOT_FOUND_404).end() 146 return res.fail({
147 status: HttpStatusCode.NOT_FOUND_404,
148 message: 'Playlist not found'
149 })
148 } 150 }
149 151
150 if (videoPlaylist.privacy === VideoPlaylistPrivacy.PRIVATE) { 152 if (videoPlaylist.privacy === VideoPlaylistPrivacy.PRIVATE) {
@@ -156,8 +158,10 @@ const videoPlaylistsGetValidator = (fetchType: VideoPlaylistFetchType) => {
156 !user || 158 !user ||
157 (videoPlaylist.OwnerAccount.id !== user.Account.id && !user.hasRight(UserRight.UPDATE_ANY_VIDEO_PLAYLIST)) 159 (videoPlaylist.OwnerAccount.id !== user.Account.id && !user.hasRight(UserRight.UPDATE_ANY_VIDEO_PLAYLIST))
158 ) { 160 ) {
159 return res.status(HttpStatusCode.FORBIDDEN_403) 161 return res.fail({
160 .json({ error: 'Cannot get this private video playlist.' }) 162 status: HttpStatusCode.FORBIDDEN_403,
163 message: 'Cannot get this private video playlist.'
164 })
161 } 165 }
162 166
163 return next() 167 return next()
@@ -233,10 +237,10 @@ const videoPlaylistsUpdateOrRemoveVideoValidator = [
233 237
234 const videoPlaylistElement = await VideoPlaylistElementModel.loadById(req.params.playlistElementId) 238 const videoPlaylistElement = await VideoPlaylistElementModel.loadById(req.params.playlistElementId)
235 if (!videoPlaylistElement) { 239 if (!videoPlaylistElement) {
236 res.status(HttpStatusCode.NOT_FOUND_404) 240 res.fail({
237 .json({ error: 'Video playlist element not found' }) 241 status: HttpStatusCode.NOT_FOUND_404,
238 .end() 242 message: 'Video playlist element not found'
239 243 })
240 return 244 return
241 } 245 }
242 res.locals.videoPlaylistElement = videoPlaylistElement 246 res.locals.videoPlaylistElement = videoPlaylistElement
@@ -263,15 +267,18 @@ const videoPlaylistElementAPGetValidator = [
263 267
264 const videoPlaylistElement = await VideoPlaylistElementModel.loadByPlaylistAndElementIdForAP(playlistId, playlistElementId) 268 const videoPlaylistElement = await VideoPlaylistElementModel.loadByPlaylistAndElementIdForAP(playlistId, playlistElementId)
265 if (!videoPlaylistElement) { 269 if (!videoPlaylistElement) {
266 res.status(HttpStatusCode.NOT_FOUND_404) 270 res.fail({
267 .json({ error: 'Video playlist element not found' }) 271 status: HttpStatusCode.NOT_FOUND_404,
268 .end() 272 message: 'Video playlist element not found'
269 273 })
270 return 274 return
271 } 275 }
272 276
273 if (videoPlaylistElement.VideoPlaylist.privacy === VideoPlaylistPrivacy.PRIVATE) { 277 if (videoPlaylistElement.VideoPlaylist.privacy === VideoPlaylistPrivacy.PRIVATE) {
274 return res.status(HttpStatusCode.FORBIDDEN_403).end() 278 return res.fail({
279 status: HttpStatusCode.FORBIDDEN_403,
280 message: 'Cannot get this private video playlist.'
281 })
275 } 282 }
276 283
277 res.locals.videoPlaylistElementAP = videoPlaylistElement 284 res.locals.videoPlaylistElementAP = videoPlaylistElement
@@ -307,18 +314,12 @@ const videoPlaylistsReorderVideosValidator = [
307 const reorderLength: number = req.body.reorderLength 314 const reorderLength: number = req.body.reorderLength
308 315
309 if (startPosition >= nextPosition || insertAfterPosition >= nextPosition) { 316 if (startPosition >= nextPosition || insertAfterPosition >= nextPosition) {
310 res.status(HttpStatusCode.BAD_REQUEST_400) 317 res.fail({ message: `Start position or insert after position exceed the playlist limits (max: ${nextPosition - 1})` })
311 .json({ error: `Start position or insert after position exceed the playlist limits (max: ${nextPosition - 1})` })
312 .end()
313
314 return 318 return
315 } 319 }
316 320
317 if (reorderLength && reorderLength + startPosition > nextPosition) { 321 if (reorderLength && reorderLength + startPosition > nextPosition) {
318 res.status(HttpStatusCode.BAD_REQUEST_400) 322 res.fail({ message: `Reorder length with this start position exceeds the playlist limits (max: ${nextPosition - startPosition})` })
319 .json({ error: `Reorder length with this start position exceeds the playlist limits (max: ${nextPosition - startPosition})` })
320 .end()
321
322 return 323 return
323 } 324 }
324 325
@@ -401,10 +402,10 @@ function getCommonPlaylistEditAttributes () {
401 402
402function checkUserCanManageVideoPlaylist (user: MUserAccountId, videoPlaylist: MVideoPlaylist, right: UserRight, res: express.Response) { 403function checkUserCanManageVideoPlaylist (user: MUserAccountId, videoPlaylist: MVideoPlaylist, right: UserRight, res: express.Response) {
403 if (videoPlaylist.isOwned() === false) { 404 if (videoPlaylist.isOwned() === false) {
404 res.status(HttpStatusCode.FORBIDDEN_403) 405 res.fail({
405 .json({ error: 'Cannot manage video playlist of another server.' }) 406 status: HttpStatusCode.FORBIDDEN_403,
406 .end() 407 message: 'Cannot manage video playlist of another server.'
407 408 })
408 return false 409 return false
409 } 410 }
410 411
@@ -412,10 +413,10 @@ function checkUserCanManageVideoPlaylist (user: MUserAccountId, videoPlaylist: M
412 // The user can delete it if s/he is an admin 413 // The user can delete it if s/he is an admin
413 // Or if s/he is the video playlist's owner 414 // Or if s/he is the video playlist's owner
414 if (user.hasRight(right) === false && videoPlaylist.ownerAccountId !== user.Account.id) { 415 if (user.hasRight(right) === false && videoPlaylist.ownerAccountId !== user.Account.id) {
415 res.status(HttpStatusCode.FORBIDDEN_403) 416 res.fail({
416 .json({ error: 'Cannot manage video playlist of another user' }) 417 status: HttpStatusCode.FORBIDDEN_403,
417 .end() 418 message: 'Cannot manage video playlist of another user'
418 419 })
419 return false 420 return false
420 } 421 }
421 422
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) {
37 37
38 const rate = await AccountVideoRateModel.loadLocalAndPopulateVideo(rateType, req.params.name, +req.params.videoId) 38 const rate = await AccountVideoRateModel.loadLocalAndPopulateVideo(rateType, req.params.name, +req.params.videoId)
39 if (!rate) { 39 if (!rate) {
40 return res.status(HttpStatusCode.NOT_FOUND_404) 40 return res.fail({
41 .json({ error: 'Video rate not found' }) 41 status: HttpStatusCode.NOT_FOUND_404,
42 message: 'Video rate not found'
43 })
42 } 44 }
43 45
44 res.locals.accountVideoRate = rate 46 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 = [
21 const user = res.locals.oauth.token.User 21 const user = res.locals.oauth.token.User
22 if (user.videosHistoryEnabled === false) { 22 if (user.videosHistoryEnabled === false) {
23 logger.warn('Cannot set videos to watch by user %d: videos history is disabled.', user.id) 23 logger.warn('Cannot set videos to watch by user %d: videos history is disabled.', user.id)
24 return res.status(HttpStatusCode.CONFLICT_409).end() 24 return res.fail({
25 status: HttpStatusCode.CONFLICT_409,
26 message: 'Video history is disabled'
27 })
25 } 28 }
26 29
27 return next() 30 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([
73 .custom(isIdValid).withMessage('Should have correct video channel id'), 73 .custom(isIdValid).withMessage('Should have correct video channel id'),
74 74
75 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 75 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
76 res.docs = "https://docs.joinpeertube.org/api-rest-reference.html#operation/uploadLegacy"
76 logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files }) 77 logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files })
77 78
78 if (areValidationErrors(req, res)) return cleanUpReqFiles(req) 79 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
@@ -88,9 +89,11 @@ const videosAddLegacyValidator = getCommonVideoEditAttributes().concat([
88 if (!videoFile.duration) await addDurationToVideo(videoFile) 89 if (!videoFile.duration) await addDurationToVideo(videoFile)
89 } catch (err) { 90 } catch (err) {
90 logger.error('Invalid input file in videosAddLegacyValidator.', { err }) 91 logger.error('Invalid input file in videosAddLegacyValidator.', { err })
91 res.status(HttpStatusCode.UNPROCESSABLE_ENTITY_422)
92 .json({ error: 'Video file unreadable.' })
93 92
93 res.fail({
94 status: HttpStatusCode.UNPROCESSABLE_ENTITY_422,
95 message: 'Video file unreadable.'
96 })
94 return cleanUpReqFiles(req) 97 return cleanUpReqFiles(req)
95 } 98 }
96 99
@@ -105,6 +108,7 @@ const videosAddLegacyValidator = getCommonVideoEditAttributes().concat([
105 */ 108 */
106const videosAddResumableValidator = [ 109const videosAddResumableValidator = [
107 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 110 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
111 res.docs = "https://docs.joinpeertube.org/api-rest-reference.html#operation/uploadResumable"
108 const user = res.locals.oauth.token.User 112 const user = res.locals.oauth.token.User
109 113
110 const body: express.CustomUploadXFile<express.UploadXFileMetadata> = req.body 114 const body: express.CustomUploadXFile<express.UploadXFileMetadata> = req.body
@@ -118,9 +122,11 @@ const videosAddResumableValidator = [
118 if (!file.duration) await addDurationToVideo(file) 122 if (!file.duration) await addDurationToVideo(file)
119 } catch (err) { 123 } catch (err) {
120 logger.error('Invalid input file in videosAddResumableValidator.', { err }) 124 logger.error('Invalid input file in videosAddResumableValidator.', { err })
121 res.status(HttpStatusCode.UNPROCESSABLE_ENTITY_422)
122 .json({ error: 'Video file unreadable.' })
123 125
126 res.fail({
127 status: HttpStatusCode.UNPROCESSABLE_ENTITY_422,
128 message: 'Video file unreadable.'
129 })
124 return cleanup() 130 return cleanup()
125 } 131 }
126 132
@@ -164,6 +170,7 @@ const videosAddResumableInitValidator = getCommonVideoEditAttributes().concat([
164 .withMessage('Should specify the file mimetype'), 170 .withMessage('Should specify the file mimetype'),
165 171
166 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 172 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
173 res.docs = "https://docs.joinpeertube.org/api-rest-reference.html#operation/uploadResumableInit"
167 const videoFileMetadata = { 174 const videoFileMetadata = {
168 mimetype: req.headers['x-upload-content-type'] as string, 175 mimetype: req.headers['x-upload-content-type'] as string,
169 size: +req.headers['x-upload-content-length'], 176 size: +req.headers['x-upload-content-length'],
@@ -207,6 +214,7 @@ const videosUpdateValidator = getCommonVideoEditAttributes().concat([
207 .custom(isIdValid).withMessage('Should have correct video channel id'), 214 .custom(isIdValid).withMessage('Should have correct video channel id'),
208 215
209 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 216 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
217 res.docs = 'https://docs.joinpeertube.org/api-rest-reference.html#operation/putVideo'
210 logger.debug('Checking videosUpdate parameters', { parameters: req.body }) 218 logger.debug('Checking videosUpdate parameters', { parameters: req.body })
211 219
212 if (areValidationErrors(req, res)) return cleanUpReqFiles(req) 220 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
@@ -242,12 +250,14 @@ async function checkVideoFollowConstraints (req: express.Request, res: express.R
242 const serverActor = await getServerActor() 250 const serverActor = await getServerActor()
243 if (await VideoModel.checkVideoHasInstanceFollow(video.id, serverActor.id) === true) return next() 251 if (await VideoModel.checkVideoHasInstanceFollow(video.id, serverActor.id) === true) return next()
244 252
245 return res.status(HttpStatusCode.FORBIDDEN_403) 253 return res.fail({
246 .json({ 254 status: HttpStatusCode.FORBIDDEN_403,
247 errorCode: ServerErrorCode.DOES_NOT_RESPECT_FOLLOW_CONSTRAINTS, 255 message: 'Cannot get this video regarding follow constraints.',
248 error: 'Cannot get this video regarding follow constraints.', 256 type: ServerErrorCode.DOES_NOT_RESPECT_FOLLOW_CONSTRAINTS.toString(),
249 originUrl: video.url 257 data: {
250 }) 258 originUrl: video.url
259 }
260 })
251} 261}
252 262
253const videosCustomGetValidator = ( 263const videosCustomGetValidator = (
@@ -258,6 +268,7 @@ const videosCustomGetValidator = (
258 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), 268 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
259 269
260 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 270 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
271 res.docs = 'https://docs.joinpeertube.org/api-rest-reference.html#operation/getVideo'
261 logger.debug('Checking videosGet parameters', { parameters: req.params }) 272 logger.debug('Checking videosGet parameters', { parameters: req.params })
262 273
263 if (areValidationErrors(req, res)) return 274 if (areValidationErrors(req, res)) return
@@ -276,8 +287,10 @@ const videosCustomGetValidator = (
276 287
277 // Only the owner or a user that have blacklist rights can see the video 288 // Only the owner or a user that have blacklist rights can see the video
278 if (!user || !user.canGetVideo(video)) { 289 if (!user || !user.canGetVideo(video)) {
279 return res.status(HttpStatusCode.FORBIDDEN_403) 290 return res.fail({
280 .json({ error: 'Cannot get this private/internal or blacklisted video.' }) 291 status: HttpStatusCode.FORBIDDEN_403,
292 message: 'Cannot get this private/internal or blacklisted video.'
293 })
281 } 294 }
282 295
283 return next() 296 return next()
@@ -291,7 +304,10 @@ const videosCustomGetValidator = (
291 if (isUUIDValid(req.params.id)) return next() 304 if (isUUIDValid(req.params.id)) return next()
292 305
293 // Don't leak this unlisted video 306 // Don't leak this unlisted video
294 return res.status(HttpStatusCode.NOT_FOUND_404).end() 307 return res.fail({
308 status: HttpStatusCode.NOT_FOUND_404,
309 message: 'Video not found'
310 })
295 } 311 }
296 } 312 }
297 ] 313 ]
@@ -318,6 +334,7 @@ const videosRemoveValidator = [
318 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), 334 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
319 335
320 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 336 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
337 res.docs = "https://docs.joinpeertube.org/api-rest-reference.html#operation/delVideo"
321 logger.debug('Checking videosRemove parameters', { parameters: req.params }) 338 logger.debug('Checking videosRemove parameters', { parameters: req.params })
322 339
323 if (areValidationErrors(req, res)) return 340 if (areValidationErrors(req, res)) return
@@ -344,13 +361,11 @@ const videosChangeOwnershipValidator = [
344 361
345 const nextOwner = await AccountModel.loadLocalByName(req.body.username) 362 const nextOwner = await AccountModel.loadLocalByName(req.body.username)
346 if (!nextOwner) { 363 if (!nextOwner) {
347 res.status(HttpStatusCode.BAD_REQUEST_400) 364 res.fail({ message: 'Changing video ownership to a remote account is not supported yet' })
348 .json({ error: 'Changing video ownership to a remote account is not supported yet' })
349
350 return 365 return
351 } 366 }
352 res.locals.nextOwner = nextOwner
353 367
368 res.locals.nextOwner = nextOwner
354 return next() 369 return next()
355 } 370 }
356] 371]
@@ -370,8 +385,10 @@ const videosTerminateChangeOwnershipValidator = [
370 const videoChangeOwnership = res.locals.videoChangeOwnership 385 const videoChangeOwnership = res.locals.videoChangeOwnership
371 386
372 if (videoChangeOwnership.status !== VideoChangeOwnershipStatus.WAITING) { 387 if (videoChangeOwnership.status !== VideoChangeOwnershipStatus.WAITING) {
373 res.status(HttpStatusCode.FORBIDDEN_403) 388 res.fail({
374 .json({ error: 'Ownership already accepted or refused' }) 389 status: HttpStatusCode.FORBIDDEN_403,
390 message: 'Ownership already accepted or refused'
391 })
375 return 392 return
376 } 393 }
377 394
@@ -388,9 +405,10 @@ const videosAcceptChangeOwnershipValidator = [
388 const videoChangeOwnership = res.locals.videoChangeOwnership 405 const videoChangeOwnership = res.locals.videoChangeOwnership
389 const isAble = await isAbleToUploadVideo(user.id, videoChangeOwnership.Video.getMaxQualityFile().size) 406 const isAble = await isAbleToUploadVideo(user.id, videoChangeOwnership.Video.getMaxQualityFile().size)
390 if (isAble === false) { 407 if (isAble === false) {
391 res.status(HttpStatusCode.PAYLOAD_TOO_LARGE_413) 408 res.fail({
392 .json({ error: 'The user video quota is exceeded with this video.' }) 409 status: HttpStatusCode.PAYLOAD_TOO_LARGE_413,
393 410 message: 'The user video quota is exceeded with this video.'
411 })
394 return 412 return
395 } 413 }
396 414
@@ -538,9 +556,10 @@ const commonVideosFiltersValidator = [
538 (req.query.filter === 'all-local' || req.query.filter === 'all') && 556 (req.query.filter === 'all-local' || req.query.filter === 'all') &&
539 (!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) === false) 557 (!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) === false)
540 ) { 558 ) {
541 res.status(HttpStatusCode.UNAUTHORIZED_401) 559 res.fail({
542 .json({ error: 'You are not allowed to see all local videos.' }) 560 status: HttpStatusCode.UNAUTHORIZED_401,
543 561 message: 'You are not allowed to see all local videos.'
562 })
544 return 563 return
545 } 564 }
546 565
@@ -581,9 +600,7 @@ function areErrorsInScheduleUpdate (req: express.Request, res: express.Response)
581 if (!req.body.scheduleUpdate.updateAt) { 600 if (!req.body.scheduleUpdate.updateAt) {
582 logger.warn('Invalid parameters: scheduleUpdate.updateAt is mandatory.') 601 logger.warn('Invalid parameters: scheduleUpdate.updateAt is mandatory.')
583 602
584 res.status(HttpStatusCode.BAD_REQUEST_400) 603 res.fail({ message: 'Schedule update at is mandatory.' })
585 .json({ error: 'Schedule update at is mandatory.' })
586
587 return true 604 return true
588 } 605 }
589 } 606 }
@@ -605,26 +622,27 @@ async function commonVideoChecksPass (parameters: {
605 if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return false 622 if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return false
606 623
607 if (!isVideoFileMimeTypeValid(files)) { 624 if (!isVideoFileMimeTypeValid(files)) {
608 res.status(HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415) 625 res.fail({
609 .json({ 626 status: HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415,
610 error: 'This file is not supported. Please, make sure it is of the following type: ' + 627 message: 'This file is not supported. Please, make sure it is of the following type: ' +
611 CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ') 628 CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ')
612 }) 629 })
613
614 return false 630 return false
615 } 631 }
616 632
617 if (!isVideoFileSizeValid(videoFileSize.toString())) { 633 if (!isVideoFileSizeValid(videoFileSize.toString())) {
618 res.status(HttpStatusCode.PAYLOAD_TOO_LARGE_413) 634 res.fail({
619 .json({ error: 'This file is too large. It exceeds the maximum file size authorized.' }) 635 status: HttpStatusCode.PAYLOAD_TOO_LARGE_413,
620 636 message: 'This file is too large. It exceeds the maximum file size authorized.'
637 })
621 return false 638 return false
622 } 639 }
623 640
624 if (await isAbleToUploadVideo(user.id, videoFileSize) === false) { 641 if (await isAbleToUploadVideo(user.id, videoFileSize) === false) {
625 res.status(HttpStatusCode.PAYLOAD_TOO_LARGE_413) 642 res.fail({
626 .json({ error: 'The user video quota is exceeded with this video.' }) 643 status: HttpStatusCode.PAYLOAD_TOO_LARGE_413,
627 644 message: 'The user video quota is exceeded with this video.'
645 })
628 return false 646 return false
629 } 647 }
630 648
@@ -650,9 +668,10 @@ export async function isVideoAccepted (
650 668
651 if (!acceptedResult || acceptedResult.accepted !== true) { 669 if (!acceptedResult || acceptedResult.accepted !== true) {
652 logger.info('Refused local video.', { acceptedResult, acceptParameters }) 670 logger.info('Refused local video.', { acceptedResult, acceptParameters })
653 res.status(HttpStatusCode.FORBIDDEN_403) 671 res.fail({
654 .json({ error: acceptedResult.errorMessage || 'Refused local video' }) 672 status: HttpStatusCode.FORBIDDEN_403,
655 673 message: acceptedResult.errorMessage || 'Refused local video'
674 })
656 return false 675 return false
657 } 676 }
658 677
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 = [
21 21
22 const actor = await ActorModel.loadLocalUrlByName(name) 22 const actor = await ActorModel.loadLocalUrlByName(name)
23 if (!actor) { 23 if (!actor) {
24 return res.status(HttpStatusCode.NOT_FOUND_404) 24 return res.fail({
25 .send({ error: 'Actor not found' }) 25 status: HttpStatusCode.NOT_FOUND_404,
26 .end() 26 message: 'Actor not found'
27 })
27 } 28 }
28 29
29 res.locals.actorUrl = actor 30 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
19import { MockSmtpServer } from '../../../../shared/extra-utils/miscs/email' 19import { MockSmtpServer } from '../../../../shared/extra-utils/miscs/email'
20import { waitJobs } from '../../../../shared/extra-utils/server/jobs' 20import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
21import { User } from '../../../../shared/models/users' 21import { User } from '../../../../shared/models/users'
22import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
22 23
23const expect = chai.expect 24const expect = chai.expect
24 25
@@ -89,8 +90,8 @@ describe('Test users account verification', function () {
89 }) 90 })
90 91
91 it('Should not allow login for user with unverified email', async function () { 92 it('Should not allow login for user with unverified email', async function () {
92 const resLogin = await login(server.url, server.client, user1, 400) 93 const resLogin = await login(server.url, server.client, user1, HttpStatusCode.BAD_REQUEST_400)
93 expect(resLogin.body.error).to.contain('User email is not verified.') 94 expect(resLogin.body.detail).to.contain('User email is not verified.')
94 }) 95 })
95 96
96 it('Should verify the user via email and allow login', async function () { 97 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 () {
93 const client = { id: 'client', secret: server.client.secret } 93 const client = { id: 'client', secret: server.client.secret }
94 const res = await login(server.url, client, server.user, HttpStatusCode.BAD_REQUEST_400) 94 const res = await login(server.url, client, server.user, HttpStatusCode.BAD_REQUEST_400)
95 95
96 expect(res.body.code).to.equal('invalid_client') 96 expect(res.body.type).to.equal('invalid_client')
97 expect(res.body.error).to.contain('client is invalid') 97 expect(res.body.detail).to.contain('client is invalid')
98 }) 98 })
99 99
100 it('Should not login with an invalid client secret', async function () { 100 it('Should not login with an invalid client secret', async function () {
101 const client = { id: server.client.id, secret: 'coucou' } 101 const client = { id: server.client.id, secret: 'coucou' }
102 const res = await login(server.url, client, server.user, HttpStatusCode.BAD_REQUEST_400) 102 const res = await login(server.url, client, server.user, HttpStatusCode.BAD_REQUEST_400)
103 103
104 expect(res.body.code).to.equal('invalid_client') 104 expect(res.body.type).to.equal('invalid_client')
105 expect(res.body.error).to.contain('client is invalid') 105 expect(res.body.detail).to.contain('client is invalid')
106 }) 106 })
107 }) 107 })
108 108
@@ -112,16 +112,16 @@ describe('Test users', function () {
112 const user = { username: 'captain crochet', password: server.user.password } 112 const user = { username: 'captain crochet', password: server.user.password }
113 const res = await login(server.url, server.client, user, HttpStatusCode.BAD_REQUEST_400) 113 const res = await login(server.url, server.client, user, HttpStatusCode.BAD_REQUEST_400)
114 114
115 expect(res.body.code).to.equal('invalid_grant') 115 expect(res.body.type).to.equal('invalid_grant')
116 expect(res.body.error).to.contain('credentials are invalid') 116 expect(res.body.detail).to.contain('credentials are invalid')
117 }) 117 })
118 118
119 it('Should not login with an invalid password', async function () { 119 it('Should not login with an invalid password', async function () {
120 const user = { username: server.user.username, password: 'mew_three' } 120 const user = { username: server.user.username, password: 'mew_three' }
121 const res = await login(server.url, server.client, user, HttpStatusCode.BAD_REQUEST_400) 121 const res = await login(server.url, server.client, user, HttpStatusCode.BAD_REQUEST_400)
122 122
123 expect(res.body.code).to.equal('invalid_grant') 123 expect(res.body.type).to.equal('invalid_grant')
124 expect(res.body.error).to.contain('credentials are invalid') 124 expect(res.body.detail).to.contain('credentials are invalid')
125 }) 125 })
126 126
127 it('Should not be able to upload a video', async function () { 127 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-
22import { HttpMethod } from '@shared/core-utils/miscs/http-methods' 22import { HttpMethod } from '@shared/core-utils/miscs/http-methods'
23import { VideoCreate } from '@shared/models' 23import { VideoCreate } from '@shared/models'
24import { File as UploadXFile, Metadata } from '@uploadx/core' 24import { File as UploadXFile, Metadata } from '@uploadx/core'
25import { ProblemDocumentOptions } from 'http-problem-details/dist/ProblemDocument'
25import { RegisteredPlugin } from '../../lib/plugins/plugin-manager' 26import { RegisteredPlugin } from '../../lib/plugins/plugin-manager'
26import { 27import {
27 MAccountDefault, 28 MAccountDefault,
@@ -83,8 +84,15 @@ declare module 'express' {
83 filename: string 84 filename: string
84 } 85 }
85 86
86 // Extends locals property from Response 87 // Extends Response with added functions and potential variables passed by middlewares
87 interface Response { 88 interface Response {
89 docs?: string
90 fail: (options: {
91 data?: Record<string, Object>
92 docs?: string
93 message: string
94 } & ProblemDocumentOptions) => void
95
88 locals: { 96 locals: {
89 videoAll?: MVideoFullLight 97 videoAll?: MVideoFullLight
90 onlyImmutableVideo?: MVideoImmutable 98 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:
38 # Errors 38 # Errors
39 39
40 The API uses standard HTTP status codes to indicate the success or failure 40 The API uses standard HTTP status codes to indicate the success or failure
41 of the API call. 41 of the API call, completed by a [RFC7807-compliant](https://tools.ietf.org/html/rfc7807) response body.
42 42
43 ``` 43 ```
44 HTTP 1.1 404 Not Found 44 HTTP 1.1 404 Not Found
45 Content-Type: application/json 45 Content-Type: application/problem+json; charset=utf-8
46 46
47 { 47 {
48 "errorCode": 1 48 "detail": "Video not found",
49 "error": "Account not found" 49 "status": 404,
50 "title": "Not Found",
51 "type": "about:blank"
50 } 52 }
51 ``` 53 ```
52 54
53 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), 55 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),
54 but it is still optional. 56 but it is still optional.
55 57
56 ### Validation errors 58 ### Validation errors
57 59
58 Each parameter is evaluated on its own against a set of rules before the route validator 60 Each parameter is evaluated on its own against a set of rules before the route validator
59 proceeds with potential testing involving parameter combinations. Errors coming from Validation 61 proceeds with potential testing involving parameter combinations. Errors coming from validation
60 errors appear earlier and benefit from a more detailed error type: 62 errors appear earlier and benefit from a more detailed error type:
61 63
62 ``` 64 ```
63 HTTP 1.1 400 Bad Request 65 HTTP 1.1 400 Bad Request
64 Content-Type: application/json 66 Content-Type: application/problem+json; charset=utf-8
65 67
66 { 68 {
67 "errors": { 69 "detail": "Incorrect request parameters: id",
70 "instance": "/api/v1/videos/9c9de5e8-0a1e-484a-b099-e80766180",
71 "invalid-params": {
68 "id": { 72 "id": {
69 "value": "a117eb-c6a9-4756-bb09-2a956239f", 73 "location": "params",
70 "msg": "Should have a valid id", 74 "msg": "Invalid value",
71 "param": "id", 75 "param": "id",
72 "location": "params" 76 "value": "9c9de5e8-0a1e-484a-b099-e80766180"
73 } 77 }
74 } 78 },
79 "status": 400,
80 "title": "Bad Request",
81 "type": "about:blank"
75 } 82 }
76 ``` 83 ```
77 84
78 Where `id` is the name of the field concerned by the error, within the route definition. 85 Where `id` is the name of the field concerned by the error, within the route definition.
79 `errors.<field>.location` can be either 'params', 'body', 'header', 'query' or 'cookies', and 86 `invalid-params.<field>.location` can be either 'params', 'body', 'header', 'query' or 'cookies', and
80 `errors.<field>.value` reports the value that didn't pass validation whose `errors.<field>.msg` 87 `invalid-params.<field>.value` reports the value that didn't pass validation whose `invalid-params.<field>.msg`
81 is about. 88 is about.
82 89
83 # Rate limits 90 # 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:
4136 resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.3.tgz#01d2709c79d41698bb01d4decc5e9da4e4a033d9" 4136 resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.3.tgz#01d2709c79d41698bb01d4decc5e9da4e4a033d9"
4137 integrity sha512-t7hjvef/5HEK7RWTdUzVUhl8zkEu+LlaE0IYzdMuvbSDipxBRpOn4Uhw8ZyECEa808iVT8XCjzo6xmYt4CiLZg== 4137 integrity sha512-t7hjvef/5HEK7RWTdUzVUhl8zkEu+LlaE0IYzdMuvbSDipxBRpOn4Uhw8ZyECEa808iVT8XCjzo6xmYt4CiLZg==
4138 4138
4139http-problem-details@^0.1.5:
4140 version "0.1.5"
4141 resolved "https://registry.yarnpkg.com/http-problem-details/-/http-problem-details-0.1.5.tgz#f8f94f4ab9d4050749e9f8566fb85bb8caa2be56"
4142 integrity sha512-GHxfQZ0POP4FWbAM0guOyZyJNWwbLUXp+4XOJdmitS2tp3gHVSatrSX59Yyq/dCkhk4KiGtTWIlXZC83yCkBkA==
4143
4139http-signature@1.3.5: 4144http-signature@1.3.5:
4140 version "1.3.5" 4145 version "1.3.5"
4141 resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.3.5.tgz#9f19496ffbf3227298d7b5f156e0e1a948678683" 4146 resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.3.5.tgz#9f19496ffbf3227298d7b5f156e0e1a948678683"