From b7b4483a33b10b8ca2fb6e956e24b18bd1c0a9ef Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Sat, 9 Dec 2023 21:46:56 -0500 Subject: [PATCH] fix(server): connection aborted logging (#5595) --- server/src/domain/domain.util.ts | 2 ++ .../src/immich/api-v1/asset/asset.service.ts | 27 ++++++++++--------- .../immich/interceptors/error.interceptor.ts | 5 +++- 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/server/src/domain/domain.util.ts b/server/src/domain/domain.util.ts index 1b1c0a3e27..890ea3a8b5 100644 --- a/server/src/domain/domain.util.ts +++ b/server/src/domain/domain.util.ts @@ -21,6 +21,8 @@ export type Options = { each?: boolean; }; +export const isConnectionAborted = (error: Error | any) => error.code === 'ECONNABORTED'; + export function ValidateUUID({ optional, each }: Options = { optional: false, each: false }) { return applyDecorators( IsUUID('4', { each }), diff --git a/server/src/immich/api-v1/asset/asset.service.ts b/server/src/immich/api-v1/asset/asset.service.ts index cbe63e5577..f21c75befd 100644 --- a/server/src/immich/api-v1/asset/asset.service.ts +++ b/server/src/immich/api-v1/asset/asset.service.ts @@ -6,6 +6,7 @@ import { IAccessRepository, IJobRepository, ILibraryRepository, + isConnectionAborted, JobName, mapAsset, mimeTypes, @@ -20,6 +21,7 @@ import { constants } from 'fs'; import fs from 'fs/promises'; import path from 'path'; import { QueryFailedError } from 'typeorm'; +import { promisify } from 'util'; import { IAssetRepository } from './asset-repository'; import { AssetCore } from './asset.core'; import { AssetBulkUploadCheckDto } from './dto/asset-check.dto'; @@ -42,6 +44,10 @@ import { CuratedObjectsResponseDto } from './response-dto/curated-objects-respon type SendFile = Parameters; type SendFileOptions = SendFile[1]; +// TODO: move file sending logic to an interceptor +const sendFile = (res: Response, path: string, options: SendFileOptions) => + promisify(res.sendFile).bind(res)(path, options); + @Injectable() export class AssetService { readonly logger = new Logger(AssetService.name); @@ -336,19 +342,16 @@ export class AssetService { res.set('Cache-Control', 'private, max-age=86400, no-transform'); res.header('Content-Type', mimeTypes.lookup(filepath)); - return new Promise((resolve, reject) => { - res.sendFile(filepath, options, (error: Error) => { - if (!error) { - resolve(); - return; - } - if (error.message !== 'Request aborted') { - this.logger.error(`Unable to send file: ${error.name}`, error.stack); - } - reject(error); - }); - }); + try { + await sendFile(res, filepath, options); + } catch (error: Error | any) { + if (!isConnectionAborted(error)) { + this.logger.error(`Unable to send file: ${error.name}`, error.stack); + } + // throwing closes the connection and prevents `Error: write EPIPE` + throw error; + } } private async getLibraryId(authUser: AuthUserDto, libraryId?: string) { diff --git a/server/src/immich/interceptors/error.interceptor.ts b/server/src/immich/interceptors/error.interceptor.ts index 5e18684fd7..9ccdabd72f 100644 --- a/server/src/immich/interceptors/error.interceptor.ts +++ b/server/src/immich/interceptors/error.interceptor.ts @@ -8,6 +8,7 @@ import { NestInterceptor, } from '@nestjs/common'; import { Observable, catchError, throwError } from 'rxjs'; +import { isConnectionAborted } from '../../domain'; import { routeToErrorMessage } from '../app.utils'; @Injectable() @@ -20,7 +21,9 @@ export class ErrorInterceptor implements NestInterceptor { throwError(() => { if (error instanceof HttpException === false) { const errorMessage = routeToErrorMessage(context.getHandler().name); - this.logger.error(errorMessage, error, error?.errors); + if (!isConnectionAborted(error)) { + this.logger.error(errorMessage, error, error?.errors); + } return new InternalServerErrorException(errorMessage); } else { return error;