GASで外部APIにファイルを
アップロードする方法|multipart/form-data

GASからファイルを外部APIに送ろうとして、ブラウザで使うFormDataが使えず詰まる人は多いはずです。結論は単純で、payloadにBlobを入れるだけでGASが自動的にmultipart/form-dataを組み立ててくれます。基本形から、手組みが必要になるケース、サイズ上限やエラー調査まで動くコードで解説します。

|対象: GAS / UrlFetchApp / ファイル連携

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のサーバー上で動くため、windowfetchと同様に使えません。

代わりに使うのが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のドキュメントでfileuploadなどと指定されている名前)になること。文字列とファイルを混ぜて渡せること。そして認証は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を使ったファイル連携の設計・開発をご相談いただけます。

GAS開発サービスを見る