aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2021-06-02 18:15:41 +0200
committerChocobozzz <me@florianbigard.com>2021-06-02 18:15:41 +0200
commite030bfb59dd5ee65f20a64686ec9b22ca39f70ae (patch)
treec9a439159ef540291e3c030bcaf958b953442147
parent463206948d6a9d46e7e68d55c7b763e601ecc870 (diff)
downloadPeerTube-e030bfb59dd5ee65f20a64686ec9b22ca39f70ae.tar.gz
PeerTube-e030bfb59dd5ee65f20a64686ec9b22ca39f70ae.tar.zst
PeerTube-e030bfb59dd5ee65f20a64686ec9b22ca39f70ae.zip
Refactor server errors handler
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-go-live.component.ts8
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.ts6
-rw-r--r--client/src/app/+videos/+video-watch/video-watch.component.ts8
-rw-r--r--client/src/app/shared/shared-main/auth/auth-interceptor.service.ts5
-rw-r--r--client/src/standalone/videos/embed.ts5
-rw-r--r--server.ts7
-rw-r--r--server/controllers/api/videos/index.ts3
-rw-r--r--server/controllers/api/videos/update.ts2
-rw-r--r--server/controllers/api/videos/upload.ts4
-rw-r--r--server/helpers/express-utils.ts32
-rw-r--r--server/middlewares/doc.ts13
-rw-r--r--server/middlewares/error.ts39
-rw-r--r--server/middlewares/index.ts2
-rw-r--r--server/middlewares/validators/videos/videos.ts6
-rw-r--r--server/tests/api/check-params/videos.ts78
-rw-r--r--server/tests/api/server/follow-constraints.ts16
-rw-r--r--server/typings/express/index.d.ts19
-rw-r--r--shared/models/server/index.ts1
-rw-r--r--shared/models/server/peertube-problem-document.model.ts32
-rw-r--r--shared/models/server/server-error-code.enum.ts10
20 files changed, 236 insertions, 60 deletions
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-go-live.component.ts b/client/src/app/+videos/+video-edit/video-add-components/video-go-live.component.ts
index 727bbc32f..15178a267 100644
--- a/client/src/app/+videos/+video-edit/video-add-components/video-go-live.component.ts
+++ b/client/src/app/+videos/+video-edit/video-add-components/video-go-live.component.ts
@@ -8,7 +8,7 @@ import { FormValidatorService } from '@app/shared/shared-forms'
8import { VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main' 8import { VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main'
9import { LiveVideoService } from '@app/shared/shared-video-live' 9import { LiveVideoService } from '@app/shared/shared-video-live'
10import { LoadingBarService } from '@ngx-loading-bar/core' 10import { LoadingBarService } from '@ngx-loading-bar/core'
11import { LiveVideo, LiveVideoCreate, LiveVideoUpdate, ServerErrorCode, VideoPrivacy } from '@shared/models' 11import { LiveVideo, LiveVideoCreate, LiveVideoUpdate, PeerTubeProblemDocument, ServerErrorCode, VideoPrivacy } from '@shared/models'
12import { VideoSend } from './video-send' 12import { VideoSend } from './video-send'
13 13
14@Component({ 14@Component({
@@ -92,9 +92,11 @@ export class VideoGoLiveComponent extends VideoSend implements OnInit, AfterView
92 92
93 let message = err.message 93 let message = err.message
94 94
95 if (err.body?.code === ServerErrorCode.MAX_INSTANCE_LIVES_LIMIT_REACHED) { 95 const error = err.body as PeerTubeProblemDocument
96
97 if (error?.code === ServerErrorCode.MAX_INSTANCE_LIVES_LIMIT_REACHED) {
96 message = $localize`Cannot create live because this instance have too many created lives` 98 message = $localize`Cannot create live because this instance have too many created lives`
97 } else if (err.body?.code) { 99 } else if (error?.code === ServerErrorCode.MAX_USER_LIVES_LIMIT_REACHED) {
98 message = $localize`Cannot create live because you created too many lives` 100 message = $localize`Cannot create live because you created too many lives`
99 } 101 }
100 102
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.ts b/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.ts
index 23bd5ef76..2837b30c1 100644
--- a/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.ts
+++ b/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.ts
@@ -5,7 +5,7 @@ import { scrollToTop } from '@app/helpers'
5import { FormValidatorService } from '@app/shared/shared-forms' 5import { FormValidatorService } from '@app/shared/shared-forms'
6import { VideoCaptionService, VideoEdit, VideoImportService, VideoService } from '@app/shared/shared-main' 6import { VideoCaptionService, VideoEdit, VideoImportService, VideoService } from '@app/shared/shared-main'
7import { LoadingBarService } from '@ngx-loading-bar/core' 7import { LoadingBarService } from '@ngx-loading-bar/core'
8import { ServerErrorCode, VideoPrivacy, VideoUpdate } from '@shared/models' 8import { PeerTubeProblemDocument, ServerErrorCode, VideoPrivacy, VideoUpdate } from '@shared/models'
9import { hydrateFormFromVideo } from '../shared/video-edit-utils' 9import { hydrateFormFromVideo } from '../shared/video-edit-utils'
10import { VideoSend } from './video-send' 10import { VideoSend } from './video-send'
11 11
@@ -115,7 +115,9 @@ export class VideoImportTorrentComponent extends VideoSend implements OnInit, Af
115 this.firstStepError.emit() 115 this.firstStepError.emit()
116 116
117 let message = err.message 117 let message = err.message
118 if (err.body?.code === ServerErrorCode.INCORRECT_FILES_IN_TORRENT) { 118
119 const error = err.body as PeerTubeProblemDocument
120 if (error?.code === ServerErrorCode.INCORRECT_FILES_IN_TORRENT) {
119 message = $localize`Torrents with only 1 file are supported.` 121 message = $localize`Torrents with only 1 file are supported.`
120 } 122 }
121 123
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 8034ccebf..540b568ed 100644
--- a/client/src/app/+videos/+video-watch/video-watch.component.ts
+++ b/client/src/app/+videos/+video-watch/video-watch.component.ts
@@ -28,7 +28,7 @@ import { VideoActionsDisplayType, VideoDownloadComponent } from '@app/shared/sha
28import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist' 28import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist'
29import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage' 29import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage'
30import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' 30import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
31import { ServerConfig, ServerErrorCode, UserVideoRateType, VideoCaption, VideoPrivacy, VideoState } from '@shared/models' 31import { PeerTubeProblemDocument, ServerConfig, ServerErrorCode, UserVideoRateType, VideoCaption, VideoPrivacy, VideoState } from '@shared/models'
32import { 32import {
33 cleanupVideoWatch, 33 cleanupVideoWatch,
34 getStoredP2PEnabled, 34 getStoredP2PEnabled,
@@ -431,9 +431,11 @@ 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.type === ServerErrorCode.DOES_NOT_RESPECT_FOLLOW_CONSTRAINTS && err.body.originUrl) { 434 const errorBody = err.body as PeerTubeProblemDocument
435
436 if (errorBody.code === ServerErrorCode.DOES_NOT_RESPECT_FOLLOW_CONSTRAINTS && errorBody.originUrl) {
435 const search = window.location.search 437 const search = window.location.search
436 let originUrl = err.body.originUrl 438 let originUrl = errorBody.originUrl
437 if (search) originUrl += search 439 if (search) originUrl += search
438 440
439 this.confirmService.confirm( 441 this.confirmService.confirm(
diff --git a/client/src/app/shared/shared-main/auth/auth-interceptor.service.ts b/client/src/app/shared/shared-main/auth/auth-interceptor.service.ts
index 4fe3b964d..5bcad36d0 100644
--- a/client/src/app/shared/shared-main/auth/auth-interceptor.service.ts
+++ b/client/src/app/shared/shared-main/auth/auth-interceptor.service.ts
@@ -5,6 +5,7 @@ import { Injectable, Injector } from '@angular/core'
5import { AuthService } from '@app/core/auth/auth.service' 5import { AuthService } from '@app/core/auth/auth.service'
6import { Router } from '@angular/router' 6import { Router } from '@angular/router'
7import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' 7import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
8import { OAuth2ErrorCode, PeerTubeProblemDocument, ServerErrorCode } from '@shared/models/server'
8 9
9@Injectable() 10@Injectable()
10export class AuthInterceptor implements HttpInterceptor { 11export class AuthInterceptor implements HttpInterceptor {
@@ -25,7 +26,9 @@ export class AuthInterceptor implements HttpInterceptor {
25 return next.handle(authReq) 26 return next.handle(authReq)
26 .pipe( 27 .pipe(
27 catchError((err: HttpErrorResponse) => { 28 catchError((err: HttpErrorResponse) => {
28 if (err.status === HttpStatusCode.UNAUTHORIZED_401 && err.error && err.error.code === 'invalid_token') { 29 const error = err.error as PeerTubeProblemDocument
30
31 if (err.status === HttpStatusCode.UNAUTHORIZED_401 && error && error.code === OAuth2ErrorCode.INVALID_TOKEN) {
29 return this.handleTokenExpired(req, next) 32 return this.handleTokenExpired(req, next)
30 } 33 }
31 34
diff --git a/client/src/standalone/videos/embed.ts b/client/src/standalone/videos/embed.ts
index fc61d3730..4ce5c78e8 100644
--- a/client/src/standalone/videos/embed.ts
+++ b/client/src/standalone/videos/embed.ts
@@ -5,6 +5,7 @@ import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-c
5import { 5import {
6 ClientHookName, 6 ClientHookName,
7 HTMLServerConfig, 7 HTMLServerConfig,
8 OAuth2ErrorCode,
8 PluginType, 9 PluginType,
9 ResultList, 10 ResultList,
10 UserRefreshToken, 11 UserRefreshToken,
@@ -118,8 +119,8 @@ export class PeerTubeEmbed {
118 if (res.status === HttpStatusCode.UNAUTHORIZED_401) return undefined 119 if (res.status === HttpStatusCode.UNAUTHORIZED_401) return undefined
119 120
120 return res.json() 121 return res.json()
121 }).then((obj: UserRefreshToken & { code: 'invalid_grant'}) => { 122 }).then((obj: UserRefreshToken & { code?: OAuth2ErrorCode }) => {
122 if (!obj || obj.code === 'invalid_grant') { 123 if (!obj || obj.code === OAuth2ErrorCode.INVALID_GRANT) {
123 Tokens.flush() 124 Tokens.flush()
124 this.removeTokensFromHeaders() 125 this.removeTokensFromHeaders()
125 126
diff --git a/server.ts b/server.ts
index 66c9173ca..2caee18e7 100644
--- a/server.ts
+++ b/server.ts
@@ -106,6 +106,7 @@ import {
106 downloadRouter 106 downloadRouter
107} from './server/controllers' 107} from './server/controllers'
108import { advertiseDoNotTrack } from './server/middlewares/dnt' 108import { advertiseDoNotTrack } from './server/middlewares/dnt'
109import { apiFailMiddleware } from './server/middlewares/error'
109import { Redis } from './server/lib/redis' 110import { Redis } from './server/lib/redis'
110import { ActorFollowScheduler } from './server/lib/schedulers/actor-follow-scheduler' 111import { ActorFollowScheduler } from './server/lib/schedulers/actor-follow-scheduler'
111import { RemoveOldViewsScheduler } from './server/lib/schedulers/remove-old-views-scheduler' 112import { RemoveOldViewsScheduler } from './server/lib/schedulers/remove-old-views-scheduler'
@@ -127,7 +128,6 @@ import { LiveManager } from './server/lib/live-manager'
127import { HttpStatusCode } from './shared/core-utils/miscs/http-error-codes' 128import { HttpStatusCode } from './shared/core-utils/miscs/http-error-codes'
128import { VideosTorrentCache } from '@server/lib/files-cache/videos-torrent-cache' 129import { VideosTorrentCache } from '@server/lib/files-cache/videos-torrent-cache'
129import { ServerConfigManager } from '@server/lib/server-config-manager' 130import { ServerConfigManager } from '@server/lib/server-config-manager'
130import { apiResponseHelpers } from '@server/helpers/express-utils'
131 131
132// ----------- Command line ----------- 132// ----------- Command line -----------
133 133
@@ -169,8 +169,8 @@ app.use(morgan('combined', {
169 skip: req => CONFIG.LOG.LOG_PING_REQUESTS === false && req.originalUrl === '/api/v1/ping' 169 skip: req => CONFIG.LOG.LOG_PING_REQUESTS === false && req.originalUrl === '/api/v1/ping'
170})) 170}))
171 171
172// Response helpers used for errors 172// Add .fail() helper to response
173app.use(apiResponseHelpers) 173app.use(apiFailMiddleware)
174 174
175// For body requests 175// For body requests
176app.use(express.urlencoded({ extended: false })) 176app.use(express.urlencoded({ extended: false }))
@@ -179,6 +179,7 @@ app.use(express.json({
179 limit: '500kb', 179 limit: '500kb',
180 verify: (req: express.Request, res: express.Response, buf: Buffer) => { 180 verify: (req: express.Request, res: express.Response, buf: Buffer) => {
181 const valid = isHTTPSignatureDigestValid(buf, req) 181 const valid = isHTTPSignatureDigestValid(buf, req)
182
182 if (valid !== true) { 183 if (valid !== true) {
183 res.fail({ 184 res.fail({
184 status: HttpStatusCode.FORBIDDEN_403, 185 status: HttpStatusCode.FORBIDDEN_403,
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts
index db23e5630..7671f099e 100644
--- a/server/controllers/api/videos/index.ts
+++ b/server/controllers/api/videos/index.ts
@@ -2,6 +2,7 @@ import * as express from 'express'
2import toInt from 'validator/lib/toInt' 2import toInt from 'validator/lib/toInt'
3import { doJSONRequest } from '@server/helpers/requests' 3import { doJSONRequest } from '@server/helpers/requests'
4import { LiveManager } from '@server/lib/live-manager' 4import { LiveManager } from '@server/lib/live-manager'
5import { docMiddleware } from '@server/middlewares/doc'
5import { getServerActor } from '@server/models/application/application' 6import { getServerActor } from '@server/models/application/application'
6import { MVideoAccountLight } from '@server/types/models' 7import { MVideoAccountLight } from '@server/types/models'
7import { VideosCommonQuery } from '../../../../shared' 8import { VideosCommonQuery } from '../../../../shared'
@@ -83,6 +84,7 @@ videosRouter.get('/:id/metadata/:videoFileId',
83 asyncMiddleware(getVideoFileMetadata) 84 asyncMiddleware(getVideoFileMetadata)
84) 85)
85videosRouter.get('/:id', 86videosRouter.get('/:id',
87 docMiddleware('https://docs.joinpeertube.org/api-rest-reference.html#operation/getVideo'),
86 optionalAuthenticate, 88 optionalAuthenticate,
87 asyncMiddleware(videosCustomGetValidator('only-video-with-rights')), 89 asyncMiddleware(videosCustomGetValidator('only-video-with-rights')),
88 asyncMiddleware(checkVideoFollowConstraints), 90 asyncMiddleware(checkVideoFollowConstraints),
@@ -94,6 +96,7 @@ videosRouter.post('/:id/views',
94) 96)
95 97
96videosRouter.delete('/:id', 98videosRouter.delete('/:id',
99 docMiddleware('https://docs.joinpeertube.org/api-rest-reference.html#operation/delVideo'),
97 authenticate, 100 authenticate,
98 asyncMiddleware(videosRemoveValidator), 101 asyncMiddleware(videosRemoveValidator),
99 asyncRetryTransactionMiddleware(removeVideo) 102 asyncRetryTransactionMiddleware(removeVideo)
diff --git a/server/controllers/api/videos/update.ts b/server/controllers/api/videos/update.ts
index 2450abd0e..09e584d30 100644
--- a/server/controllers/api/videos/update.ts
+++ b/server/controllers/api/videos/update.ts
@@ -20,6 +20,7 @@ import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist'
20import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videosUpdateValidator } from '../../../middlewares' 20import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videosUpdateValidator } from '../../../middlewares'
21import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update' 21import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update'
22import { VideoModel } from '../../../models/video/video' 22import { VideoModel } from '../../../models/video/video'
23import { docMiddleware } from '@server/middlewares/doc'
23 24
24const lTags = loggerTagsFactory('api', 'video') 25const lTags = loggerTagsFactory('api', 'video')
25const auditLogger = auditLoggerFactory('videos') 26const auditLogger = auditLoggerFactory('videos')
@@ -35,6 +36,7 @@ const reqVideoFileUpdate = createReqFiles(
35) 36)
36 37
37updateRouter.put('/:id', 38updateRouter.put('/:id',
39 docMiddleware('https://docs.joinpeertube.org/api-rest-reference.html#operation/putVideo'),
38 authenticate, 40 authenticate,
39 reqVideoFileUpdate, 41 reqVideoFileUpdate,
40 asyncMiddleware(videosUpdateValidator), 42 asyncMiddleware(videosUpdateValidator),
diff --git a/server/controllers/api/videos/upload.ts b/server/controllers/api/videos/upload.ts
index c33d7fcb9..93a68f759 100644
--- a/server/controllers/api/videos/upload.ts
+++ b/server/controllers/api/videos/upload.ts
@@ -6,6 +6,7 @@ import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent'
6import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url' 6import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url'
7import { addOptimizeOrMergeAudioJob, buildLocalVideoFromReq, buildVideoThumbnailsFromReq, setVideoTags } from '@server/lib/video' 7import { addOptimizeOrMergeAudioJob, buildLocalVideoFromReq, buildVideoThumbnailsFromReq, setVideoTags } from '@server/lib/video'
8import { generateVideoFilename, getVideoFilePath } from '@server/lib/video-paths' 8import { generateVideoFilename, getVideoFilePath } from '@server/lib/video-paths'
9import { docMiddleware } from '@server/middlewares/doc'
9import { MVideo, MVideoFile, MVideoFullLight } from '@server/types/models' 10import { MVideo, MVideoFile, MVideoFullLight } from '@server/types/models'
10import { uploadx } from '@uploadx/core' 11import { uploadx } from '@uploadx/core'
11import { VideoCreate, VideoState } from '../../../../shared' 12import { VideoCreate, VideoState } from '../../../../shared'
@@ -60,6 +61,7 @@ const reqVideoFileAddResumable = createReqFiles(
60) 61)
61 62
62uploadRouter.post('/upload', 63uploadRouter.post('/upload',
64 docMiddleware('https://docs.joinpeertube.org/api-rest-reference.html#operation/uploadLegacy'),
63 authenticate, 65 authenticate,
64 reqVideoFileAdd, 66 reqVideoFileAdd,
65 asyncMiddleware(videosAddLegacyValidator), 67 asyncMiddleware(videosAddLegacyValidator),
@@ -67,6 +69,7 @@ uploadRouter.post('/upload',
67) 69)
68 70
69uploadRouter.post('/upload-resumable', 71uploadRouter.post('/upload-resumable',
72 docMiddleware('https://docs.joinpeertube.org/api-rest-reference.html#operation/uploadResumableInit'),
70 authenticate, 73 authenticate,
71 reqVideoFileAddResumable, 74 reqVideoFileAddResumable,
72 asyncMiddleware(videosAddResumableInitValidator), 75 asyncMiddleware(videosAddResumableInitValidator),
@@ -79,6 +82,7 @@ uploadRouter.delete('/upload-resumable',
79) 82)
80 83
81uploadRouter.put('/upload-resumable', 84uploadRouter.put('/upload-resumable',
85 docMiddleware('https://docs.joinpeertube.org/api-rest-reference.html#operation/uploadResumable'),
82 authenticate, 86 authenticate,
83 uploadxMiddleware, // uploadx doesn't use call next() before the file upload completes 87 uploadxMiddleware, // uploadx doesn't use call next() before the file upload completes
84 asyncMiddleware(videosAddResumableValidator), 88 asyncMiddleware(videosAddResumableValidator),
diff --git a/server/helpers/express-utils.ts b/server/helpers/express-utils.ts
index 10a860787..010c6961a 100644
--- a/server/helpers/express-utils.ts
+++ b/server/helpers/express-utils.ts
@@ -8,7 +8,6 @@ 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'
12 11
13function buildNSFWFilter (res?: express.Response, paramNSFW?: string) { 12function buildNSFWFilter (res?: express.Response, paramNSFW?: string) {
14 if (paramNSFW === 'true') return true 13 if (paramNSFW === 'true') return true
@@ -126,34 +125,6 @@ function getCountVideos (req: express.Request) {
126 return req.query.skipCount !== true 125 return req.query.skipCount !== true
127} 126}
128 127
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 = HttpStatusCode.BAD_REQUEST_400, message, title, type, docs = res.docs, instance } = options
133
134 const extension = new ProblemDocumentExtension({
135 ...data,
136 docs,
137 // fields for <= 3.2 compatibility, deprecated
138 error: message,
139 code: type
140 })
141
142 res.status(status)
143 res.setHeader('Content-Type', 'application/problem+json')
144 res.json(new ProblemDocument({
145 status,
146 title,
147 instance,
148 // fields intended to replace 'error' and 'code' respectively
149 detail: message,
150 type: type && 'https://docs.joinpeertube.org/api-rest-reference.html#section/Errors/' + type
151 }, extension))
152 }
153
154 if (next) next()
155}
156
157// --------------------------------------------------------------------------- 128// ---------------------------------------------------------------------------
158 129
159export { 130export {
@@ -163,6 +134,5 @@ export {
163 badRequest, 134 badRequest,
164 createReqFiles, 135 createReqFiles,
165 cleanUpReqFiles, 136 cleanUpReqFiles,
166 getCountVideos, 137 getCountVideos
167 apiResponseHelpers
168} 138}
diff --git a/server/middlewares/doc.ts b/server/middlewares/doc.ts
new file mode 100644
index 000000000..aa852cd77
--- /dev/null
+++ b/server/middlewares/doc.ts
@@ -0,0 +1,13 @@
1import * as express from 'express'
2
3function docMiddleware (docUrl: string) {
4 return (req: express.Request, res: express.Response, next: express.NextFunction) => {
5 res.locals.docUrl = docUrl
6
7 if (next) return next()
8 }
9}
10
11export {
12 docMiddleware
13}
diff --git a/server/middlewares/error.ts b/server/middlewares/error.ts
new file mode 100644
index 000000000..e3eb1c8f5
--- /dev/null
+++ b/server/middlewares/error.ts
@@ -0,0 +1,39 @@
1import * as express from 'express'
2import { ProblemDocument, ProblemDocumentExtension } from 'http-problem-details'
3import { HttpStatusCode } from '@shared/core-utils'
4
5function apiFailMiddleware (req: express.Request, res: express.Response, next: express.NextFunction) {
6 res.fail = options => {
7 const { status = HttpStatusCode.BAD_REQUEST_400, message, title, type, data, instance } = options
8
9 const extension = new ProblemDocumentExtension({
10 ...data,
11
12 docs: res.locals.docUrl,
13 code: type,
14
15 // For <= 3.2 compatibility
16 error: message
17 })
18
19 res.status(status)
20 res.setHeader('Content-Type', 'application/problem+json')
21 res.json(new ProblemDocument({
22 status,
23 title,
24 instance,
25
26 detail: message,
27
28 type: type
29 ? `https://docs.joinpeertube.org/api-rest-reference.html#section/Errors/${type}`
30 : undefined
31 }, extension))
32 }
33
34 if (next) next()
35}
36
37export {
38 apiFailMiddleware
39}
diff --git a/server/middlewares/index.ts b/server/middlewares/index.ts
index 3e280e16f..413653dac 100644
--- a/server/middlewares/index.ts
+++ b/server/middlewares/index.ts
@@ -7,4 +7,6 @@ export * from './servers'
7export * from './sort' 7export * from './sort'
8export * from './user-right' 8export * from './user-right'
9export * from './dnt' 9export * from './dnt'
10export * from './error'
11export * from './doc'
10export * from './csp' 12export * from './csp'
diff --git a/server/middlewares/validators/videos/videos.ts b/server/middlewares/validators/videos/videos.ts
index 64e09234e..b7a9bcbe3 100644
--- a/server/middlewares/validators/videos/videos.ts
+++ b/server/middlewares/validators/videos/videos.ts
@@ -73,7 +73,6 @@ 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"
77 logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files }) 76 logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files })
78 77
79 if (areValidationErrors(req, res)) return cleanUpReqFiles(req) 78 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
@@ -108,7 +107,6 @@ const videosAddLegacyValidator = getCommonVideoEditAttributes().concat([
108 */ 107 */
109const videosAddResumableValidator = [ 108const videosAddResumableValidator = [
110 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 109 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
111 res.docs = "https://docs.joinpeertube.org/api-rest-reference.html#operation/uploadResumable"
112 const user = res.locals.oauth.token.User 110 const user = res.locals.oauth.token.User
113 111
114 const body: express.CustomUploadXFile<express.UploadXFileMetadata> = req.body 112 const body: express.CustomUploadXFile<express.UploadXFileMetadata> = req.body
@@ -170,7 +168,6 @@ const videosAddResumableInitValidator = getCommonVideoEditAttributes().concat([
170 .withMessage('Should specify the file mimetype'), 168 .withMessage('Should specify the file mimetype'),
171 169
172 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 170 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
173 res.docs = "https://docs.joinpeertube.org/api-rest-reference.html#operation/uploadResumableInit"
174 const videoFileMetadata = { 171 const videoFileMetadata = {
175 mimetype: req.headers['x-upload-content-type'] as string, 172 mimetype: req.headers['x-upload-content-type'] as string,
176 size: +req.headers['x-upload-content-length'], 173 size: +req.headers['x-upload-content-length'],
@@ -214,7 +211,6 @@ const videosUpdateValidator = getCommonVideoEditAttributes().concat([
214 .custom(isIdValid).withMessage('Should have correct video channel id'), 211 .custom(isIdValid).withMessage('Should have correct video channel id'),
215 212
216 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 213 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
217 res.docs = 'https://docs.joinpeertube.org/api-rest-reference.html#operation/putVideo'
218 logger.debug('Checking videosUpdate parameters', { parameters: req.body }) 214 logger.debug('Checking videosUpdate parameters', { parameters: req.body })
219 215
220 if (areValidationErrors(req, res)) return cleanUpReqFiles(req) 216 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
@@ -268,7 +264,6 @@ const videosCustomGetValidator = (
268 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), 264 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
269 265
270 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 266 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
271 res.docs = 'https://docs.joinpeertube.org/api-rest-reference.html#operation/getVideo'
272 logger.debug('Checking videosGet parameters', { parameters: req.params }) 267 logger.debug('Checking videosGet parameters', { parameters: req.params })
273 268
274 if (areValidationErrors(req, res)) return 269 if (areValidationErrors(req, res)) return
@@ -334,7 +329,6 @@ const videosRemoveValidator = [
334 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), 329 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
335 330
336 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 331 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
337 res.docs = "https://docs.joinpeertube.org/api-rest-reference.html#operation/delVideo"
338 logger.debug('Checking videosRemove parameters', { parameters: req.params }) 332 logger.debug('Checking videosRemove parameters', { parameters: req.params })
339 333
340 if (areValidationErrors(req, res)) return 334 if (areValidationErrors(req, res)) return
diff --git a/server/tests/api/check-params/videos.ts b/server/tests/api/check-params/videos.ts
index c970c4a15..a6eecb13a 100644
--- a/server/tests/api/check-params/videos.ts
+++ b/server/tests/api/check-params/videos.ts
@@ -4,6 +4,8 @@ import 'mocha'
4import * as chai from 'chai' 4import * as chai from 'chai'
5import { omit } from 'lodash' 5import { omit } from 'lodash'
6import { join } from 'path' 6import { join } from 'path'
7import { randomInt } from '@shared/core-utils'
8import { PeerTubeProblemDocument } from '@shared/models'
7import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' 9import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
8import { 10import {
9 checkUploadVideoParam, 11 checkUploadVideoParam,
@@ -30,7 +32,6 @@ import {
30 checkBadStartPagination 32 checkBadStartPagination
31} from '../../../../shared/extra-utils/requests/check-api-params' 33} from '../../../../shared/extra-utils/requests/check-api-params'
32import { VideoPrivacy } from '../../../../shared/models/videos/video-privacy.enum' 34import { VideoPrivacy } from '../../../../shared/models/videos/video-privacy.enum'
33import { randomInt } from '@shared/core-utils'
34 35
35const expect = chai.expect 36const expect = chai.expect
36 37
@@ -411,6 +412,31 @@ describe('Test videos API validator', function () {
411 await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode) 412 await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode)
412 }) 413 })
413 414
415 it('Should report the appropriate error', async function () {
416 const fields = immutableAssign(baseCorrectParams, { language: 'a'.repeat(15) })
417 const attaches = baseCorrectAttaches
418
419 const attributes = { ...fields, ...attaches }
420 const res = await checkUploadVideoParam(server.url, server.accessToken, attributes, HttpStatusCode.BAD_REQUEST_400, mode)
421
422 const error = res.body as PeerTubeProblemDocument
423
424 if (mode === 'legacy') {
425 expect(error.docs).to.equal('https://docs.joinpeertube.org/api-rest-reference.html#operation/uploadLegacy')
426 } else {
427 expect(error.docs).to.equal('https://docs.joinpeertube.org/api-rest-reference.html#operation/uploadResumableInit')
428 }
429
430 expect(error.type).to.equal('about:blank')
431 expect(error.title).to.equal('Bad Request')
432
433 expect(error.detail).to.equal('Incorrect request parameters: language')
434 expect(error.error).to.equal('Incorrect request parameters: language')
435
436 expect(error.status).to.equal(HttpStatusCode.BAD_REQUEST_400)
437 expect(error['invalid-params'].language).to.exist
438 })
439
414 it('Should succeed with the correct parameters', async function () { 440 it('Should succeed with the correct parameters', async function () {
415 this.timeout(10000) 441 this.timeout(10000)
416 442
@@ -645,6 +671,24 @@ describe('Test videos API validator', function () {
645 671
646 it('Should fail with a video of another server') 672 it('Should fail with a video of another server')
647 673
674 it('Shoud report the appropriate error', async function () {
675 const fields = immutableAssign(baseCorrectParams, { licence: 125 })
676
677 const res = await makePutBodyRequest({ url: server.url, path: path + videoId, token: server.accessToken, fields })
678 const error = res.body as PeerTubeProblemDocument
679
680 expect(error.docs).to.equal('https://docs.joinpeertube.org/api-rest-reference.html#operation/putVideo')
681
682 expect(error.type).to.equal('about:blank')
683 expect(error.title).to.equal('Bad Request')
684
685 expect(error.detail).to.equal('Incorrect request parameters: licence')
686 expect(error.error).to.equal('Incorrect request parameters: licence')
687
688 expect(error.status).to.equal(HttpStatusCode.BAD_REQUEST_400)
689 expect(error['invalid-params'].licence).to.exist
690 })
691
648 it('Should succeed with the correct parameters', async function () { 692 it('Should succeed with the correct parameters', async function () {
649 const fields = baseCorrectParams 693 const fields = baseCorrectParams
650 694
@@ -678,6 +722,22 @@ describe('Test videos API validator', function () {
678 await getVideo(server.url, '4da6fde3-88f7-4d16-b119-108df5630b06', HttpStatusCode.NOT_FOUND_404) 722 await getVideo(server.url, '4da6fde3-88f7-4d16-b119-108df5630b06', HttpStatusCode.NOT_FOUND_404)
679 }) 723 })
680 724
725 it('Shoud report the appropriate error', async function () {
726 const res = await getVideo(server.url, 'hi', HttpStatusCode.BAD_REQUEST_400)
727 const error = res.body as PeerTubeProblemDocument
728
729 expect(error.docs).to.equal('https://docs.joinpeertube.org/api-rest-reference.html#operation/getVideo')
730
731 expect(error.type).to.equal('about:blank')
732 expect(error.title).to.equal('Bad Request')
733
734 expect(error.detail).to.equal('Incorrect request parameters: id')
735 expect(error.error).to.equal('Incorrect request parameters: id')
736
737 expect(error.status).to.equal(HttpStatusCode.BAD_REQUEST_400)
738 expect(error['invalid-params'].id).to.exist
739 })
740
681 it('Should succeed with the correct parameters', async function () { 741 it('Should succeed with the correct parameters', async function () {
682 await getVideo(server.url, videoId) 742 await getVideo(server.url, videoId)
683 }) 743 })
@@ -755,6 +815,22 @@ describe('Test videos API validator', function () {
755 815
756 it('Should fail with a video of another server') 816 it('Should fail with a video of another server')
757 817
818 it('Shoud report the appropriate error', async function () {
819 const res = await removeVideo(server.url, server.accessToken, 'hello', HttpStatusCode.BAD_REQUEST_400)
820 const error = res.body as PeerTubeProblemDocument
821
822 expect(error.docs).to.equal('https://docs.joinpeertube.org/api-rest-reference.html#operation/delVideo')
823
824 expect(error.type).to.equal('about:blank')
825 expect(error.title).to.equal('Bad Request')
826
827 expect(error.detail).to.equal('Incorrect request parameters: id')
828 expect(error.error).to.equal('Incorrect request parameters: id')
829
830 expect(error.status).to.equal(HttpStatusCode.BAD_REQUEST_400)
831 expect(error['invalid-params'].id).to.exist
832 })
833
758 it('Should succeed with the correct parameters', async function () { 834 it('Should succeed with the correct parameters', async function () {
759 await removeVideo(server.url, server.accessToken, videoId) 835 await removeVideo(server.url, server.accessToken, videoId)
760 }) 836 })
diff --git a/server/tests/api/server/follow-constraints.ts b/server/tests/api/server/follow-constraints.ts
index 8a91fbba3..3f2f71f46 100644
--- a/server/tests/api/server/follow-constraints.ts
+++ b/server/tests/api/server/follow-constraints.ts
@@ -18,6 +18,7 @@ import { unfollow } from '../../../../shared/extra-utils/server/follows'
18import { userLogin } from '../../../../shared/extra-utils/users/login' 18import { userLogin } from '../../../../shared/extra-utils/users/login'
19import { createUser } from '../../../../shared/extra-utils/users/users' 19import { createUser } from '../../../../shared/extra-utils/users/users'
20import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' 20import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
21import { PeerTubeProblemDocument, ServerErrorCode } from '@shared/models'
21 22
22const expect = chai.expect 23const expect = chai.expect
23 24
@@ -153,7 +154,20 @@ describe('Test follow constraints', function () {
153 }) 154 })
154 155
155 it('Should not get the remote video', async function () { 156 it('Should not get the remote video', async function () {
156 await getVideo(servers[0].url, video2UUID, HttpStatusCode.FORBIDDEN_403) 157 const res = await getVideo(servers[0].url, video2UUID, HttpStatusCode.FORBIDDEN_403)
158
159 const error = res.body as PeerTubeProblemDocument
160
161 const doc = 'https://docs.joinpeertube.org/api-rest-reference.html#section/Errors/does_not_respect_follow_constraints'
162 expect(error.type).to.equal(doc)
163 expect(error.code).to.equal(ServerErrorCode.DOES_NOT_RESPECT_FOLLOW_CONSTRAINTS)
164
165 expect(error.detail).to.equal('Cannot get this video regarding follow constraints')
166 expect(error.error).to.equal(error.detail)
167
168 expect(error.status).to.equal(HttpStatusCode.FORBIDDEN_403)
169
170 expect(error.originUrl).to.contains(servers[1].url)
157 }) 171 })
158 172
159 it('Should list local account videos', async function () { 173 it('Should list local account videos', async function () {
diff --git a/server/typings/express/index.d.ts b/server/typings/express/index.d.ts
index f58436ce1..cbbf40a78 100644
--- a/server/typings/express/index.d.ts
+++ b/server/typings/express/index.d.ts
@@ -1,3 +1,4 @@
1
1import { RegisterServerAuthExternalOptions } from '@server/types' 2import { RegisterServerAuthExternalOptions } from '@server/types'
2import { 3import {
3 MAbuseMessage, 4 MAbuseMessage,
@@ -20,9 +21,8 @@ import { MVideoImportDefault } from '@server/types/models/video/video-import'
20import { MVideoPlaylistElement, MVideoPlaylistElementVideoUrlPlaylistPrivacy } from '@server/types/models/video/video-playlist-element' 21import { MVideoPlaylistElement, MVideoPlaylistElementVideoUrlPlaylistPrivacy } from '@server/types/models/video/video-playlist-element'
21import { MAccountVideoRateAccountVideo } from '@server/types/models/video/video-rate' 22import { MAccountVideoRateAccountVideo } from '@server/types/models/video/video-rate'
22import { HttpMethod } from '@shared/core-utils/miscs/http-methods' 23import { HttpMethod } from '@shared/core-utils/miscs/http-methods'
23import { VideoCreate } from '@shared/models' 24import { PeerTubeProblemDocumentData, ServerErrorCode, VideoCreate } from '@shared/models'
24import { File as UploadXFile, Metadata } from '@uploadx/core' 25import { File as UploadXFile, Metadata } from '@uploadx/core'
25import { ProblemDocumentOptions } from 'http-problem-details/dist/ProblemDocument'
26import { RegisteredPlugin } from '../../lib/plugins/plugin-manager' 26import { RegisteredPlugin } from '../../lib/plugins/plugin-manager'
27import { 27import {
28 MAccountDefault, 28 MAccountDefault,
@@ -41,6 +41,7 @@ import {
41 MVideoThumbnail, 41 MVideoThumbnail,
42 MVideoWithRights 42 MVideoWithRights
43} from '../../types/models' 43} from '../../types/models'
44
44declare module 'express' { 45declare module 'express' {
45 export interface Request { 46 export interface Request {
46 query: any 47 query: any
@@ -86,14 +87,20 @@ declare module 'express' {
86 87
87 // Extends Response with added functions and potential variables passed by middlewares 88 // Extends Response with added functions and potential variables passed by middlewares
88 interface Response { 89 interface Response {
89 docs?: string
90 fail: (options: { 90 fail: (options: {
91 data?: Record<string, Object>
92 docs?: string
93 message: string 91 message: string
94 } & ProblemDocumentOptions) => void 92
93 title?: string
94 status?: number
95 type?: ServerErrorCode
96 instance?: string
97
98 data?: PeerTubeProblemDocumentData
99 }) => void
95 100
96 locals: { 101 locals: {
102 docUrl?: string
103
97 videoAll?: MVideoFullLight 104 videoAll?: MVideoFullLight
98 onlyImmutableVideo?: MVideoImmutable 105 onlyImmutableVideo?: MVideoImmutable
99 onlyVideo?: MVideoThumbnail 106 onlyVideo?: MVideoThumbnail
diff --git a/shared/models/server/index.ts b/shared/models/server/index.ts
index b5163954a..06bf5c599 100644
--- a/shared/models/server/index.ts
+++ b/shared/models/server/index.ts
@@ -6,6 +6,7 @@ export * from './debug.model'
6export * from './emailer.model' 6export * from './emailer.model'
7export * from './job.model' 7export * from './job.model'
8export * from './log-level.type' 8export * from './log-level.type'
9export * from './peertube-problem-document.model'
9export * from './server-config.model' 10export * from './server-config.model'
10export * from './server-debug.model' 11export * from './server-debug.model'
11export * from './server-error-code.enum' 12export * from './server-error-code.enum'
diff --git a/shared/models/server/peertube-problem-document.model.ts b/shared/models/server/peertube-problem-document.model.ts
new file mode 100644
index 000000000..5e1c320f3
--- /dev/null
+++ b/shared/models/server/peertube-problem-document.model.ts
@@ -0,0 +1,32 @@
1import { HttpStatusCode } from '@shared/core-utils'
2import { OAuth2ErrorCode, ServerErrorCode } from './server-error-code.enum'
3
4export interface PeerTubeProblemDocumentData {
5 'invalid-params'?: Record<string, Object>
6
7 originUrl?: string
8
9 keyId?: string
10
11 targetUrl?: string
12
13 actorUrl?: string
14
15 // Feeds
16 format?: string
17 url?: string
18}
19
20export interface PeerTubeProblemDocument extends PeerTubeProblemDocumentData {
21 type: string
22 title: string
23
24 detail: string
25 // Compat PeerTube <= 3.2
26 error: string
27
28 status: HttpStatusCode
29
30 docs?: string
31 code?: ServerErrorCode | OAuth2ErrorCode
32}
diff --git a/shared/models/server/server-error-code.enum.ts b/shared/models/server/server-error-code.enum.ts
index 93b9ce20d..43996e7aa 100644
--- a/shared/models/server/server-error-code.enum.ts
+++ b/shared/models/server/server-error-code.enum.ts
@@ -48,5 +48,13 @@ export const enum OAuth2ErrorCode {
48 * 48 *
49 * @see https://github.com/oauthjs/node-oauth2-server/blob/master/lib/errors/invalid-client-error.js 49 * @see https://github.com/oauthjs/node-oauth2-server/blob/master/lib/errors/invalid-client-error.js
50 */ 50 */
51 INVALID_CLIENT = 'invalid_client' 51 INVALID_CLIENT = 'invalid_client',
52
53
54 /**
55 * The access token provided is expired, revoked, malformed, or invalid for other reasons
56 *
57 * @see https://github.com/oauthjs/node-oauth2-server/blob/master/lib/errors/invalid-token-error.js
58 */
59 INVALID_TOKEN = 'invalid_token',
52} 60}