サーバレスに認証付きの配信APIを作るときの2つの落とし穴(静的配信で認証が無意味/200なのに0件)
リポ直下に置いたペイロードが認証なしで配信され認証が無意味になる罠と、自作probeのPOSTと実クライアントのGETがズレて200なのに配置0件になる罠。api/_前置の実測表、素のURLを404で確認する検証ループ、実クライアントをimportして受け取り件数で判定する完了条件まで。
約1.5万トークンの節約 (API料金換算で約23円分)。 要件定義・技術調査・試行錯誤ぶんのトークンがまるごと不要になります。※ 出品者申告とレビューに基づく推定値。モデル・タスク内容により変動します。
この巻物について
「サーバレスに認証付きの配信APIを作るときの2つの落とし穴(静的配信で認証が無意味/200なのに0件)」は、Web開発カテゴリのAI指示書(MDファイル)です。リポ直下に置いたペイロードが認証なしで配信され認証が無意味になる罠と、自作probeのPOSTと実クライアントのGETがズレて200なのに配置0件になる罠。api/_前置の実測表、素のURLを404で確認する検証ループ、実クライアントをimportして受け取り件数で判定する完了条件まで。この巻物をAIに読み込ませると、ゼロから設計・調査する場合に比べて 約1.5万トークン(API料金換算で約23円)・86%のトークンを節約できます。
- カテゴリ
- Web開発
- 対応AI
- claude-code、cursor、codex-cli
- ライセンス
- 商用利用可 (再販不可)
- 価格
- 無料
- ゼロから開発時
- 約1.8万トークン
- この巻物使用時
- 約2,600トークン
- 節約量
- 約1.5万トークン (約23円)
- 更新日
- 2026-09-03
使い方 (AIに渡す3つの方法)
いちばん簡単なのはワンライナー。Claude Code のターミナルに貼るだけです。
claude "https://makimono-md.vercel.app/api/v1/files/api-2-200-0/raw を読み込んで、この指示書どおりに実装して"
中身
サーバレスに「認証付きの配信API」を作るときの2つの落とし穴
配布物(設定ファイル・ナレッジ・辞書など)を複数端末へ HTTP で配る仕組みを、 サーバレス(Vercel / Netlify / Cloudflare Pages Functions など、リポジトリをそのまま置くタイプ)に作るとき、 「エンドポイントが 200 を返した」だけで完了にすると必ず踏む落とし穴が2つある。 どちらも実測で踏んで直したもので、どちらもログ上は成功に見える。
このドキュメントは AI エージェントにそのまま読ませて、実装と検証をやらせる粒度で書いてある。
落とし穴1: 認証を掛けたペイロードが、素のURLで誰でも取れている
症状
POST /api/<name> に HMAC 認証を掛けた。認証なしのリクエストは 401 になる。ここまでは正しい。
ところがその API が返す実体ファイルをリポジトリ直下に置いていたため、
curl https://<host>/<payload>.js → 200(認証なし・全文)
となり、認証が完全に無意味になっていた。
原因
リポジトリをそのまま置くタイプのサーバレスは、関数ディレクトリ以外のファイルを静的配信する。
.gitignore に書いても除外されない(git 非追跡のファイルでも配信される)。
つまり「関数だけが読むつもりのデータ」を直下に置くと、それは公開ファイルである。
実測で分かった配信規則(Vercel zero-config)
| パス | 結果 |
|---|---|
/<payload>.js(リポ直下の .js) | 200(誰でも取得できる) |
/package.json /README.md | 404(既定で除外される) |
/api/_helper.js(_ 前置) | 404(配信も関数化もされない) |
/api/<name>.test.mjs | 500 — テストファイルが関数としてデプロイされていた |
対策
- 関数だけが読むデータは
api/_<name>.jsに置き、関数からrequire('./_<name>.js')で読む。_前置は「配信されない」かつ「関数として公開されない」。関数バンドルには require 追跡で含まれる。 - テストファイルも
api/_<name>.test.mjsにする(node --test api/*.test.mjsのグロブは_付きでもマッチする)。 - ルーティング設定(
vercel.jsonの routes 等)で deny を書くのは最後の手段。 ルーティングを触ると既存の別エンドポイントを壊し、全端末を配布から締め出すリスクがある。 ファイルの置き場所で解くほうが安全。
検証(デプロイ後に必ず実行する)
for p in /<payload>.js /.env /.env.local /api/_helper.js /api/<name>.test.mjs; do
echo "$p -> $(curl -s -o /dev/null -w '%{http_code}' https://<host>$p)"
done
全部 404 になることを確認する。ここを見ないと「認証を掛けた」で安心してしまう。 認証の強度ではなく、そのデータが他にどこから取れるかが実際の露出面である。
落とし穴2: 200 を確認したのに、実クライアントは1件も受け取っていない
症状
デプロイ後、自分で書いた probe で叩いて HTTP 200 かつ files が期待件数、を確認した。
ところが実際のクライアントを走らせると HTTP 405 で配置0件だった。
原因は単純で、
- probe は
fetch(url, { method: 'POST', headers })で叩いていた - クライアントは
fetch(url, { headers })= GET で叩いていた - サーバは
if (req.method !== 'POST') return 405
自分で書いた probe は「自分の想定」を検証しているだけで、クライアントの想定とはズレる。
対策
サーバ側を両対応にする(読み取りは冪等なので GET/POST 両方許可して問題ない)。
if (req.method !== 'GET' && req.method !== 'POST') {
res.setHeader('Allow', 'GET, POST');
return json(res, 405, { error: 'method not allowed' });
}
クライアント側を直す選択もあるが、配布物のクライアントは各端末の自己更新を待つので反映に最大1日かかる。 サーバ側で吸収すればデプロイ1回で全端末に効く。直せる側が早い側で直す。
なおレスポンスには Cache-Control: no-store を必ず付ける(認証付きの本文が中間キャッシュに残らないように)。
落とし穴2の親戚: 「届いた」のに参照されていない(成功カウントが嘘をつく)
配布は成功し、ファイルは置かれた。しかし受け側の索引(目次・設定・一覧)に登録されなかった。
実測した実装はこうなっていた:
function updateIndex(text, count) {
if (existingLinePattern.test(text)) return text.replace(existingLinePattern, line);
if (headingPattern.test(text)) return text.replace(headingPattern, (h) => `${h}\n${line}`);
return text; // ← どちらのアンカーも無ければ「何もせず」返す
}
呼び出し側は result.indexed = entries.length を無条件に代入していたので、
1バイトも書いていないのに indexed: 258 と報告した。ログを監視していても気付けない。
しかもそのアンカー(## <見出し>)は配布元の端末に固有の慣習で、受信側の端末には無い。
つまり「配布元では動く/受信側では静かに何もしない」という、最も見つけにくい形になっていた。
対策
- アンカーが無い場合のフォールバックを必ず書く(末尾に見出しごと追記する等)。
- 対象ファイル自体が存在しない場合は新規作成する(例外を catch して 0 を返す設計にしない)。
- 成功カウントは「実際に書いた/実際に置いた」結果から作る。引数の件数をそのまま代入しない。
完了条件の書き方(これがこの文書の本題)
「エンドポイントが 200 を返す」を完了条件にしてはいけない。 次の3つを完了条件にする。
-
実クライアントを走らせて受け取れた件数 実クライアントの関数をそのまま import し、
home(または保存先ルート)を差し替えた 使い捨てディレクトリに対して実HTTPで走らせる。モックにしない。const { installer } = await import(pathToFileURL('<repo>/tools/<client>.mjs').href); const result = await installer({ home: '<throwaway dir>' }); // 既定の本番URLをそのまま使う console.log(JSON.stringify(result)); // → {"installed":N,...}受信側を模したディレクトリは「認証情報だけを置いた空の状態」から作る=新規端末の再現になる。
-
反映先まで見る — ファイルが置かれただけでなく、 索引/設定/一覧からそれが参照されているかを実測する。返り値とディスクの実態を別々に確認する。
-
素のURLが 404 — 上の検証ループ。
遠隔の端末で確認する手段も同時に作る
配布系は「他人の端末に届いたか」が本当の完了条件になる。 状況照会の仕組みがあるなら、ログの最終行ではなくディスクの実態を返させる。
memory=共有258件 / 索引参照=yes
memory=共有0件 / 索引参照=no
memory=なし
ログの最終行は後続処理のログに押し出されるので観測に使えない。 文字数上限のある通知先(チャットなど)へ返す場合は、行を1本増やしたぶんだけ他の行の予算計算を直すこと。
AI エージェントへの指示テンプレ
次の2点を実測で確認してから完了と言うこと。
1) デプロイ後に、配布物の実体ファイルの素のURLを curl して 404 であること
(リポ直下に置いたファイルは認証なしで配信される。api/_ 前置なら配信されない)
2) 自作の probe ではなく、実クライアントのコードを import して
使い捨てディレクトリを保存先に指定し、実HTTPで走らせて「受け取れた件数」と
「索引に参照が入ったか」を出すこと
どちらかを飛ばした報告は受け付けない。
よくある質問
+「サーバレスに認証付きの配信APIを作るときの2つの落とし穴(静的配信で認証が無意味/200なのに0件)」とは何ですか?
リポ直下に置いたペイロードが認証なしで配信され認証が無意味になる罠と、自作probeのPOSTと実クライアントのGETがズレて200なのに配置0件になる罠。api/_前置の実測表、素のURLを404で確認する検証ループ、実クライアントをimportして受け取り件数で判定する完了条件まで。
+どれくらいトークン(費用)を節約できますか?
ゼロから開発すると約1.8万トークンかかりますが、この巻物を使えば約2,600トークンで済みます。差し引き約1.5万トークン(API料金換算で約23円)・86%の節約です。
+どうやって使いますか?
無料です。MDファイルを Claude Code などのAIに読み込ませるだけ。ワンライナーをターミナルに貼れば実装が始まります。要件定義や技術調査を省いて実装だけにトークンを使えます。
+どのAIツールに対応していますか?
claude-code、cursor、codex-cli に対応しています。
+商用利用できますか?
ライセンスは「商用利用可 (再販不可)」です。
🤝 自分でAIを動かすのは、まだ不安…という方へ
この巻物の内容を、AIを使うプロに丸ごと任せることもできます。姉妹サービスAI代行堂なら「LINEで頼むだけで、仕事が完成」。
関連する巻物
Next.js + Supabase + Vercel 立ち上げ完全自動化MD
新規Webサービスの立ち上げ (GCP/GitHub/Vercel/Supabase のプロジェクト作成〜環境変数〜本番デプロイ) を AI に一気通貫でやらせる指示書。人間の作業はログイン1回だけ。
投稿の審査キューを「信頼済みだけ自動公開」で捌く設計(なりすまし穴つき)
審査キューに投稿が溜まったまま埋もれる問題を、信頼済み投稿だけ即公開する形で潰す指示書。無検証のキー発行を信頼判定に使うと第三者が自社メールを騙れる穴と、サーバレスで静的公開棚に実行時公開を足す方法、検証10項目まで含む。
DB型サイトを「一覧だけ会員限定・個別ページは残す」に切り替える指示書(Next.js App Router)
自社DBで集客していたサイトが競合のリスト抜き取りに気づいた時の改修手順。名前が並ぶバルクな一覧だけを会員限定にし、個別ページはtitle/H1とCTAを残す。ItemList JSON-LDやsitemapの漏れ、force-dynamic化のコスト副作用、Layer1(HTML)+Layer2(Playwright実描画)の受け入れテストまで含む。
この巻物、誰かのトークンも救えます
𝕏 で節約レシートをシェア