Enhance S3 error handling in StorageService and update .env.example documentation
- Added detailed error handling for S3 operations in StorageService to interpret and throw appropriate exceptions based on the response. - Updated .env.example to clarify the S3 endpoint configuration for MinIO in Docker.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { DeleteObjectCommand, GetObjectCommand, PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
|
||||
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
||||
import { Injectable, ServiceUnavailableException } from '@nestjs/common';
|
||||
import { HttpException, Injectable, ServiceUnavailableException } from '@nestjs/common';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
export type StoredAttachmentMeta = {
|
||||
@@ -54,19 +54,27 @@ export class StorageService {
|
||||
|
||||
async putObject(key: string, body: Buffer, contentType: string): Promise<void> {
|
||||
this.assertConfigured();
|
||||
await this.client!.send(
|
||||
new PutObjectCommand({
|
||||
Bucket: this.bucket,
|
||||
Key: key,
|
||||
Body: body,
|
||||
ContentType: contentType || 'application/octet-stream',
|
||||
}),
|
||||
);
|
||||
try {
|
||||
await this.client!.send(
|
||||
new PutObjectCommand({
|
||||
Bucket: this.bucket,
|
||||
Key: key,
|
||||
Body: body,
|
||||
ContentType: contentType || 'application/octet-stream',
|
||||
}),
|
||||
);
|
||||
} catch (err) {
|
||||
throw interpretS3Failure('PutObject', err);
|
||||
}
|
||||
}
|
||||
|
||||
async deleteObject(key: string): Promise<void> {
|
||||
this.assertConfigured();
|
||||
await this.client!.send(new DeleteObjectCommand({ Bucket: this.bucket, Key: key }));
|
||||
try {
|
||||
await this.client!.send(new DeleteObjectCommand({ Bucket: this.bucket, Key: key }));
|
||||
} catch (err) {
|
||||
throw interpretS3Failure('DeleteObject', err);
|
||||
}
|
||||
}
|
||||
|
||||
async getDownloadUrl(objectKey: string): Promise<string> {
|
||||
@@ -80,8 +88,12 @@ export class StorageService {
|
||||
return `${base}/${path}`;
|
||||
}
|
||||
|
||||
const command = new GetObjectCommand({ Bucket: this.bucket, Key: objectKey });
|
||||
return getSignedUrl(this.client!, command, { expiresIn: this.presignExpiresSeconds });
|
||||
try {
|
||||
const command = new GetObjectCommand({ Bucket: this.bucket, Key: objectKey });
|
||||
return await getSignedUrl(this.client!, command, { expiresIn: this.presignExpiresSeconds });
|
||||
} catch (err) {
|
||||
throw interpretS3Failure('GetObject (presign)', err);
|
||||
}
|
||||
}
|
||||
|
||||
buildEquipmentObjectKey(equipmentId: string, originalName: string): string {
|
||||
@@ -95,3 +107,39 @@ function sanitizeFileName(name: string): string {
|
||||
const base = name.replace(/[/\\]/g, '_').trim() || 'file';
|
||||
return base.slice(0, 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* AWS SDK expects XML from S3-compatible APIs. A JSON/HTML 404 (e.g. wrong reverse-proxy URL)
|
||||
* surfaces as fast-xml-parser "char '{' is not expected".
|
||||
*/
|
||||
function interpretS3Failure(operation: string, err: unknown): never {
|
||||
if (err instanceof HttpException) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
const status = (err as { $metadata?: { httpStatusCode?: number } }).$metadata?.httpStatusCode;
|
||||
const code = (err as { Code?: string; name?: string }).Code ?? (err as { name?: string }).name;
|
||||
|
||||
const looksLikeNonXml =
|
||||
message.includes("char '{'") ||
|
||||
message.includes('Deserialization error') ||
|
||||
message.includes('fast-xml-parser');
|
||||
|
||||
if (looksLikeNonXml || status === 404) {
|
||||
throw new ServiceUnavailableException(
|
||||
`Хранилище S3/MinIO вернуло ответ не в формате S3 XML (${operation}, HTTP ${status ?? '?' }). ` +
|
||||
'Обычно это неверный S3_ENDPOINT: укажите API MinIO внутри Docker-сети, например ' +
|
||||
'`http://<имя-контейнера-minio>:9000` (порт S3 API), а не публичный HTTPS URL NPM, если он отдаёт JSON/HTML 404. ' +
|
||||
'Проверьте также S3_BUCKET и что контейнер toir-light-server в одной сети с MinIO.',
|
||||
);
|
||||
}
|
||||
|
||||
if (code === 'NoSuchBucket' || message.includes('NoSuchBucket')) {
|
||||
throw new ServiceUnavailableException(
|
||||
`Бакет «${process.env.S3_BUCKET}» не найден в MinIO (${operation}). Создайте бакет или поправьте S3_BUCKET.`,
|
||||
);
|
||||
}
|
||||
|
||||
throw new ServiceUnavailableException(`S3 ${operation}: ${message}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user