From d4132d3f56b392a2e4e632db59e6644e4851228e Mon Sep 17 00:00:00 2001 From: Rigel Kent Date: Mon, 23 Nov 2020 10:45:42 +0100 Subject: [PATCH] more explicit error messages for file uploads --- .../my-account-settings.component.ts | 8 +- .../video-upload.component.html | 19 +- .../video-upload.component.scss | 12 +- .../video-upload.component.ts | 57 ++- .../app/core/notification/notifier.service.ts | 20 +- client/src/app/helpers/utils.ts | 33 +- .../shared/shared-main/video/video.service.ts | 6 +- client/src/sass/bootstrap.scss | 5 + client/src/sass/include/_mixins.scss | 4 + server/controllers/api/videos/index.ts | 4 +- shared/core-utils/miscs/http-error-codes.ts | 419 ++++++++++++++++++ shared/core-utils/miscs/index.ts | 1 + support/doc/api/openapi.yaml | 16 + support/nginx/peertube | 25 +- 14 files changed, 574 insertions(+), 55 deletions(-) create mode 100644 shared/core-utils/miscs/http-error-codes.ts diff --git a/client/src/app/+my-account/my-account-settings/my-account-settings.component.ts b/client/src/app/+my-account/my-account-settings/my-account-settings.component.ts index 7ea4610d4..d5d019b35 100644 --- a/client/src/app/+my-account/my-account-settings/my-account-settings.component.ts +++ b/client/src/app/+my-account/my-account-settings/my-account-settings.component.ts @@ -1,6 +1,8 @@ import { ViewportScroller } from '@angular/common' +import { HttpErrorResponse } from '@angular/common/http' import { AfterViewChecked, Component, OnInit } from '@angular/core' import { AuthService, Notifier, User, UserService } from '@app/core' +import { uploadErrorHandler } from '@app/helpers' @Component({ selector: 'my-account-settings', @@ -44,7 +46,11 @@ export class MyAccountSettingsComponent implements OnInit, AfterViewChecked { this.user.updateAccountAvatar(data.avatar) }, - err => this.notifier.error(err.message) + (err: HttpErrorResponse) => uploadErrorHandler({ + err, + name: $localize`avatar`, + notifier: this.notifier + }) ) } } diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.html b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.html index 677fa1197..88ee4e32a 100644 --- a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.html +++ b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.html @@ -44,17 +44,30 @@ +
-
+
Processing… {{ videoUploadPercents }}%
- +
-
+
+
+
+ {{ error }} +
+
+
+ + +
+
+ +
Sorry, but something went wrong
{{ error }}
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.scss b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.scss index 9ebfa2f2f..9549257f6 100644 --- a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.scss +++ b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.scss @@ -16,27 +16,27 @@ } } +.upload-progress-retry, .upload-progress-cancel { display: flex; - margin-top: 25px; margin-bottom: 40px; .progress { @include progressbar; flex-grow: 1; height: 30px; - font-size: 15px; + font-size: 14px; background-color: rgba(11, 204, 41, 0.16); .progress-bar { background-color: $green; line-height: 30px; text-align: left; - font-weight: $font-bold; + font-weight: $font-semibold; span { color: pvar(--mainBackgroundColor); - margin-left: 18px; + margin-left: 13px; } } } @@ -47,4 +47,8 @@ margin-left: 10px; } + + .btn-group > input:not(:first-child) { + margin-left: 0; + } } diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts index 258f5c7a0..fdd0a56e5 100644 --- a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts +++ b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts @@ -1,9 +1,9 @@ import { Subscription } from 'rxjs' -import { HttpEventType, HttpResponse } from '@angular/common/http' +import { HttpErrorResponse, HttpEventType, HttpResponse } from '@angular/common/http' import { Component, ElementRef, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core' import { Router } from '@angular/router' import { AuthService, CanComponentDeactivate, Notifier, ServerService, UserService } from '@app/core' -import { scrollToTop } from '@app/helpers' +import { scrollToTop, uploadErrorHandler } from '@app/helpers' import { FormValidatorService } from '@app/shared/shared-forms' import { BytesPipe, VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main' import { LoadingBarService } from '@ngx-loading-bar/core' @@ -41,11 +41,13 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy id: 0, uuid: '' } + formData: FormData waitTranscodingEnabled = true previewfileUpload: File error: string + enableRetryAfterError: boolean protected readonly DEFAULT_VIDEO_PRIVACY = VideoPrivacy.PUBLIC @@ -118,6 +120,12 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy this.uploadFirstStep() } + retryUpload () { + this.enableRetryAfterError = false + this.error = '' + this.uploadVideo() + } + cancelUpload () { if (this.videoUploadObservable !== null) { this.videoUploadObservable.unsubscribe() @@ -127,6 +135,8 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy this.videoUploadObservable = null this.firstStepError.emit() + this.enableRetryAfterError = false + this.error = '' this.notifier.info($localize`Upload cancelled`) } @@ -163,20 +173,20 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy const downloadEnabled = true const channelId = this.firstStepChannelId.toString() - const formData = new FormData() - formData.append('name', name) + this.formData = new FormData() + this.formData.append('name', name) // Put the video "private" -> we are waiting the user validation of the second step - formData.append('privacy', VideoPrivacy.PRIVATE.toString()) - formData.append('nsfw', '' + nsfw) - formData.append('commentsEnabled', '' + commentsEnabled) - formData.append('downloadEnabled', '' + downloadEnabled) - formData.append('waitTranscoding', '' + waitTranscoding) - formData.append('channelId', '' + channelId) - formData.append('videofile', videofile) + this.formData.append('privacy', VideoPrivacy.PRIVATE.toString()) + this.formData.append('nsfw', '' + nsfw) + this.formData.append('commentsEnabled', '' + commentsEnabled) + this.formData.append('downloadEnabled', '' + downloadEnabled) + this.formData.append('waitTranscoding', '' + waitTranscoding) + this.formData.append('channelId', '' + channelId) + this.formData.append('videofile', videofile) if (this.previewfileUpload) { - formData.append('previewfile', this.previewfileUpload) - formData.append('thumbnailfile', this.previewfileUpload) + this.formData.append('previewfile', this.previewfileUpload) + this.formData.append('thumbnailfile', this.previewfileUpload) } this.isUploadingVideo = true @@ -190,7 +200,11 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy previewfile: this.previewfileUpload }) - this.videoUploadObservable = this.videoService.uploadVideo(formData).subscribe( + this.uploadVideo() + } + + uploadVideo () { + this.videoUploadObservable = this.videoService.uploadVideo(this.formData).subscribe( event => { if (event.type === HttpEventType.UploadProgress) { this.videoUploadPercents = Math.round(100 * event.loaded / event.total) @@ -203,13 +217,18 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy } }, - err => { - // Reset progress - this.isUploadingVideo = false + (err: HttpErrorResponse) => { + // Reset progress (but keep isUploadingVideo true) this.videoUploadPercents = 0 this.videoUploadObservable = null - this.firstStepError.emit() - this.notifier.error(err.message) + this.enableRetryAfterError = true + + this.error = uploadErrorHandler({ + err, + name: $localize`video`, + notifier: this.notifier, + sticky: false + }) } ) } diff --git a/client/src/app/core/notification/notifier.service.ts b/client/src/app/core/notification/notifier.service.ts index f736672bb..165bb0c76 100644 --- a/client/src/app/core/notification/notifier.service.ts +++ b/client/src/app/core/notification/notifier.service.ts @@ -7,31 +7,35 @@ export class Notifier { constructor (private messageService: MessageService) { } - info (text: string, title?: string, timeout?: number) { + info (text: string, title?: string, timeout?: number, sticky?: boolean) { if (!title) title = $localize`Info` - return this.notify('info', text, title, timeout) + console.info(`${title}: ${text}`) + return this.notify('info', text, title, timeout, sticky) } - error (text: string, title?: string, timeout?: number) { + error (text: string, title?: string, timeout?: number, sticky?: boolean) { if (!title) title = $localize`Error` - return this.notify('error', text, title, timeout) + console.error(`${title}: ${text}`) + return this.notify('error', text, title, timeout, sticky) } - success (text: string, title?: string, timeout?: number) { + success (text: string, title?: string, timeout?: number, sticky?: boolean) { if (!title) title = $localize`Success` - return this.notify('success', text, title, timeout) + console.log(`${title}: ${text}`) + return this.notify('success', text, title, timeout, sticky) } - private notify (severity: 'success' | 'info' | 'warn' | 'error', text: string, title: string, timeout?: number) { + private notify (severity: 'success' | 'info' | 'warn' | 'error', text: string, title: string, timeout?: number, sticky?: boolean) { this.messageService.add({ severity, summary: title, detail: text, closable: true, - life: timeout || this.TIMEOUT + life: timeout || this.TIMEOUT, + sticky }) } } diff --git a/client/src/app/helpers/utils.ts b/client/src/app/helpers/utils.ts index 9c805b4ca..f96f26fff 100644 --- a/client/src/app/helpers/utils.ts +++ b/client/src/app/helpers/utils.ts @@ -1,7 +1,10 @@ import { DatePipe } from '@angular/common' +import { HttpErrorResponse } from '@angular/common/http' +import { Notifier } from '@app/core' import { SelectChannelItem } from '@app/shared/shared-forms' import { environment } from '../../environments/environment' import { AuthService } from '../core/auth' +import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' // Thanks: https://stackoverflow.com/questions/901115/how-can-i-get-query-string-values-in-javascript function getParameterByName (name: string, url: string) { @@ -172,6 +175,33 @@ function isXPercentInViewport (el: HTMLElement, percentVisible: number) { ) } +function uploadErrorHandler (parameters: { + err: HttpErrorResponse + name: string + notifier: Notifier + sticky?: boolean +}) { + const { err, name, notifier, sticky } = { sticky: false, ...parameters } + const title = $localize`The upload failed` + let message = err.message + + if (err instanceof ErrorEvent) { // network error + message = $localize`The connection was interrupted` + notifier.error(message, title, null, sticky) + } else if (err.status === HttpStatusCode.REQUEST_TIMEOUT_408) { + message = $localize`Your ${name} file couldn't be transferred before the set timeout (usually 10min)` + notifier.error(message, title, null, sticky) + } else if (err.status === HttpStatusCode.PAYLOAD_TOO_LARGE_413) { + const maxFileSize = err.headers?.get('X-File-Maximum-Size') || '8G' + message = $localize`Your ${name} file was too large (max. size: ${maxFileSize})` + notifier.error(message, title, null, sticky) + } else { + notifier.error(err.message, title) + } + + return message +} + export { sortBy, durationToString, @@ -187,5 +217,6 @@ export { removeElementFromArray, scrollToTop, isInViewport, - isXPercentInViewport + isXPercentInViewport, + uploadErrorHandler } diff --git a/client/src/app/shared/shared-main/video/video.service.ts b/client/src/app/shared/shared-main/video/video.service.ts index 70be5d7d2..59860c5cb 100644 --- a/client/src/app/shared/shared-main/video/video.service.ts +++ b/client/src/app/shared/shared-main/video/video.service.ts @@ -1,6 +1,6 @@ -import { Observable } from 'rxjs' -import { catchError, map, switchMap } from 'rxjs/operators' -import { HttpClient, HttpParams, HttpRequest } from '@angular/common/http' +import { Observable, of, throwError } from 'rxjs' +import { catchError, map, mergeMap, switchMap } from 'rxjs/operators' +import { HttpClient, HttpErrorResponse, HttpParams, HttpRequest } from '@angular/common/http' import { Injectable } from '@angular/core' import { ComponentPaginationLight, RestExtractor, RestService, ServerService, UserService, AuthService } from '@app/core' import { objectToFormData } from '@app/helpers' diff --git a/client/src/sass/bootstrap.scss b/client/src/sass/bootstrap.scss index b90bffbfc..208c7f582 100644 --- a/client/src/sass/bootstrap.scss +++ b/client/src/sass/bootstrap.scss @@ -44,6 +44,11 @@ $icon-font-path: '~@neos21/bootstrap3-glyphicons/assets/fonts/'; z-index: inherit !important; } +.btn-group > .btn:not(:first-child) { + border-top-left-radius: 0 !important; + border-bottom-left-radius: 0 !important; +} + .dropdown-menu { z-index: z(dropdown) + 1 !important; diff --git a/client/src/sass/include/_mixins.scss b/client/src/sass/include/_mixins.scss index 1a94de5b2..fecae9fbc 100644 --- a/client/src/sass/include/_mixins.scss +++ b/client/src/sass/include/_mixins.scss @@ -732,6 +732,10 @@ &.secondary { background-color: pvar(--secondaryColor); } + + &.red { + background-color: lighten($color: #c54130, $amount: 10); + } } } diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts index e8480d749..0dcd38ad2 100644 --- a/server/controllers/api/videos/index.ts +++ b/server/controllers/api/videos/index.ts @@ -174,8 +174,8 @@ function listVideoPrivacies (req: express.Request, res: express.Response) { } async function addVideo (req: express.Request, res: express.Response) { - // Processing the video could be long - // Set timeout to 10 minutes + // Transferring the video could be long + // Set timeout to 10 minutes, as Express's default is 2 minutes req.setTimeout(1000 * 60 * 10, () => { logger.error('Upload video has timed out.') return res.sendStatus(408) diff --git a/shared/core-utils/miscs/http-error-codes.ts b/shared/core-utils/miscs/http-error-codes.ts new file mode 100644 index 000000000..6dfe73c2e --- /dev/null +++ b/shared/core-utils/miscs/http-error-codes.ts @@ -0,0 +1,419 @@ +/** + * Hypertext Transfer Protocol (HTTP) response status codes. + * @see {@link https://en.wikipedia.org/wiki/List_of_HTTP_status_codes} + */ +export enum HttpStatusCode { + + /** + * Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.2.1 + * + * The server has received the request headers and the client should proceed to send the request body + * (in the case of a request for which a body needs to be sent; for example, a POST request). + * Sending a large request body to a server after a request has been rejected for inappropriate headers would be inefficient. + * To have a server check the request's headers, a client must send Expect: 100-continue as a header in its initial request + * and receive a 100 Continue status code in response before sending the body. The response 417 Expectation Failed indicates + * the request should not be continued. + */ + CONTINUE_100 = 100, + + /** + * Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.2.2 + * + * This code is sent in response to an Upgrade request header by the client, and indicates the protocol the server is switching too. + */ + SWITCHING_PROTOCOLS_101 = 101, + + /** + * Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.3.1 + * + * Standard response for successful HTTP requests. The actual response will depend on the request method used: + * GET: The resource has been fetched and is transmitted in the message body. + * HEAD: The entity headers are in the message body. + * POST: The resource describing the result of the action is transmitted in the message body. + * TRACE: The message body contains the request message as received by the server + */ + OK_200 = 200, + + /** + * Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.3.2 + * + * The request has been fulfilled, resulting in the creation of a new resource, typically after a PUT. + */ + CREATED_201 = 201, + + /** + * The request has been accepted for processing, but the processing has not been completed. + * The request might or might not be eventually acted upon, and may be disallowed when processing occurs. + */ + ACCEPTED_202 = 202, + + /** + * Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.3.4 + * + * SINCE HTTP/1.1 + * The server is a transforming proxy that received a 200 OK from its origin, + * but is returning a modified version of the origin's response. + */ + NON_AUTHORITATIVE_INFORMATION_203 = 203, + + /** + * Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.3.5 + * + * There is no content to send for this request, but the headers may be useful. + * The user-agent may update its cached headers for this resource with the new ones. + */ + NO_CONTENT_204 = 204, + + /** + * The server successfully processed the request, but is not returning any content. + * Unlike a 204 response, this response requires that the requester reset the document view. + */ + RESET_CONTENT_205 = 205, + + /** + * The server is delivering only part of the resource (byte serving) due to a range header sent by the client. + * The range header is used by HTTP clients to enable resuming of interrupted downloads, + * or split a download into multiple simultaneous streams. + */ + PARTIAL_CONTENT_206 = 206, + + /** + * The message body that follows is an XML message and can contain a number of separate response codes, + * depending on how many sub-requests were made. + */ + MULTI_STATUS_207 = 207, + + /** + * The server has fulfilled a request for the resource, + * and the response is a representation of the result of one or more instance-manipulations applied to the current instance. + */ + IM_USED_226 = 226, + + /** + * Indicates multiple options for the resource from which the client may choose (via agent-driven content negotiation). + * For example, this code could be used to present multiple video format options, + * to list files with different filename extensions, or to suggest word-sense disambiguation. + */ + MULTIPLE_CHOICES_300 = 300, + + /** + * This and all future requests should be directed to the given URI. + */ + MOVED_PERMANENTLY_301 = 301, + + /** + * This is an example of industry practice contradicting the standard. + * The HTTP/1.0 specification (RFC 1945) required the client to perform a temporary redirect + * (the original describing phrase was "Moved Temporarily"), but popular browsers implemented 302 + * with the functionality of a 303 See Other. Therefore, HTTP/1.1 added status codes 303 and 307 + * to distinguish between the two behaviours. However, some Web applications and frameworks + * use the 302 status code as if it were the 303. + */ + FOUND_302 = 302, + + /** + * SINCE HTTP/1.1 + * The response to the request can be found under another URI using a GET method. + * When received in response to a POST (or PUT/DELETE), the client should presume that + * the server has received the data and should issue a redirect with a separate GET message. + */ + SEE_OTHER_303 = 303, + + /** + * Official Documentation @ https://tools.ietf.org/html/rfc7232#section-4.1 + * + * Indicates that the resource has not been modified since the version specified by the request headers + * `If-Modified-Since` or `If-None-Match`. + * In such case, there is no need to retransmit the resource since the client still has a previously-downloaded copy. + */ + NOT_MODIFIED_304 = 304, + + /** + * @deprecated + * SINCE HTTP/1.1 + * The requested resource is available only through a proxy, the address for which is provided in the response. + * Many HTTP clients (such as Mozilla and Internet Explorer) do not correctly handle responses with this status + * code, primarily for security reasons. + */ + USE_PROXY_305 = 305, + + /** + * No longer used. Originally meant "Subsequent requests should use the specified proxy." + */ + SWITCH_PROXY_306 = 306, + + /** + * SINCE HTTP/1.1 + * In this case, the request should be repeated with another URI; however, future requests should still use the original URI. + * In contrast to how 302 was historically implemented, the request method is not allowed to be changed when reissuing the + * original request. + * For example, a POST request should be repeated using another POST request. + */ + TEMPORARY_REDIRECT_307 = 307, + + /** + * The request and all future requests should be repeated using another URI. + * 307 and 308 parallel the behaviors of 302 and 301, but do not allow the HTTP method to change. + * So, for example, submitting a form to a permanently redirected resource may continue smoothly. + */ + PERMANENT_REDIRECT_308 = 308, + + /** + * The server cannot or will not process the request due to an apparent client error + * (e.g., malformed request syntax, too large size, invalid request message framing, or deceptive request routing). + */ + BAD_REQUEST_400 = 400, + + /** + * Official Documentation @ https://tools.ietf.org/html/rfc7235#section-3.1 + * + * Similar to 403 Forbidden, but specifically for use when authentication is required and has failed or has not yet + * been provided. The response must include a `WWW-Authenticate` header field containing a challenge applicable to the + * requested resource. See Basic access authentication and Digest access authentication. 401 semantically means + * "unauthenticated",i.e. the user does not have the necessary credentials. + */ + UNAUTHORIZED_401 = 401, + + /** + * Reserved for future use. The original intention was that this code might be used as part of some form of digital + * cash or micro payment scheme, but that has not happened, and this code is not usually used. + * Google Developers API uses this status if a particular developer has exceeded the daily limit on requests. + */ + PAYMENT_REQUIRED_402 = 402, + + /** + * Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.5.3 + * + * The client does not have access rights to the content, i.e. they are unauthorized, so server is rejecting to + * give proper response. Unlike 401, the client's identity is known to the server. + */ + FORBIDDEN_403 = 403, + + /** + * Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.6.2 + * + * The requested resource could not be found but may be available in the future. + * Subsequent requests by the client are permissible. + */ + NOT_FOUND_404 = 404, + + /** + * Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.5.5 + * + * A request method is not supported for the requested resource; + * for example, a GET request on a form that requires data to be presented via POST, or a PUT request on a read-only resource. + */ + METHOD_NOT_ALLOWED_405 = 405, + + /** + * The requested resource is capable of generating only content not acceptable according to the Accept headers sent in the request. + */ + NOT_ACCEPTABLE_406 = 406, + + /** + * The client must first authenticate itself with the proxy. + */ + PROXY_AUTHENTICATION_REQUIRED_407 = 407, + + /** + * Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.5.7 + * + * This response is sent on an idle connection by some servers, even without any previous request by the client. + * It means that the server would like to shut down this unused connection. This response is used much more since + * some browsers, like Chrome, Firefox 27+, or IE9, use HTTP pre-connection mechanisms to speed up surfing. Also + * note that some servers merely shut down the connection without sending this message. + */ + REQUEST_TIMEOUT_408 = 408, + + /** + * Indicates that the request could not be processed because of conflict in the request, + * such as an edit conflict between multiple simultaneous updates. + */ + CONFLICT_409 = 409, + + /** + * Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.5.9 + * + * Indicates that the resource requested is no longer available and will not be available again. + * This should be used when a resource has been intentionally removed and the resource should be purged. + * Upon receiving a 410 status code, the client should not request the resource in the future. + * Clients such as search engines should remove the resource from their indices. + * Most use cases do not require clients and search engines to purge the resource, and a "404 Not Found" may be used instead. + */ + GONE_410 = 410, + + /** + * The request did not specify the length of its content, which is required by the requested resource. + */ + LENGTH_REQUIRED_411 = 411, + + /** + * The server does not meet one of the preconditions that the requester put on the request. + */ + PRECONDITION_FAILED_412 = 412, + + /** + * Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.5.11 + * + * The request is larger than the server is willing or able to process ; the server might close the connection + * or return an Retry-After header field. + * Previously called "Request Entity Too Large". + */ + PAYLOAD_TOO_LARGE_413 = 413, + + /** + * The URI provided was too long for the server to process. Often the result of too much data being encoded as a + * query-string of a GET request, in which case it should be converted to a POST request. + * Called "Request-URI Too Long" previously. + */ + URI_TOO_LONG_414 = 414, + + /** + * Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.5.13 + * + * The request entity has a media type which the server or resource does not support. + * For example, the client uploads an image as image/svg+xml, but the server requires that images use a different format. + */ + UNSUPPORTED_MEDIA_TYPE_415 = 415, + + /** + * The client has asked for a portion of the file (byte serving), but the server cannot supply that portion. + * For example, if the client asked for a part of the file that lies beyond the end of the file. + * Called "Requested Range Not Satisfiable" previously. + */ + RANGE_NOT_SATISFIABLE_416 = 416, + + /** + * The server cannot meet the requirements of the Expect request-header field. + */ + EXPECTATION_FAILED_417 = 417, + + /** + * This code was defined in 1998 as one of the traditional IETF April Fools' jokes, in RFC 2324, Hyper Text Coffee Pot Control Protocol, + * and is not expected to be implemented by actual HTTP servers. The RFC specifies this code should be returned by + * teapots requested to brew coffee. This HTTP status is used as an Easter egg in some websites, including Google.com. + */ + I_AM_A_TEAPOT_418 = 418, + + /** + * The request was directed at a server that is not able to produce a response (for example because a connection reuse). + */ + MISDIRECTED_REQUEST_421 = 421, + + /** + * Official Documentation @ https://tools.ietf.org/html/rfc2518#section-10.3 + * + * The request was well-formed but was unable to be followed due to semantic errors. + */ + UNPROCESSABLE_ENTITY_422 = 422, + + /** + * The resource that is being accessed is locked. + */ + LOCKED_423 = 423, + + /** + * The request failed due to failure of a previous request (e.g., a PROPPATCH). + */ + FAILED_DEPENDENCY_424 = 424, + + /** + * The client should switch to a different protocol such as TLS/1.0, given in the Upgrade header field. + */ + UPGRADE_REQUIRED_426 = 426, + + /** + * The origin server requires the request to be conditional. + * Intended to prevent "the 'lost update' problem, where a client + * GETs a resource's state, modifies it, and PUTs it back to the server, + * when meanwhile a third party has modified the state on the server, leading to a conflict." + */ + PRECONDITION_REQUIRED_428 = 428, + + /** + * Official Documentation @ https://tools.ietf.org/html/rfc6585#section-4 + * + * The user has sent too many requests in a given amount of time. Intended for use with rate-limiting schemes. + */ + TOO_MANY_REQUESTS_429 = 429, + + /** + * Official Documentation @ https://tools.ietf.org/html/rfc6585#section-5 + * + * The server is unwilling to process the request because either an individual header field, + * or all the header fields collectively, are too large. + */ + REQUEST_HEADER_FIELDS_TOO_LARGE_431 = 431, + + /** + * Official Documentation @ https://tools.ietf.org/html/rfc7725 + * + * A server operator has received a legal demand to deny access to a resource or to a set of resources + * that includes the requested resource. The code 451 was chosen as a reference to the novel Fahrenheit 451. + */ + UNAVAILABLE_FOR_LEGAL_REASONS_451 = 451, + + /** + * A generic error message, given when an unexpected condition was encountered and no more specific message is suitable. + */ + INTERNAL_SERVER_ERROR_500 = 500, + + /** + * The server either does not recognize the request method, or it lacks the ability to fulfill the request. + * Usually this implies future availability (e.g., a new feature of a web-service API). + */ + NOT_IMPLEMENTED_501 = 501, + + /** + * The server was acting as a gateway or proxy and received an invalid response from the upstream server. + */ + BAD_GATEWAY_502 = 502, + + /** + * The server is currently unavailable (because it is overloaded or down for maintenance). + * Generally, this is a temporary state. + */ + SERVICE_UNAVAILABLE_503 = 503, + + /** + * The server was acting as a gateway or proxy and did not receive a timely response from the upstream server. + */ + GATEWAY_TIMEOUT_504 = 504, + + /** + * The server does not support the HTTP protocol version used in the request + */ + HTTP_VERSION_NOT_SUPPORTED_505 = 505, + + /** + * Transparent content negotiation for the request results in a circular reference. + */ + VARIANT_ALSO_NEGOTIATES_506 = 506, + + /** + * Official Documentation @ https://tools.ietf.org/html/rfc2518#section-10.6 + * + * The 507 (Insufficient Storage) status code means the method could not be performed on the resource because the + * server is unable to store the representation needed to successfully complete the request. This condition is + * considered to be temporary. If the request which received this status code was the result of a user action, + * the request MUST NOT be repeated until it is requested by a separate user action. + */ + INSUFFICIENT_STORAGE_507 = 507, + + /** + * The server detected an infinite loop while processing the request. + */ + LOOP_DETECTED_508 = 508, + + /** + * Further extensions to the request are required for the server to fulfill it. + */ + NOT_EXTENDED_510 = 510, + + /** + * The client needs to authenticate to gain network access. + * Intended for use by intercepting proxies used to control access to the network (e.g., "captive portals" used + * to require agreement to Terms of Service before granting full Internet access via a Wi-Fi hotspot). + */ + NETWORK_AUTHENTICATION_REQUIRED_511 = 511 +} diff --git a/shared/core-utils/miscs/index.ts b/shared/core-utils/miscs/index.ts index afd147f24..898fd4791 100644 --- a/shared/core-utils/miscs/index.ts +++ b/shared/core-utils/miscs/index.ts @@ -1,3 +1,4 @@ export * from './date' export * from './miscs' export * from './types' +export * from './http-error-codes' diff --git a/support/doc/api/openapi.yaml b/support/doc/api/openapi.yaml index 6dd51ec7c..4f9bca729 100644 --- a/support/doc/api/openapi.yaml +++ b/support/doc/api/openapi.yaml @@ -972,6 +972,14 @@ paths: application/json: schema: $ref: '#/components/schemas/Avatar' + '413': + description: image file too large + headers: + X-File-Maximum-Size: + schema: + type: string + format: Nginx size + description: Maximum file size for the avatar requestBody: content: multipart/form-data: @@ -1308,6 +1316,14 @@ paths: description: user video quota is exceeded with this video '408': description: upload has timed out + '413': + description: video file too large + headers: + X-File-Maximum-Size: + schema: + type: string + format: Nginx size + description: Maximum file size for the video '422': description: invalid input file requestBody: diff --git a/support/nginx/peertube b/support/nginx/peertube index f1ef4ccd1..641d254af 100644 --- a/support/nginx/peertube +++ b/support/nginx/peertube @@ -62,9 +62,9 @@ server { ## location @api { - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; client_max_body_size 100k; # default is 1M @@ -81,26 +81,23 @@ server { } location = /api/v1/users/me/avatar/pick { - limit_except POST { deny all; } + limit_except POST HEAD { deny all; } - client_max_body_size 2M; # default is 1M + client_max_body_size 2M; # default is 1M + add_header X-File-Maximum-Size 2M always; # inform backend of the set value in bytes try_files /dev/null @api; } location = /api/v1/videos/upload { - limit_except POST { deny all; } - - # This is the maximum upload size, which roughly matches the maximum size of a video file - # you can send via the API or the web interface. By default we set it to 8GB, but administrators - # can increase or decrease the limit. Currently there's no way to communicate this limit - # to users automatically, so you may want to leave a note in your instance 'about' page if - # you change this. - # + limit_except POST HEAD { deny all; } + + # This is the maximum upload size, which roughly matches the maximum size of a video file. # Note that temporary space is needed equal to the total size of all concurrent uploads. # This data gets stored in /var/lib/nginx by default, so you may want to put this directory # on a dedicated filesystem. - client_max_body_size 8G; # default is 1M + client_max_body_size 8G; # default is 1M + add_header X-File-Maximum-Size 8G always; # inform backend of the set value in bytes try_files /dev/null @api; } -- 2.41.0