import { VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main'
import { LiveVideoService } from '@app/shared/shared-video-live'
import { LoadingBarService } from '@ngx-loading-bar/core'
-import { LiveVideo, LiveVideoCreate, LiveVideoUpdate, ServerErrorCode, VideoPrivacy } from '@shared/models'
+import { LiveVideo, LiveVideoCreate, LiveVideoUpdate, PeerTubeProblemDocument, ServerErrorCode, VideoPrivacy } from '@shared/models'
import { VideoSend } from './video-send'
@Component({
let message = err.message
- if (err.body?.code === ServerErrorCode.MAX_INSTANCE_LIVES_LIMIT_REACHED) {
+ const error = err.body as PeerTubeProblemDocument
+
+ if (error?.code === ServerErrorCode.MAX_INSTANCE_LIVES_LIMIT_REACHED) {
message = $localize`Cannot create live because this instance have too many created lives`
- } else if (err.body?.code) {
+ } else if (error?.code === ServerErrorCode.MAX_USER_LIVES_LIMIT_REACHED) {
message = $localize`Cannot create live because you created too many lives`
}
import { FormValidatorService } from '@app/shared/shared-forms'
import { VideoCaptionService, VideoEdit, VideoImportService, VideoService } from '@app/shared/shared-main'
import { LoadingBarService } from '@ngx-loading-bar/core'
-import { ServerErrorCode, VideoPrivacy, VideoUpdate } from '@shared/models'
+import { PeerTubeProblemDocument, ServerErrorCode, VideoPrivacy, VideoUpdate } from '@shared/models'
import { hydrateFormFromVideo } from '../shared/video-edit-utils'
import { VideoSend } from './video-send'
this.firstStepError.emit()
let message = err.message
- if (err.body?.code === ServerErrorCode.INCORRECT_FILES_IN_TORRENT) {
+
+ const error = err.body as PeerTubeProblemDocument
+ if (error?.code === ServerErrorCode.INCORRECT_FILES_IN_TORRENT) {
message = $localize`Torrents with only 1 file are supported.`
}
import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist'
import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage'
import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
-import { ServerConfig, ServerErrorCode, UserVideoRateType, VideoCaption, VideoPrivacy, VideoState } from '@shared/models'
+import { PeerTubeProblemDocument, ServerConfig, ServerErrorCode, UserVideoRateType, VideoCaption, VideoPrivacy, VideoState } from '@shared/models'
import {
cleanupVideoWatch,
getStoredP2PEnabled,
.pipe(
// If 400, 403 or 404, the video is private or blocked so redirect to 404
catchError(err => {
- if (err.body.type === ServerErrorCode.DOES_NOT_RESPECT_FOLLOW_CONSTRAINTS && err.body.originUrl) {
+ const errorBody = err.body as PeerTubeProblemDocument
+
+ if (errorBody.code === ServerErrorCode.DOES_NOT_RESPECT_FOLLOW_CONSTRAINTS && errorBody.originUrl) {
const search = window.location.search
- let originUrl = err.body.originUrl
+ let originUrl = errorBody.originUrl
if (search) originUrl += search
this.confirmService.confirm(
import { AuthService } from '@app/core/auth/auth.service'
import { Router } from '@angular/router'
import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
+import { OAuth2ErrorCode, PeerTubeProblemDocument, ServerErrorCode } from '@shared/models/server'
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
return next.handle(authReq)
.pipe(
catchError((err: HttpErrorResponse) => {
- if (err.status === HttpStatusCode.UNAUTHORIZED_401 && err.error && err.error.code === 'invalid_token') {
+ const error = err.error as PeerTubeProblemDocument
+
+ if (err.status === HttpStatusCode.UNAUTHORIZED_401 && error && error.code === OAuth2ErrorCode.INVALID_TOKEN) {
return this.handleTokenExpired(req, next)
}
import {
ClientHookName,
HTMLServerConfig,
+ OAuth2ErrorCode,
PluginType,
ResultList,
UserRefreshToken,
if (res.status === HttpStatusCode.UNAUTHORIZED_401) return undefined
return res.json()
- }).then((obj: UserRefreshToken & { code: 'invalid_grant'}) => {
- if (!obj || obj.code === 'invalid_grant') {
+ }).then((obj: UserRefreshToken & { code?: OAuth2ErrorCode }) => {
+ if (!obj || obj.code === OAuth2ErrorCode.INVALID_GRANT) {
Tokens.flush()
this.removeTokensFromHeaders()
downloadRouter
} from './server/controllers'
import { advertiseDoNotTrack } from './server/middlewares/dnt'
+import { apiFailMiddleware } from './server/middlewares/error'
import { Redis } from './server/lib/redis'
import { ActorFollowScheduler } from './server/lib/schedulers/actor-follow-scheduler'
import { RemoveOldViewsScheduler } from './server/lib/schedulers/remove-old-views-scheduler'
import { HttpStatusCode } from './shared/core-utils/miscs/http-error-codes'
import { VideosTorrentCache } from '@server/lib/files-cache/videos-torrent-cache'
import { ServerConfigManager } from '@server/lib/server-config-manager'
-import { apiResponseHelpers } from '@server/helpers/express-utils'
// ----------- Command line -----------
skip: req => CONFIG.LOG.LOG_PING_REQUESTS === false && req.originalUrl === '/api/v1/ping'
}))
-// Response helpers used for errors
-app.use(apiResponseHelpers)
+// Add .fail() helper to response
+app.use(apiFailMiddleware)
// For body requests
app.use(express.urlencoded({ extended: false }))
limit: '500kb',
verify: (req: express.Request, res: express.Response, buf: Buffer) => {
const valid = isHTTPSignatureDigestValid(buf, req)
+
if (valid !== true) {
res.fail({
status: HttpStatusCode.FORBIDDEN_403,
import toInt from 'validator/lib/toInt'
import { doJSONRequest } from '@server/helpers/requests'
import { LiveManager } from '@server/lib/live-manager'
+import { docMiddleware } from '@server/middlewares/doc'
import { getServerActor } from '@server/models/application/application'
import { MVideoAccountLight } from '@server/types/models'
import { VideosCommonQuery } from '../../../../shared'
asyncMiddleware(getVideoFileMetadata)
)
videosRouter.get('/:id',
+ docMiddleware('https://docs.joinpeertube.org/api-rest-reference.html#operation/getVideo'),
optionalAuthenticate,
asyncMiddleware(videosCustomGetValidator('only-video-with-rights')),
asyncMiddleware(checkVideoFollowConstraints),
)
videosRouter.delete('/:id',
+ docMiddleware('https://docs.joinpeertube.org/api-rest-reference.html#operation/delVideo'),
authenticate,
asyncMiddleware(videosRemoveValidator),
asyncRetryTransactionMiddleware(removeVideo)
import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videosUpdateValidator } from '../../../middlewares'
import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update'
import { VideoModel } from '../../../models/video/video'
+import { docMiddleware } from '@server/middlewares/doc'
const lTags = loggerTagsFactory('api', 'video')
const auditLogger = auditLoggerFactory('videos')
)
updateRouter.put('/:id',
+ docMiddleware('https://docs.joinpeertube.org/api-rest-reference.html#operation/putVideo'),
authenticate,
reqVideoFileUpdate,
asyncMiddleware(videosUpdateValidator),
import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url'
import { addOptimizeOrMergeAudioJob, buildLocalVideoFromReq, buildVideoThumbnailsFromReq, setVideoTags } from '@server/lib/video'
import { generateVideoFilename, getVideoFilePath } from '@server/lib/video-paths'
+import { docMiddleware } from '@server/middlewares/doc'
import { MVideo, MVideoFile, MVideoFullLight } from '@server/types/models'
import { uploadx } from '@uploadx/core'
import { VideoCreate, VideoState } from '../../../../shared'
)
uploadRouter.post('/upload',
+ docMiddleware('https://docs.joinpeertube.org/api-rest-reference.html#operation/uploadLegacy'),
authenticate,
reqVideoFileAdd,
asyncMiddleware(videosAddLegacyValidator),
)
uploadRouter.post('/upload-resumable',
+ docMiddleware('https://docs.joinpeertube.org/api-rest-reference.html#operation/uploadResumableInit'),
authenticate,
reqVideoFileAddResumable,
asyncMiddleware(videosAddResumableInitValidator),
)
uploadRouter.put('/upload-resumable',
+ docMiddleware('https://docs.joinpeertube.org/api-rest-reference.html#operation/uploadResumable'),
authenticate,
uploadxMiddleware, // uploadx doesn't use call next() before the file upload completes
asyncMiddleware(videosAddResumableValidator),
import { logger } from './logger'
import { deleteFileAndCatch, generateRandomString } from './utils'
import { getExtFromMimetype } from './video'
-import { ProblemDocument, ProblemDocumentExtension } from 'http-problem-details'
function buildNSFWFilter (res?: express.Response, paramNSFW?: string) {
if (paramNSFW === 'true') return true
return req.query.skipCount !== true
}
-// helpers added in server.ts and used in subsequent controllers used
-const apiResponseHelpers = (req, res: express.Response, next = null) => {
- res.fail = (options) => {
- const { data, status = HttpStatusCode.BAD_REQUEST_400, message, title, type, docs = res.docs, instance } = options
-
- const extension = new ProblemDocumentExtension({
- ...data,
- docs,
- // fields for <= 3.2 compatibility, deprecated
- error: message,
- code: type
- })
-
- res.status(status)
- res.setHeader('Content-Type', 'application/problem+json')
- res.json(new ProblemDocument({
- status,
- title,
- instance,
- // fields intended to replace 'error' and 'code' respectively
- detail: message,
- type: type && 'https://docs.joinpeertube.org/api-rest-reference.html#section/Errors/' + type
- }, extension))
- }
-
- if (next) next()
-}
-
// ---------------------------------------------------------------------------
export {
badRequest,
createReqFiles,
cleanUpReqFiles,
- getCountVideos,
- apiResponseHelpers
+ getCountVideos
}
--- /dev/null
+import * as express from 'express'
+
+function docMiddleware (docUrl: string) {
+ return (req: express.Request, res: express.Response, next: express.NextFunction) => {
+ res.locals.docUrl = docUrl
+
+ if (next) return next()
+ }
+}
+
+export {
+ docMiddleware
+}
--- /dev/null
+import * as express from 'express'
+import { ProblemDocument, ProblemDocumentExtension } from 'http-problem-details'
+import { HttpStatusCode } from '@shared/core-utils'
+
+function apiFailMiddleware (req: express.Request, res: express.Response, next: express.NextFunction) {
+ res.fail = options => {
+ const { status = HttpStatusCode.BAD_REQUEST_400, message, title, type, data, instance } = options
+
+ const extension = new ProblemDocumentExtension({
+ ...data,
+
+ docs: res.locals.docUrl,
+ code: type,
+
+ // For <= 3.2 compatibility
+ error: message
+ })
+
+ res.status(status)
+ res.setHeader('Content-Type', 'application/problem+json')
+ res.json(new ProblemDocument({
+ status,
+ title,
+ instance,
+
+ detail: message,
+
+ type: type
+ ? `https://docs.joinpeertube.org/api-rest-reference.html#section/Errors/${type}`
+ : undefined
+ }, extension))
+ }
+
+ if (next) next()
+}
+
+export {
+ apiFailMiddleware
+}
export * from './sort'
export * from './user-right'
export * from './dnt'
+export * from './error'
+export * from './doc'
export * from './csp'
.custom(isIdValid).withMessage('Should have correct video channel id'),
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
- res.docs = "https://docs.joinpeertube.org/api-rest-reference.html#operation/uploadLegacy"
logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files })
if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
*/
const videosAddResumableValidator = [
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
- res.docs = "https://docs.joinpeertube.org/api-rest-reference.html#operation/uploadResumable"
const user = res.locals.oauth.token.User
const body: express.CustomUploadXFile<express.UploadXFileMetadata> = req.body
.withMessage('Should specify the file mimetype'),
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
- res.docs = "https://docs.joinpeertube.org/api-rest-reference.html#operation/uploadResumableInit"
const videoFileMetadata = {
mimetype: req.headers['x-upload-content-type'] as string,
size: +req.headers['x-upload-content-length'],
.custom(isIdValid).withMessage('Should have correct video channel id'),
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
- res.docs = 'https://docs.joinpeertube.org/api-rest-reference.html#operation/putVideo'
logger.debug('Checking videosUpdate parameters', { parameters: req.body })
if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
- res.docs = 'https://docs.joinpeertube.org/api-rest-reference.html#operation/getVideo'
logger.debug('Checking videosGet parameters', { parameters: req.params })
if (areValidationErrors(req, res)) return
param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
- res.docs = "https://docs.joinpeertube.org/api-rest-reference.html#operation/delVideo"
logger.debug('Checking videosRemove parameters', { parameters: req.params })
if (areValidationErrors(req, res)) return
import * as chai from 'chai'
import { omit } from 'lodash'
import { join } from 'path'
+import { randomInt } from '@shared/core-utils'
+import { PeerTubeProblemDocument } from '@shared/models'
import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
import {
checkUploadVideoParam,
checkBadStartPagination
} from '../../../../shared/extra-utils/requests/check-api-params'
import { VideoPrivacy } from '../../../../shared/models/videos/video-privacy.enum'
-import { randomInt } from '@shared/core-utils'
const expect = chai.expect
await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode)
})
+ it('Should report the appropriate error', async function () {
+ const fields = immutableAssign(baseCorrectParams, { language: 'a'.repeat(15) })
+ const attaches = baseCorrectAttaches
+
+ const attributes = { ...fields, ...attaches }
+ const res = await checkUploadVideoParam(server.url, server.accessToken, attributes, HttpStatusCode.BAD_REQUEST_400, mode)
+
+ const error = res.body as PeerTubeProblemDocument
+
+ if (mode === 'legacy') {
+ expect(error.docs).to.equal('https://docs.joinpeertube.org/api-rest-reference.html#operation/uploadLegacy')
+ } else {
+ expect(error.docs).to.equal('https://docs.joinpeertube.org/api-rest-reference.html#operation/uploadResumableInit')
+ }
+
+ expect(error.type).to.equal('about:blank')
+ expect(error.title).to.equal('Bad Request')
+
+ expect(error.detail).to.equal('Incorrect request parameters: language')
+ expect(error.error).to.equal('Incorrect request parameters: language')
+
+ expect(error.status).to.equal(HttpStatusCode.BAD_REQUEST_400)
+ expect(error['invalid-params'].language).to.exist
+ })
+
it('Should succeed with the correct parameters', async function () {
this.timeout(10000)
it('Should fail with a video of another server')
+ it('Shoud report the appropriate error', async function () {
+ const fields = immutableAssign(baseCorrectParams, { licence: 125 })
+
+ const res = await makePutBodyRequest({ url: server.url, path: path + videoId, token: server.accessToken, fields })
+ const error = res.body as PeerTubeProblemDocument
+
+ expect(error.docs).to.equal('https://docs.joinpeertube.org/api-rest-reference.html#operation/putVideo')
+
+ expect(error.type).to.equal('about:blank')
+ expect(error.title).to.equal('Bad Request')
+
+ expect(error.detail).to.equal('Incorrect request parameters: licence')
+ expect(error.error).to.equal('Incorrect request parameters: licence')
+
+ expect(error.status).to.equal(HttpStatusCode.BAD_REQUEST_400)
+ expect(error['invalid-params'].licence).to.exist
+ })
+
it('Should succeed with the correct parameters', async function () {
const fields = baseCorrectParams
await getVideo(server.url, '4da6fde3-88f7-4d16-b119-108df5630b06', HttpStatusCode.NOT_FOUND_404)
})
+ it('Shoud report the appropriate error', async function () {
+ const res = await getVideo(server.url, 'hi', HttpStatusCode.BAD_REQUEST_400)
+ const error = res.body as PeerTubeProblemDocument
+
+ expect(error.docs).to.equal('https://docs.joinpeertube.org/api-rest-reference.html#operation/getVideo')
+
+ expect(error.type).to.equal('about:blank')
+ expect(error.title).to.equal('Bad Request')
+
+ expect(error.detail).to.equal('Incorrect request parameters: id')
+ expect(error.error).to.equal('Incorrect request parameters: id')
+
+ expect(error.status).to.equal(HttpStatusCode.BAD_REQUEST_400)
+ expect(error['invalid-params'].id).to.exist
+ })
+
it('Should succeed with the correct parameters', async function () {
await getVideo(server.url, videoId)
})
it('Should fail with a video of another server')
+ it('Shoud report the appropriate error', async function () {
+ const res = await removeVideo(server.url, server.accessToken, 'hello', HttpStatusCode.BAD_REQUEST_400)
+ const error = res.body as PeerTubeProblemDocument
+
+ expect(error.docs).to.equal('https://docs.joinpeertube.org/api-rest-reference.html#operation/delVideo')
+
+ expect(error.type).to.equal('about:blank')
+ expect(error.title).to.equal('Bad Request')
+
+ expect(error.detail).to.equal('Incorrect request parameters: id')
+ expect(error.error).to.equal('Incorrect request parameters: id')
+
+ expect(error.status).to.equal(HttpStatusCode.BAD_REQUEST_400)
+ expect(error['invalid-params'].id).to.exist
+ })
+
it('Should succeed with the correct parameters', async function () {
await removeVideo(server.url, server.accessToken, videoId)
})
import { userLogin } from '../../../../shared/extra-utils/users/login'
import { createUser } from '../../../../shared/extra-utils/users/users'
import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
+import { PeerTubeProblemDocument, ServerErrorCode } from '@shared/models'
const expect = chai.expect
})
it('Should not get the remote video', async function () {
- await getVideo(servers[0].url, video2UUID, HttpStatusCode.FORBIDDEN_403)
+ const res = await getVideo(servers[0].url, video2UUID, HttpStatusCode.FORBIDDEN_403)
+
+ const error = res.body as PeerTubeProblemDocument
+
+ const doc = 'https://docs.joinpeertube.org/api-rest-reference.html#section/Errors/does_not_respect_follow_constraints'
+ expect(error.type).to.equal(doc)
+ expect(error.code).to.equal(ServerErrorCode.DOES_NOT_RESPECT_FOLLOW_CONSTRAINTS)
+
+ expect(error.detail).to.equal('Cannot get this video regarding follow constraints')
+ expect(error.error).to.equal(error.detail)
+
+ expect(error.status).to.equal(HttpStatusCode.FORBIDDEN_403)
+
+ expect(error.originUrl).to.contains(servers[1].url)
})
it('Should list local account videos', async function () {
+
import { RegisterServerAuthExternalOptions } from '@server/types'
import {
MAbuseMessage,
import { MVideoPlaylistElement, MVideoPlaylistElementVideoUrlPlaylistPrivacy } from '@server/types/models/video/video-playlist-element'
import { MAccountVideoRateAccountVideo } from '@server/types/models/video/video-rate'
import { HttpMethod } from '@shared/core-utils/miscs/http-methods'
-import { VideoCreate } from '@shared/models'
+import { PeerTubeProblemDocumentData, ServerErrorCode, VideoCreate } from '@shared/models'
import { File as UploadXFile, Metadata } from '@uploadx/core'
-import { ProblemDocumentOptions } from 'http-problem-details/dist/ProblemDocument'
import { RegisteredPlugin } from '../../lib/plugins/plugin-manager'
import {
MAccountDefault,
MVideoThumbnail,
MVideoWithRights
} from '../../types/models'
+
declare module 'express' {
export interface Request {
query: any
// Extends Response with added functions and potential variables passed by middlewares
interface Response {
- docs?: string
fail: (options: {
- data?: Record<string, Object>
- docs?: string
message: string
- } & ProblemDocumentOptions) => void
+
+ title?: string
+ status?: number
+ type?: ServerErrorCode
+ instance?: string
+
+ data?: PeerTubeProblemDocumentData
+ }) => void
locals: {
+ docUrl?: string
+
videoAll?: MVideoFullLight
onlyImmutableVideo?: MVideoImmutable
onlyVideo?: MVideoThumbnail
export * from './emailer.model'
export * from './job.model'
export * from './log-level.type'
+export * from './peertube-problem-document.model'
export * from './server-config.model'
export * from './server-debug.model'
export * from './server-error-code.enum'
--- /dev/null
+import { HttpStatusCode } from '@shared/core-utils'
+import { OAuth2ErrorCode, ServerErrorCode } from './server-error-code.enum'
+
+export interface PeerTubeProblemDocumentData {
+ 'invalid-params'?: Record<string, Object>
+
+ originUrl?: string
+
+ keyId?: string
+
+ targetUrl?: string
+
+ actorUrl?: string
+
+ // Feeds
+ format?: string
+ url?: string
+}
+
+export interface PeerTubeProblemDocument extends PeerTubeProblemDocumentData {
+ type: string
+ title: string
+
+ detail: string
+ // Compat PeerTube <= 3.2
+ error: string
+
+ status: HttpStatusCode
+
+ docs?: string
+ code?: ServerErrorCode | OAuth2ErrorCode
+}
*
* @see https://github.com/oauthjs/node-oauth2-server/blob/master/lib/errors/invalid-client-error.js
*/
- INVALID_CLIENT = 'invalid_client'
+ INVALID_CLIENT = 'invalid_client',
+
+
+ /**
+ * The access token provided is expired, revoked, malformed, or invalid for other reasons
+ *
+ * @see https://github.com/oauthjs/node-oauth2-server/blob/master/lib/errors/invalid-token-error.js
+ */
+ INVALID_TOKEN = 'invalid_token',
}