GASで外部APIにファイルを
アップロードする方法|multipart/form-data
GASからファイルを外部APIに送ろうとして、ブラウザで使うFormDataが使えず詰まる人は多いはずです。結論は単純で、payloadにBlobを入れるだけでGASが自動的にmultipart/form-dataを組み立ててくれます。基本形から、手組みが必要になるケース、サイズ上限やエラー調査まで動くコードで解説します。
Table of Contents
GASでFormDataが使えない理由
Web記事のサンプルをそのまま持ってきて、次のように書いてエラーになるのが典型的なつまずきです。
// これは動きません。FormData はブラウザのAPIで、GASには存在しない
function uploadNg() {
const form = new FormData(); // ReferenceError: FormData is not defined
form.append("file", someFile);
UrlFetchApp.fetch("https://api.example.com/upload", {
method: "post",
payload: form,
});
}FormData(フォーム送信用のデータを組み立てるブラウザのAPI)は、ブラウザにしか存在しません。GASの.gsファイルはGoogleのサーバー上で動くため、windowやfetchと同様に使えません。
代わりに使うのがBlob(ファイルの中身とファイル名・MIMEタイプをひとまとめにしたGASのオブジェクト)です。GASでは、ドライブのファイル、シートのPDF、文字列から作ったCSVなど、あらゆるファイルがBlobとして扱われます。UrlFetchAppはこのBlobを理解するので、こちらでmultipartの組み立てを書く必要はありません。
基本形:payloadにBlobを入れるだけ
ほとんどのAPIは、これだけで送れます。payloadにオブジェクトを渡し、その値のどれかにBlobを入れます。値にBlobが1つでも含まれていれば、UrlFetchAppは自動でmultipart/form-data(ファイルと文字列を1回のリクエストで送るための形式)として組み立てます。
function uploadFile() {
// 送りたいファイルをBlob(ファイルの中身を表すGASのオブジェクト)として用意する
const blob = DriveApp.getFileById("ここにファイルID").getBlob();
const options = {
method: "post",
// payload の値に Blob が含まれていると、UrlFetchApp が
// 自動で multipart/form-data として組み立てて送信する
payload: {
file: blob, // ファイル本体のパート
title: "月次レポート", // 文字列のパートも同時に送れる
category: "report",
},
headers: {
Authorization: "Bearer " + getApiToken(),
},
muteHttpExceptions: true, // エラーでも例外にせずレスポンスを受け取る
};
const response = UrlFetchApp.fetch("https://api.example.com/v1/files", options);
const code = response.getResponseCode();
if (code >= 200 && code < 300) {
console.log("アップロード成功: " + response.getContentText());
} else {
console.error("失敗 (" + code + "): " + response.getContentText());
}
}
function getApiToken() {
// APIキーはコードに直接書かず、スクリプトプロパティに保存しておく
return PropertiesService.getScriptProperties().getProperty("API_TOKEN");
}ポイントは3つです。オブジェクトのキーがそのままフォームの項目名(APIのドキュメントでfileやuploadなどと指定されている名前)になること。文字列とファイルを混ぜて渡せること。そして認証はheadersに書くことです。
項目名はAPIのドキュメントに合わせる
アップロードで最も多い失敗が、項目名の食い違いです。サーバー側がfiles[]を期待しているのにfileで送ると、リクエスト自体は成功したように見えて「ファイルがありません」と怒られます。curlの例が載っていれば、-Fの後ろに書かれている名前をそのままキーにしてください。
contentTypeを自分で書くと壊れる
意外な落とし穴がここです。丁寧なつもりでcontentType: "multipart/form-data"と書くと、かえって送信が失敗します。
// NG: contentType を自分で書くと boundary が食い違い、サーバーが解析できない
const ngOptions = {
method: "post",
contentType: "multipart/form-data", // ← boundary が付いていない
payload: { file: blob },
};
// OK: contentType は指定しない。GAS が boundary 付きで自動生成してくれる
const okOptions = {
method: "post",
payload: { file: blob },
};multipart/form-dataは、各パートの区切りをboundary(区切り文字)という文字列で表します。この文字列はヘッダーとデータ本体の両方に現れ、一致していなければサーバーはパートを切り分けられません。自分でcontentTypeを書くとboundaryのないヘッダーで上書きされ、データ側だけにboundaryが残った不整合なリクエストになります。
自動組み立てに任せる(推奨)
contentType は書かない。payload にオブジェクト+Blob を渡すだけ。boundary はGASが生成してヘッダーにも付ける。
自分でバイト列を組む
boundary を自分で決め、contentType にも同じ boundary を書く。Drive APIのようにmultipart/related が必要なときだけ。
送りたいファイルをBlobにする4パターン
実務で使うBlobの作り方はだいたい次の4つに収まります。どれもpayloadにそのまま渡せます。
// 1. ドライブ上のファイルをそのまま送る(名前とMIMEタイプは元のまま)
const driveBlob = DriveApp.getFileById(fileId).getBlob();
// 2. スプレッドシートをPDFにして送る
const ss = SpreadsheetApp.getActive();
const pdfBlob = ss.getAs("application/pdf").setName("売上報告_2026-07.pdf");
// 3. 文字列からCSVを作って送る
const csv = "商品,数量\n" + "りんご,10\n" + "みかん,5\n";
const csvBlob = Utilities.newBlob(csv, "text/csv", "sales.csv");
// 日本語のCSVをExcelで開く前提なら、UTF-8のBOMを先頭に付けると文字化けを防げる
const bomBlob = Utilities.newBlob(
Utilities.newBlob("\uFEFF").getBytes().concat(Utilities.newBlob(csv).getBytes()),
"text/csv",
"sales.csv"
);
// 4. 別のAPIから取得したファイルをそのまま中継する
const fetched = UrlFetchApp.fetch("https://example.com/logo.png").getBlob();
fetched.setName("logo.png");
// ファイル名とMIMEタイプは Blob 自身に持たせる。ここが multipart の各パートに反映される
driveBlob.setName("契約書.pdf");
driveBlob.setContentType("application/pdf");ファイル名とMIMEタイプ(application/pdfのようなデータ種別の表記)は、Blob自身に持たせるのが基本です。setName()を呼び忘れると、サーバー側に無名のファイルとして届き、後から探せなくなることがあります。日付を含めた命名規則を決めておくと運用が安定します。
手組みが必要なケース:multipart/relatedでDrive APIへ
自動組み立てが使えないのは、APIがフォーム送信ではない形式を求める場合です。代表例がGoogle Drive APIのmultipartアップロードで、1つ目のパートにJSONのメタデータ、2つ目にファイル本体を入れたmultipart/relatedを要求します。この場合はバイト配列を自分で連結します。
// Google Drive API に multipart アップロードする例
// メタデータ(JSON)とファイル本体を multipart/related で1回のリクエストに詰める
function uploadToDrive(blob, folderId) {
const boundary = "-----gas-boundary-" + Utilities.getUuid();
const metadata = {
name: blob.getName(),
mimeType: blob.getContentType(),
parents: [folderId],
};
// 前半(テキスト)→ ファイル本体(バイナリ)→ 終端 の順にバイト配列を連結する
const head =
"--" + boundary + "\r\n" +
"Content-Type: application/json; charset=UTF-8\r\n\r\n" +
JSON.stringify(metadata) + "\r\n" +
"--" + boundary + "\r\n" +
"Content-Type: " + blob.getContentType() + "\r\n\r\n";
const tail = "\r\n--" + boundary + "--";
const payload = Utilities.newBlob(head)
.getBytes()
.concat(blob.getBytes())
.concat(Utilities.newBlob(tail).getBytes());
const response = UrlFetchApp.fetch(
"https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart",
{
method: "post",
// 手組みするときは boundary 込みで contentType を指定する
contentType: "multipart/related; boundary=" + boundary,
payload: payload,
headers: {
// 同じGoogleアカウントの権限をそのまま使う
Authorization: "Bearer " + ScriptApp.getOAuthToken(),
},
muteHttpExceptions: true,
}
);
if (response.getResponseCode() !== 200) {
throw new Error("Driveアップロード失敗: " + response.getContentText());
}
return JSON.parse(response.getContentText()).id;
}コツは「テキスト部分はUtilities.newBlob(文字列).getBytes()でバイト配列に変換し、ファイル本体のgetBytes()とconcat()でつなぐ」という点です。文字列として結合するとバイナリが壊れるため、必ずバイト配列のまま扱ってください。改行は\r\nで、末尾のboundaryにはハイフン2つを付けるのが仕様です。
なお、単に自分のドライブへ保存したいだけならDriveApp.createFile(blob)で十分です。上の書き方が要るのは、共有ドライブへの細かい指定など、DriveAppでは届かないオプションを使いたいときに限られます。認証にScriptApp.getOAuthToken()を使う場合は、必要なスコープをappsscript.jsonに明示しておきます。
// appsscript.json に必要なスコープを明示しておく
{
"timeZone": "Asia/Tokyo",
"oauthScopes": [
"https://www.googleapis.com/auth/script.external_request",
"https://www.googleapis.com/auth/drive.file"
]
}サイズ上限・実行時間と、まとめて送る設計
アップロードには2つの上限が効いてきます。1つはUrlFetchAppのPOSTペイロードで、1リクエストあたり50MBまで。もう1つはスクリプトの実行時間で、無料アカウントは6分です。数十件をまとめて送るなら、実行時間の上限に当たる前に切り上げ、次回の実行で続きから再開する作りにしておくと安全です。
// シートのファイルID一覧を順にアップロードし、結果を書き戻す
function uploadFromSheet() {
const sheet = SpreadsheetApp.getActive().getSheetByName("アップロード対象");
const values = sheet.getDataRange().getValues();
const started = new Date().getTime();
for (let i = 1; i < values.length; i++) {
const fileId = values[i][0];
const status = values[i][1];
if (!fileId || status === "完了") continue; // 処理済みは飛ばす
// 実行時間の上限(6分)に近づいたら、途中で切り上げる
if (new Date().getTime() - started > 4.5 * 60 * 1000) {
console.log(i + "行目で中断しました。次回の実行で続きから再開します。");
break;
}
try {
const blob = DriveApp.getFileById(fileId).getBlob();
const remoteId = uploadToApi(blob);
sheet.getRange(i + 1, 2).setValue("完了");
sheet.getRange(i + 1, 3).setValue(remoteId);
} catch (e) {
sheet.getRange(i + 1, 2).setValue("エラー");
sheet.getRange(i + 1, 3).setValue(String(e).slice(0, 200));
}
SpreadsheetApp.flush(); // 進捗をその場でシートに反映する
}
}処理済みかどうかをシートの列で管理しておけば、途中で止まっても同じファイルを二重にアップロードしません。あとは時間主導トリガーで数分おきに実行すれば、放っておいても最後まで進みます。50MBを超えるファイルは1回では送れないため、APIが分割アップロードに対応しているかを先に確認してください。
アップロードが失敗したときの調べ方
まずmuteHttpExceptions: trueを付けます。これがないとエラー時に例外で止まり、サーバーが返した肝心のメッセージを読めません。
function debugUpload(blob) {
const response = UrlFetchApp.fetch(url, {
method: "post",
payload: { file: blob },
muteHttpExceptions: true,
});
console.log("status: " + response.getResponseCode());
console.log("body: " + response.getContentText().slice(0, 500));
// 送信サイズを確認する(POSTペイロードの上限は 50MB)
console.log("size: " + Math.round(blob.getBytes().length / 1024) + " KB");
}400 / 422
項目名や必須パラメータの不足。curlの -F の名前と payload のキーが一致しているか確認する。
401 / 403
認証エラー。Authorizationヘッダーの形式(Bearer の後ろに半角スペース)と、トークンの有効期限を確認する。
413
ファイルが大きすぎる。API側の上限を確認し、必要なら分割アップロードに切り替える。
415
Content-Typeの不一致。自分でcontentTypeを書いていないか、boundaryが含まれているかを見直す。
それでも原因が分からないときは、同じリクエストをhttps://httpbin.org/postのようなリクエスト内容をそのまま返すサービスに送ると、GASが実際に組み立てたパート名やContent-Typeを目で確認できます。切り分けの近道です。
まとめ
GASからのファイルアップロードは、payloadにBlobを入れてcontentTypeを書かない、これが基本です。ファイル名とMIMEタイプはBlobに持たせ、項目名はAPIのドキュメントに合わせます。 multipart/relatedのような特殊な形式を求められたときだけ、boundaryを自分で決めてバイト配列を連結します。 あとはmuteHttpExceptionsでレスポンスを読める状態にし、50MBと6分という2つの上限を意識して件数を分ければ、日次のファイル連携も安定して回せます。
よくある質問
サーバー側(.gsファイル)では使えません。FormDataはブラウザのAPIで、GASの実行環境には存在しないためです。代わりにUrlFetchAppのpayloadにオブジェクトを渡し、その値にBlobを入れます。値にBlobが1つでも含まれていると、UrlFetchAppが自動でmultipart/form-data形式に組み立てて送信します。
自動組み立てに任せる場合は指定しないでください。multipart/form-dataはboundary(各パートの区切り文字)をヘッダーに含める必要があり、自分でcontentTypeを書くとGASが生成したboundaryと食い違い、サーバー側でパートを解析できなくなります。boundaryまで含めて自分で管理する場合だけ、手組みしたバイト列と一緒に指定します。
Blob自体に持たせます。blob.setName("report.pdf")でファイル名、blob.setContentType("application/pdf")でMIMEタイプを設定すると、その値がmultipartの各パートのヘッダーに反映されます。DriveApp経由で取得したファイルは元の名前とMIMEタイプを持っているため、そのまま送れば問題ありません。
UrlFetchAppのPOSTペイロードは1リクエストあたり50MBまでです。加えて、スクリプト全体の実行時間の上限(無料アカウントで6分)もあります。大きなファイルを扱う場合は、APIが分割アップロード(resumable upload)に対応していればそちらを使うか、ファイル自体を分割する設計にしてください。
optionsにmuteHttpExceptions: trueを付けて、例外ではなくレスポンスとして受け取ります。そのうえでresponse.getResponseCode()とresponse.getContentText()をログに出せば、APIが返すエラーメッセージを読めます。400番台なら認証ヘッダー・パラメータ名・必須項目、415ならContent-Typeの指定を疑うのが定石です。
Drive APIのmultipartアップロードは、1つ目のパートにJSONのメタデータ、2つ目にファイル本体を入れるmultipart/related形式を求めるためです。UrlFetchAppが自動生成するのはフォーム送信用のmultipart/form-dataなので形式が合いません。バイト配列を自分で連結してpayloadに渡す必要があります。
外部サービスとの
ファイル連携を相談する。
請求書のアップロード、帳票の外部システム連携、ドライブとSaaSの同期など、GASを使ったファイル連携の設計・開発をご相談いただけます。