From 7432a9af05b400b05d22e6f3efceb91d2187f106 Mon Sep 17 00:00:00 2001 From: amania-jailbreak Date: Wed, 5 Aug 2026 13:16:43 +0900 Subject: [PATCH] =?UTF-8?q?api:=20S3=E3=82=B9=E3=83=88=E3=83=AA=E3=83=BC?= =?UTF-8?q?=E3=83=9F=E3=83=B3=E3=82=B0=E3=82=A2=E3=83=83=E3=83=97=E3=83=AD?= =?UTF-8?q?=E3=83=BC=E3=83=89=E3=81=ABContentLength=E3=82=92=E6=B8=A1?= =?UTF-8?q?=E3=81=97500=E3=82=A8=E3=83=A9=E3=83=BC=E3=82=92=E8=A7=A3?= =?UTF-8?q?=E6=B6=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AWS SDK v3のflexible-checksumsはストリームBodyにContentLengthが 未指定だと x-amz-decoded-content-length: undefined を送信して ERR_HTTP_INVALID_HEADER_VALUE になる。expectedSizeを明示的に渡し、 writeStream側もsizeBytesを常に指定するよう修正した。 --- apps/api/src/server.ts | 2 +- apps/api/src/storage.ts | 14 +++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/apps/api/src/server.ts b/apps/api/src/server.ts index 7b18549..ad6b56a 100644 --- a/apps/api/src/server.ts +++ b/apps/api/src/server.ts @@ -180,7 +180,7 @@ export async function createServer() { const upload = await runtime.store.getUpload(uploadId); if (!upload || upload.objectKey !== key) return reply.code(404).send({ error: "UPLOAD_NOT_FOUND" }); const body = request.body as NodeJS.ReadableStream; - const receivedSize = await runtime.storage.writeUploadFromStream(key, body); + const receivedSize = await runtime.storage.writeUploadFromStream(key, body, upload.expectedSize); if (receivedSize !== upload.expectedSize) return reply.code(400).send({ error: "UPLOAD_SIZE_MISMATCH", expectedSize: upload.expectedSize, receivedSize }); await runtime.store.updateUpload(upload.id, { receivedSize, status: "uploaded" }); return { ok: true, receivedSize }; diff --git a/apps/api/src/storage.ts b/apps/api/src/storage.ts index 8048f1d..28a7ed7 100644 --- a/apps/api/src/storage.ts +++ b/apps/api/src/storage.ts @@ -27,7 +27,7 @@ export type UploadPlan = export interface StorageAdapter { createUploadPlan(key: string, contentType: string, sizeBytes: number): Promise; completeMultipartUpload(key: string, uploadId: string | undefined, parts: Array<{ partNumber: number; etag: string }> | undefined): Promise; - writeUploadFromStream(key: string, stream: NodeJS.ReadableStream): Promise; + writeUploadFromStream(key: string, stream: NodeJS.ReadableStream, sizeBytes?: number): Promise; writeObject(key: string, body: Buffer, contentType: string): Promise; writeStream(key: string, transform: NodeJS.ReadWriteStream, contentType: string, sizeBytes: number, source: NodeJS.ReadableStream): Promise; downloadToFile(key: string, filePath: string): Promise; @@ -66,7 +66,8 @@ export class LocalStorage implements StorageAdapter { return; } - async writeUploadFromStream(key: string, stream: NodeJS.ReadableStream) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars -- sizeBytes is only needed by S3. + async writeUploadFromStream(key: string, stream: NodeJS.ReadableStream, sizeBytes?: number) { const filePath = this.pathFor(key); await mkdir(dirname(filePath), { recursive: true }); await pipeline(stream, createWriteStream(filePath, { flags: "w" })); @@ -158,8 +159,11 @@ export class S3Storage implements StorageAdapter { await this.client.send(new CompleteMultipartUploadCommand({ Bucket: this.appConfig.S3_BUCKET, Key: key, UploadId: uploadId, MultipartUpload: { Parts: parts.sort((a, b) => a.partNumber - b.partNumber).map((part) => ({ PartNumber: part.partNumber, ETag: part.etag })) } })); } - async writeUploadFromStream(key: string, stream: NodeJS.ReadableStream) { - await this.client.send(new PutObjectCommand({ Bucket: this.appConfig.S3_BUCKET, Key: key, Body: stream as any })); + async writeUploadFromStream(key: string, stream: NodeJS.ReadableStream, sizeBytes?: number) { + // ContentLength must be provided for streaming bodies, otherwise the SDK's + // flexible-checksums middleware sends `x-amz-decoded-content-length: + // undefined` and the upload fails with ERR_HTTP_INVALID_HEADER_VALUE. + await this.client.send(new PutObjectCommand({ Bucket: this.appConfig.S3_BUCKET, Key: key, Body: stream as any, ContentLength: sizeBytes })); const metadata = await this.headObject(key); return metadata?.sizeBytes || 0; } @@ -169,7 +173,7 @@ export class S3Storage implements StorageAdapter { } async writeStream(key: string, transform: NodeJS.ReadWriteStream, contentType: string, sizeBytes: number, source: NodeJS.ReadableStream) { - const upload = this.client.send(new PutObjectCommand({ Bucket: this.appConfig.S3_BUCKET, Key: key, Body: transform as any, ContentType: contentType, ContentLength: sizeBytes || undefined })); + const upload = this.client.send(new PutObjectCommand({ Bucket: this.appConfig.S3_BUCKET, Key: key, Body: transform as any, ContentType: contentType, ContentLength: sizeBytes })); await pipeline(source, transform); await upload; }