0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Lambdaでmultipart/form-dataを扱う実装方法|AI実務ノート 編集部

Last updated at Posted at 2026-01-03

1. 結論(この記事で得られること)

この記事を読むと、以下が手に入ります。

  • AWS LambdaでAPIGateway経由のmultipart/form-dataを確実に処理する実装パターン
  • busboy / multiparty など Node.js パーサーライブラリの使い分け基準
  • バイナリデータが壊れる原因と、API Gateway の設定ポイント(これ、意外と見落とします)
  • AI を使って問題切り分けを 10 分で終わらせるプロンプト例
  • 実務で必要なテスト観点(メモリ・タイムアウト・ファイルサイズ上限)

正直、私も最初に Lambda で画像アップロード機能を実装したとき、「バイナリが壊れてるんだけど…?」って 3 時間ハマりました。API Gateway の 「binaryMediaTypes」 設定を知らなかったんです。この記事では、そういう地雷を全部まとめて回避します。

2. 前提(環境・読者層)

想定読者

  • Lambda + API Gateway でファイルアップロード API を作る必要がある方
  • multipart/form-data の仕様は知っているが、Lambda 特有の罠を避けたい方
  • Python や他の言語経験者で Node.js の Lambda に初挑戦する方

検証環境

  • Node.js 18.x / 20.x(Lambda ランタイム)
  • API Gateway REST API または HTTP API
  • SAM / Serverless Framework / CDK いずれでも OK(記事では SAM のテンプレート例を提示)

3. Before:よくあるつまずきポイント

3-1. 「「event.body」 がただの文字列で、どうパースすれば…」

Lambda の event オブジェクトには 「event.body」 という文字列(または Base64 エンコード済み文字列)が入ってきます。通常の JSON なら 「JSON.parse()」 で終わりですが、multipart/form-data は boundary で区切られた複雑な構造。手でパースするのは現実的じゃない。

// ❌ こんな感じで来る
{
  "body": "------WebKitFormBoundary...",
  "isBase64Encoded": false,
  "headers": {
    "content-type": "multipart/form-data; boundary=----WebKitFormBoundary..."
  }
}

3-2. 「画像ファイルが壊れる(バイナリ化けする)」

API Gateway がデフォルトでは バイナリを想定していない ため、「isBase64Encoded: false」 で来たバイナリが UTF-8 文字列として扱われて壊れます。

  • 画像ファイルをアップロードしても、S3 に保存した画像が開けない
  • 「Content-Length」 がずれる
  • 文字コード変換で一部バイトが失われる

3-3. 「ローカルでは動くのに Lambda だとエラー」

Express や Koa のミドルウェア(multer など)は、ストリームやファイルシステムへの一時保存を前提にしています。Lambda の 「/tmp」 は容量制限があり、大きなファイルだと溢れます。

4. After:基本的な解決パターン

4-1. API Gateway の設定(必須)

REST API の場合

SAM テンプレートで 「BinaryMediaTypes」 を設定:

Resources:
  MyApi:
    Type: AWS::Serverless::Api
    Properties:
      StageName: prod
      BinaryMediaTypes:
        - "multipart/form-data"
        - "image/*"
        - "application/octet-stream"

これで、該当する Content-Type のリクエストは Base64 エンコードされて 「event.body」 に入り、「isBase64Encoded: true」 になります。

HTTP API の場合

HTTP API はデフォルトでバイナリを扱えますが、ペイロードフォーマットを 「2.0」 にしておくと楽です。

4-2. Node.js でのパース(busboy を使う最小実装)

const Busboy = require('busboy');
 
exports.handler = async (event) => {
  // Base64 デコード
  const body = event.isBase64Encoded
    ? Buffer.from(event.body, 'base64')
    : Buffer.from(event.body);
 
  const busboy = Busboy({
    headers: {
      'content-type': event.headers['content-type'] || event.headers['Content-Type']
    }
  });
 
  const fields = {};
  const files = [];
 
  return new Promise((resolve, reject) => {
    busboy.on('file', (fieldname, file, info) => {
      const { filename, encoding, mimeType } = info;
      const chunks = [];
 
      file.on('data', (data) => chunks.push(data));
      file.on('end', () => {
        files.push({
          fieldname,
          filename,
          mimeType,
          buffer: Buffer.concat(chunks)
        });
      });
    });
 
    busboy.on('field', (fieldname, value) => {
      fields[fieldname] = value;
    });
 
    busboy.on('finish', () => {
      resolve({
        statusCode: 200,
        body: JSON.stringify({
          fields,
          files: files.map(f => ({
            name: f.filename,
            size: f.buffer.length,
            type: f.mimeType
          }))
        })
      });
    });
 
    busboy.on('error', reject);
    busboy.write(body);
    busboy.end();
  });
};

ポイント

  • 「busboy.write()」 で Buffer を流し込む(ストリームとして扱える)
  • ファイル本体は 「chunks」 配列に溜めて、最後に 「Buffer.concat()」 で結合
  • 「/tmp」 には書かない → メモリで完結(小〜中サイズファイル向け)
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?