NetlifyのFunctionsでファイルアップロードする方法|上限・Large Media・回避策
Netlify Functions(AWS Lambda ベースのサーバーレス関数)でファイルアップロードを実装する際、リクエストボディには上限があります。デフォルトで 6MB というこの制限を知らずにハマるケースは多いです。本記事では Netlify Functions のボディサイズ上限、netlify.toml での設定、multipart/form-data のパース方法、そして制限を超えるファイルを扱う代替手段まで解説します。
Netlify Functions のボディサイズ上限
Netlify Functions は AWS Lambda をベースにしており、リクエストボディのサイズには固定の上限があります。
| Function の種類 | ボディサイズ上限 | 変更可否 | 備考 |
|---|---|---|---|
| Netlify Functions(同期) | 6 MB | 不可 | AWS Lambda の制限 |
| Netlify Functions(Background) | 6 MB | 不可 | 処理時間は最大15分 |
| Netlify Edge Functions | 制限あり | 不可 | Deno ベースのランタイム |
Netlify Functions に 6MB を超えるリクエストを送ると HTTP 413 または関数がタイムアウトします。Vercel(4.5MB)より若干大きいですが、同様に画像や動画などの大容量ファイルは直接受け取れません。
netlify.toml の設定
netlify.toml では Functions のランタイム、タイムアウト、メモリ、リダイレクト、ヘッダーなどを設定できます。ボディサイズ自体は変更できませんが、関連設定を理解しておくことが重要です。
# netlify.toml
[build]
command = "npm run build"
publish = ".next"
functions = "netlify/functions" # Functions のディレクトリ
# Node.js バンドラーの設定
[functions]
node_bundler = "esbuild"
# 特定の Function の設定
[functions."upload"]
included_files = ["uploads/**"]
# リダイレクト設定(Next.js など SPA との組み合わせ)
[[redirects]]
from = "/api/*"
to = "/.netlify/functions/:splat"
status = 200
# ヘッダーの設定(CORS など)
[[headers]]
for = "/.netlify/functions/*"
[headers.values]
Access-Control-Allow-Origin = "*"
Access-Control-Allow-Methods = "GET, POST, PUT, DELETE, OPTIONS"
Access-Control-Allow-Headers = "Content-Type, Authorization"
# 環境変数(本番用。機密情報は Netlify UI で設定すること)
[context.production.environment]
NODE_ENV = "production"
[context.deploy-preview.environment]
NODE_ENV = "development"
multipart/form-data のパース実装
Netlify Functions ではリクエストボディがデフォルトで Base64 エンコードされている場合があります。busboy や formidable を使ったマルチパートのパース方法を解説します。
// netlify/functions/upload.js(CommonJS)
const busboy = require('busboy');
exports.handler = async (event) => {
if (event.httpMethod !== 'POST') {
return { statusCode: 405, body: 'Method Not Allowed' };
}
return new Promise((resolve, reject) => {
const contentType = event.headers['content-type'] || event.headers['Content-Type'];
const bb = busboy({ headers: { 'content-type': contentType } });
const files = [];
const fields = {};
bb.on('file', (name, file, info) => {
const { filename, encoding, mimeType } = info;
const chunks = [];
file.on('data', (data) => chunks.push(data));
file.on('end', () => {
const buffer = Buffer.concat(chunks);
// ファイルサイズの検証(6MB 上限の手前で確認)
const MAX_SIZE = 5 * 1024 * 1024; // 5MB(余裕を持たせる)
if (buffer.length > MAX_SIZE) {
resolve({
statusCode: 413,
body: JSON.stringify({ error: 'ファイルが大きすぎます(上限5MB)。' }),
});
return;
}
files.push({ name, filename, mimeType, buffer, size: buffer.length });
});
});
bb.on('field', (name, value) => {
fields[name] = value;
});
bb.on('finish', () => {
if (files.length === 0) {
resolve({
statusCode: 400,
body: JSON.stringify({ error: 'ファイルが見つかりません。' }),
});
return;
}
const file = files[0];
// ここでファイルを S3 などに保存する処理を行う
resolve({
statusCode: 200,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
success: true,
filename: file.filename,
size: file.size,
mimeType: file.mimeType,
fields,
}),
});
});
bb.on('error', (err) => {
resolve({
statusCode: 500,
body: JSON.stringify({ error: err.message }),
});
});
// Netlify Functions ではボディが Base64 エンコードされる場合がある
const body = event.isBase64Encoded
? Buffer.from(event.body, 'base64')
: event.body;
bb.end(body);
});
};
// netlify/functions/upload-s3.js(S3 に転送する例)
const busboy = require('busboy');
const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3');
const s3 = new S3Client({ region: process.env.AWS_REGION });
exports.handler = async (event) => {
if (event.httpMethod !== 'POST') {
return { statusCode: 405, body: 'Method Not Allowed' };
}
return new Promise((resolve) => {
const bb = busboy({
headers: { 'content-type': event.headers['content-type'] },
limits: { fileSize: 5 * 1024 * 1024 }, // 5MB で切り捨て
});
bb.on('file', async (fieldName, stream, { filename, mimeType }) => {
const chunks = [];
let truncated = false;
stream.on('data', (chunk) => chunks.push(chunk));
stream.on('limit', () => { truncated = true; });
stream.on('end', async () => {
if (truncated) {
return resolve({
statusCode: 413,
body: JSON.stringify({ error: 'ファイルが5MBを超えています。' }),
});
}
const buffer = Buffer.concat(chunks);
const key = `uploads/${Date.now()}-${filename}`;
try {
await s3.send(new PutObjectCommand({
Bucket: process.env.AWS_S3_BUCKET,
Key: key,
Body: buffer,
ContentType: mimeType,
}));
resolve({
statusCode: 200,
body: JSON.stringify({
success: true,
key,
url: `https://${process.env.AWS_S3_BUCKET}.s3.amazonaws.com/${key}`,
}),
});
} catch (err) {
resolve({ statusCode: 500, body: JSON.stringify({ error: err.message }) });
}
});
});
const body = event.isBase64Encoded
? Buffer.from(event.body, 'base64')
: event.body;
bb.end(body);
});
};
Netlify Large Media(廃止予定)と代替案
Netlify Large Media は Git LFS ベースで大容量ファイルを管理するサービスでしたが、現在は新規申込が制限されており、代替手段への移行が推奨されています。
# Netlify Large Media(非推奨・新規利用不可)
# .lfsconfig
[lfs]
url = https://large-media.netlify.com/<repo-id>
代替案として推奨される構成は以下のとおりです。
// Presigned URL 経由で S3 に直接アップロードする Netlify Function
// netlify/functions/get-upload-url.js
const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3');
const { getSignedUrl } = require('@aws-sdk/s3-request-presigner');
const s3 = new S3Client({ region: process.env.AWS_REGION });
exports.handler = async (event) => {
if (event.httpMethod !== 'POST') {
return { statusCode: 405, body: 'Method Not Allowed' };
}
const { filename, contentType, size } = JSON.parse(event.body);
// サーバー側でのバリデーション
const MAX_SIZE = 100 * 1024 * 1024; // 100MB
if (size > MAX_SIZE) {
return { statusCode: 413, body: JSON.stringify({ error: 'ファイルが大きすぎます。' }) };
}
const allowedTypes = ['image/jpeg', 'image/png', 'image/webp', 'application/pdf'];
if (!allowedTypes.includes(contentType)) {
return { statusCode: 415, body: JSON.stringify({ error: '許可されていない形式です。' }) };
}
const key = `uploads/${Date.now()}-${filename}`;
const command = new PutObjectCommand({
Bucket: process.env.AWS_S3_BUCKET,
Key: key,
ContentType: contentType,
ContentLength: size,
});
// 署名付き URL を生成(このリクエストはファイル本体を含まないため制限外)
const presignedUrl = await getSignedUrl(s3, command, { expiresIn: 900 }); // 15分
return {
statusCode: 200,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ presignedUrl, key }),
};
};
// クライアント側:Netlify Function から Presigned URL を取得して S3 に直接アップロード
async function uploadFile(file) {
// 1. Netlify Function から Presigned URL を取得
const res = await fetch('/.netlify/functions/get-upload-url', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
filename: file.name,
contentType: file.type,
size: file.size,
}),
});
const { presignedUrl, key } = await res.json();
// 2. S3 に直接 PUT(Netlify Function を経由しないため容量制限なし)
const uploadRes = await fetch(presignedUrl, {
method: 'PUT',
headers: { 'Content-Type': file.type },
body: file,
});
if (!uploadRes.ok) throw new Error('アップロードに失敗しました');
return key;
}
この記事で使えるテストファイル(無料)
- → PNG テスト画像(1MB) — Netlify Functions 経由のアップロード動作確認に
- → PDF テストファイル(1MB) — 6MB 制限以内のファイルアップロードテストに
- → ZIP テストファイル(1MB) — Presigned URL 経由の S3 直接アップロードテストに