api: S3ストリーミングアップロードにContentLengthを渡し500エラーを解消

AWS SDK v3のflexible-checksumsはストリームBodyにContentLengthが
未指定だと x-amz-decoded-content-length: undefined を送信して
ERR_HTTP_INVALID_HEADER_VALUE になる。expectedSizeを明示的に渡し、
writeStream側もsizeBytesを常に指定するよう修正した。
This commit is contained in:
amania-jailbreak
2026-08-05 13:16:43 +09:00
parent 3bb4d5aa65
commit 7432a9af05
2 changed files with 10 additions and 6 deletions
+1 -1
View File
@@ -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 };
+9 -5
View File
@@ -27,7 +27,7 @@ export type UploadPlan =
export interface StorageAdapter {
createUploadPlan(key: string, contentType: string, sizeBytes: number): Promise<UploadPlan>;
completeMultipartUpload(key: string, uploadId: string | undefined, parts: Array<{ partNumber: number; etag: string }> | undefined): Promise<void>;
writeUploadFromStream(key: string, stream: NodeJS.ReadableStream): Promise<number>;
writeUploadFromStream(key: string, stream: NodeJS.ReadableStream, sizeBytes?: number): Promise<number>;
writeObject(key: string, body: Buffer, contentType: string): Promise<void>;
writeStream(key: string, transform: NodeJS.ReadWriteStream, contentType: string, sizeBytes: number, source: NodeJS.ReadableStream): Promise<void>;
downloadToFile(key: string, filePath: string): Promise<void>;
@@ -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;
}