結論: Bufferはメディアを「投稿時」に取りに行く
Buffer GraphQL API でスケジュール投稿を登録すると、API の呼び出し自体は成功する。ステータスコード200が返ってくるし、Post IDも発行される。「やった、動いた!」と喜ぶ。
だが、実際の投稿時刻になると、Buffer がメディアURLにアクセスして画像・動画を取得しようとする。このタイミングでURLが死んでいたり、レスポンスヘッダーが不正だと、投稿は無言で失敗する。
この記事では、私が SNS 自動投稿パイプラインの初回テストで直面した3つの問題と、その原因調査・対策を共有する。同じ轍を踏む人が減れば嬉しい。
何が起きたか — 3つのエラーメッセージ
Remotion で生成した Instagram Carousel 画像と Reel 動画を、Buffer GraphQL API 経由でスケジュール投稿した。API のレスポンスは正常。Buffer のダッシュボードにも投稿が並んだ。
で、投稿時刻になった。
Buffer のダッシュボードを開くと、全投稿が赤いエラー表示に変わっていた。表示されたエラーメッセージは3種類:
There appears to be an issue with the attached media or link attachment.
This could be due to the file being too large or connection timing out.
Please try removing the media or link attachment and try the post again.
There is an issue with the media included.
Please remove it and try to upload it again.
It looks like there was an issue with the media attached to this post.
Please check to ensure the media meets all specifications.
正直に書くと、最初は「ファイルサイズの問題かな?」と思った。エラーメッセージが「file being too large」って言ってるし。でも、画像は50KB程度、動画は2MB未満。Instagram の上限(画像8MB、動画300MB)には遠く及ばない。
ここから原因調査が始まった。
原因1: tmpfiles.org のURLは1時間で消える
症状
夜22時に予約投稿を登録。API レスポンスは正常。翌朝、全投稿が失敗していた。
調査
最初に使っていたファイルホスティングは tmpfiles.org だった。無料で使えて、API もシンプル。アップロードするとURLが返ってくる。
// tmpfiles.org へのアップロード
const form = new FormData();
form.append('file', blob, fileName);
const res = await fetch('https://tmpfiles.org/api/v1/upload', {
method: 'POST',
body: form,
});
const json = await res.json();
// json.data.url → "https://tmpfiles.org/12345/image.jpg"
// 直リンクは "https://tmpfiles.org/dl/12345/image.jpg"
アップロード直後にブラウザでURLを開くと、ちゃんと画像が表示される。問題なさそうに見えた。
でも、tmpfiles.org のURLの有効期限は1時間だった。
核心: Buffer はいつメディアを取得するか
ここが最大の落とし穴。Buffer GraphQL API の createPost ミューテーションでメディアURLを渡すと、Buffer はその場ではメディアをダウンロードしない。 URLだけ保存して、実際に投稿するタイミング(dueAt で指定した日時、または shareNow の場合はキュー処理のタイミング)でメディアを取りに行く。
投稿登録 (API呼び出し) → 即座にはメディアを取得しない
↓
投稿時刻 (数分〜数時間後) → このときメディアURLにアクセス
↓
URLが失効していたら → 投稿失敗 ❌
つまり、tmpfiles.org(1時間で失効)でスケジュール投稿(数時間後)は、そもそも成立しない構造だった。
対策
永続URLを提供するファイルホスティングに切り替えた。具体的には catbox.moe(永続URL)をメインに採用。
// catbox.moe へのアップロード(永続URL)
const form = new FormData();
form.append('reqtype', 'fileupload');
form.append('fileToUpload', blob, fileName);
const res = await fetch('https://catbox.moe/user/api.php', {
method: 'POST',
body: form,
});
const url = (await res.text()).trim();
// → "https://files.catbox.moe/abc123.jpg" (永続)
catbox.moe はアカウント登録不要で、アップロードしたファイルのURLは無期限で有効。これで「投稿時刻にURLが死んでいる」問題は解消された。
原因2: PNG画像よりJPEGのほうが安全
症状
catbox.moe に切り替えた後、Carousel 画像の投稿は成功する場合としない場合があった。
調査
Buffer の公式サポートページを確認した。Instagram Carousel の画像に関する仕様:
| 項目 | 制限 |
|---|---|
| 最大ファイルサイズ | 8MB |
| アスペクト比 | 4:5 〜 1.91:1 |
| 推奨フォーマット | JPEG |
| 最大枚数 | 10枚 |
ここで気になったのが「推奨フォーマット」。Instagram 自体は PNG も受け付けるが、Buffer を経由する場合は JPEG が安定する という情報があった。
当時の Remotion レンダリング設定は PNG 出力だった:
// 修正前: PNG出力(1枚あたり 70KB〜1MB)
await renderStill({
composition,
serveUrl: bundleLocation,
output: `slide-01.png`,
imageFormat: 'png',
});
PNG だとファイルサイズも大きくなりがちで、表紙スライドが1MBを超えることもあった。
対策
JPEG(quality: 85) に変更した:
// 修正後: JPEG出力(1枚あたり 46〜61KB)
await renderStill({
composition,
serveUrl: bundleLocation,
output: `slide-01.jpg`,
imageFormat: 'jpeg',
jpegQuality: 85,
});
結果:
| フォーマット | 1枚あたりのサイズ | 7枚合計 |
|---|---|---|
| PNG | 70KB〜1.1MB | 約2.6MB |
| JPEG (quality 85) | 46〜61KB | 約375KB |
ファイルサイズが 約85%削減 されて、投稿の安定性も向上した。quality 85 なら見た目の劣化はほぼ気にならない。
原因3: catbox.moe は Content-Length: 0 を返す
症状
画像(Carousel)は catbox.moe で安定して投稿できるようになった。しかし、動画(Reel / Shorts)は相変わらず失敗する。 エラーメッセージが変わった:
Failed to create post: Failed to extract video metadata: Invalid media info response
これは Buffer のダッシュボードではなく、API レスポンスで直接返ってきたエラーだった。
調査
「video metadata の抽出に失敗」ということは、Buffer が動画URLにアクセスして、ファイルの情報(コーデック、解像度、時間など)を読み取ろうとして、それに失敗しているということだ。
動画ファイル自体に問題があるのか? ローカルで再生してみたが、正常に再生される。H.264 コーデック、1080×1920、34秒、1.84MB。Instagram の仕様範囲内。
次に、catbox.moe にアップロードした動画URLのレスポンスヘッダーを調べた:
# catbox.moe の HEAD レスポンスを確認
Invoke-WebRequest -Uri "https://files.catbox.moe/xxxxx.mp4" -Method Head
# 結果:
# Status: 200
# Content-Type: video/mp4
# Content-Length: 0 ← !?
Content-Length: 0。
ファイルは1.84MBあるのに、HEAD レスポンスの Content-Length が 0 を返している。GET リクエストでは正常にファイル全体が返るが、HEAD だと 0。
これが原因だった。Buffer は動画URLにまず HEAD リクエストを送って、ファイルサイズや Content-Type を確認する。Content-Length が 0 だと「空ファイルか、不正なファイルだ」と判断して、メタデータの抽出を中止する。
ちなみに、同じ catbox.moe の画像URLも Content-Length: 0 を返すが、画像の場合は Buffer がメタデータ抽出をスキップするらしく、問題にならない。動画だけがこの影響を受ける。
検証: litterbox.catbox.moe なら正しい Content-Length を返す
catbox.moe の姉妹サービスである litterbox.catbox.moe(72時間有効の一時ホスティング)を試した:
# litterbox の HEAD レスポンスを確認
Invoke-WebRequest -Uri "https://litter.catbox.moe/xxxxx.mp4" -Method Head
# 結果:
# Status: 200
# Content-Type: video/mp4
# Content-Length: 1932497 ← 正しいサイズ!
litterbox は 正しい Content-Length を返す。同じ catbox 系のサービスなのに、HEAD レスポンスの実装が違う。
対策
画像と動画でアップロード先を分ける戦略に変更した:
async function uploadToFileHost(filePath: string): Promise<string> {
const isVideo = fileName.endsWith('.mp4');
if (!isVideo) {
// 画像: catbox.moe 優先(永続URL、Content-Length不問)
return await uploadToCatbox(filePath);
} else {
// 動画: litterbox 優先(正しいContent-Lengthヘッダー)
return await uploadToLitterbox(filePath);
}
}
| メディア | アップロード先 | 理由 |
|---|---|---|
| 画像 (JPEG) | catbox.moe | 永続URL。Buffer は画像の Content-Length を要求しない |
| 動画 (MP4) | litterbox.catbox.moe | 72時間有効。正しい Content-Length を返す |
この変更で、Instagram Carousel・Reel・X・YouTube Shorts の全プラットフォームへの投稿が成功した。
ファイルホスティングサービスの比較
Buffer API でメディア付き投稿をする場合に、各ファイルホスティングサービスがどう振る舞うかをまとめた。これは公式ドキュメントには載っていない。全部自分で試した結果だ。
| サービス | URL有効期限 | HEAD Content-Length | Buffer画像投稿 | Buffer動画投稿 | 備考 |
|---|---|---|---|---|---|
| catbox.moe | 永続 | ❌ 0を返す | ✅ | ❌ メタデータ抽出失敗 | 画像に最適 |
| litterbox.catbox.moe | 72時間 | ✅ 正しい値 | ✅ | ✅ | 動画に最適 |
| tmpfiles.org | 1時間 | ✅ 正しい値 | ⚠️ 期限切れリスク | ⚠️ 期限切れリスク | 即時投稿のみ |
| 0x0.st | 永続 | — | — | — | Node.jsからHTTP 418で拒否 |
この表が、この記事で一番伝えたかったことかもしれない。
最終的なアップロード戦略
3つの問題を踏まえて、最終的に落ち着いた構成:
画像アップロード:
1. catbox.moe(永続) → 成功ならそのまま使う
2. litterbox(72h) → フォールバック
3. tmpfiles.org(1h) → 最後の手段
動画アップロード:
1. litterbox(72h) → Content-Length正常、Buffer動画対応
2. catbox.moe(永続) → 一部のAPIでは動作する可能性
3. tmpfiles.org(1h) → 最後の手段
さらに、アップロード後にURL到達性の検証を入れた:
async function verifyUrl(url: string): Promise<boolean> {
try {
const res = await fetch(url, { method: 'HEAD', redirect: 'follow' });
if (res.ok) return true;
// HEAD非対応のサービス向けに GET + Range でフォールバック
const res2 = await fetch(url, {
method: 'GET',
headers: { Range: 'bytes=0-0' },
});
return res2.ok || res2.status === 206;
} catch {
return false;
}
}
検証が失敗したら次のサービスにフォールバック。全サービス失敗なら2回までリトライ(exponential backoff)。ここまで入れて、ようやく安定した。
振り返り: 「APIが成功した」は信用するな
今回の最大の学びは、「API のレスポンスが200を返した」と「実際に投稿が成功する」は別物だということ。
Buffer の createPost ミューテーションは、メディアURLの有効性を検証しない。URLの形式だけチェックして、中身は投稿時まで見に行かない。これは設計としては合理的だが(同期的にメディアをダウンロードしたらAPI応答が遅くなる)、使う側は注意が必要。
問題の調査にかかった時間を振り返ると:
| 問題 | 気づくまでの時間 | 原因特定 | 修正 |
|---|---|---|---|
| tmpfiles.org のURL期限切れ | 翌朝(投稿失敗を確認して) | 30分 | 15分 |
| PNG → JPEG の切り替え | 同日 | 1時間(Buffer仕様の調査) | 10分 |
| catbox.moe の Content-Length: 0 | 翌日(動画だけ失敗して) | 2時間(HEADリクエストの調査) | 30分 |
Content-Length: 0 の問題が一番時間がかかった。「ファイルは正常」「URLはアクセスできる」「GETなら動画が返ってくる」のに投稿が失敗する。HEADレスポンスのヘッダーまで調べて、ようやく原因にたどり着いた。
再発防止チェックリスト
同じ問題を踏まないための確認事項をまとめた:
- ✅ ファイルホスティングのURL有効期限を確認する(スケジュール投稿なら最低72時間は必要)
- ✅ 画像は JPEG(quality 85)で出力する(PNGより軽量で互換性が高い)
- ✅ 動画のアップロード先は、HEADレスポンスで正しい Content-Length を返すサービスを選ぶ
- ✅ アップロード後にURL到達性を検証する(HEAD + GET/Range のフォールバック)
- ✅ 複数のホスティングサービスをフォールバックとして用意する
- ✅ ファイルサイズを事前チェックする(画像8MB、動画300MB)
- ✅ DRY RUN モードでアップロードとメタデータを事前確認する
よくある質問
Q: Buffer の無料プランでメディア付き投稿はできる?
できる。無料プランでも画像・動画付きの投稿が可能。制限は 3チャンネル × 10投稿/チャンネル まで。API からの投稿もこの枠に含まれる。
Q: 0x0.st が使えないのはなぜ?
0x0.st は Node.js の fetch からアップロードすると HTTP 418(I'm a teapot) を返す。User-Agent やリクエストパターンでボット判定されている模様。curl からなら問題なく使えるが、スクリプトからの自動化には不向き。
Q: catbox.moe の Content-Length: 0 は仕様?バグ?
不明。catbox.moe の公式ドキュメントにはHEADリクエストの挙動について記載がない。GETリクエストでは正常にファイルが返るので、サービスとしては動作している。ただ、Buffer のように HEAD レスポンスのヘッダーに依存するサービスと組み合わせると問題になる。
Q: litterbox の72時間制限は問題にならない?
スケジュール投稿の場合、投稿時刻から72時間以内にBufferがメディアを取得すれば問題ない。実際には、投稿時刻から数分以内にBufferがメディアを取得するので、72時間あれば十分すぎる。即時投稿(shareNow)の場合はなおさら問題ない。
Q: Remotion の renderMedia で crf と videoBitrate を同時に指定できる?
できない。crf と videoBitrate を同時に指定すると、Remotion がエラーを投げる:
"crf" and "videoBitrate" can not both be set. Choose one of either.
タイピングアニメーションのような低動作コンテンツなら、crf: 23 だけで十分な画質が得られる。ファイルサイズも2MB未満に収まった。
📝 この記事は Zenn で最初に公開されました。
最新版はZennをご覧ください。