Zettelkasten Inbox / ブロック詳細
12あるAPIすべての先頭に立つ門。サインイン済みかを1ビットだけ見て通し、どこで失敗しても同じ形のエラーに揃えて返す。8工程しかないが、このアプリの安全性はここの前提に全部乗っている。
抽象度 Lv.4(技術用語+システム上のやりとりまで/製品名・ライブラリ名は書かない)12あるAPIすべての、いちばん最初に置かれている門。やることは2つだけで、 サインイン済みかどうかを見て通すか止めるか決めることと、 途中でどんな失敗が起きても、利用者に同じ形のエラーを返すこと。
ここが薄いのは意図的な割り切りに見える。このアプリは「誰が使ったか」を一切区別しない。 サインインできる人は全員、同じNotionワークスペースの同じ3つのデータベースを読み書きする。 つまりこの門は「知らない人を入れない」ためだけにあり、「人ごとに見せるものを変える」ためには使われていない。
利用者(ブラウザ) → 1. アクセス確認と共通の応答 → 2〜11のすべてのブロック
oai-authenticated-user-id があるかを見る。値の中身は見ない・記録しない401 で返す500 と「処理中に予期しないエラーが発生しました。」に丸める503(=サービス側の準備不足)を使うここより後ろには置けない。2番以降はどれも、いきなりNotionへ要求を投げる。 身元を確かめる前にNotionを呼んでしまうと、サインインしていない人の要求でも Notion側の呼び出し回数を消費し、上限(429)に近づいてしまう。だから門は必ず手前に立つ。
戻り道の応答づくりも同じ場所に置かれている理由。
12本のAPIがそれぞれ勝手にエラーの形を決めると、画面側は12通りの読み方を覚えることになる。
ここで {"error": "…"} という1つの形に揃えているので、画面側は
「error という項目があればその文字を出す、無ければ既定の文言」という1つの処理で済む。
逆に、ここに置かれていないものもある。要求の回数制限(レート制限)、 操作の記録(ログ)、利用者ごとの権限の切り分け。どれもこの門には無い。 個人用の道具として、必要になるまで作らない判断がされている。
| 工程 | 主体 | やりとりの内容 | 方向 |
|---|---|---|---|
| 1-1 | ブラウザ → APIの入口 | HTTPの要求そのもの | → |
| 1-2 | APIの入口 → アクセス確認 | 確認の依頼(関数呼び出し) | → |
| 1-3 | アクセス確認(内部) | 実行環境が本番かどうかの判定 | ↻ |
| 1-4 | アクセス確認(内部) | 身元ヘッダが存在するかの判定 | ↻ |
| 1-5 | アクセス確認 → APIの入口 | 通過、または401相当の例外 | ← |
| 1-6 | APIの入口 → 応答づくり | 捕まえた例外(失敗したときだけ) | → |
| 1-7 | 応答づくり(内部) | 例外の種類からステータスコードを決定 | ↻ |
| 1-8 | 応答づくり → ブラウザ | 同じ形のJSONエラー | ← |
1-1 利用者がボタンを押すか画面を開くと、ブラウザからサーバーへ1回の要求が飛ぶ。 このアプリでは、画面の枠組みだけをサーバーが組み立てて返し、 中身のデータは全部、画面が読み込まれたあとにブラウザから取りに行く作りになっている。
技術的には 12本のAPIはすべて /api/ の下にあり、
読み取りは GET、書き込みや大きな入力は POST、削除は DELETE を使う。
GETとPOSTを分けているのは、GETが「何度呼んでも状態が変わらない」約束の付いたメソッドだから。
候補の下読み(6番)と原典プレビュー(4番)だけは、読み取りなのに POST を使っている。
対象のIDやURLをまとめて本文に入れて送るためで、URLの長さの制限を避ける目的。
12本すべてに「毎回サーバーで作り直す」印が付いており、生成した結果を使い回さない。
1-2 APIの本体は、自分の仕事を始める前に、まず共通の確認係へ「この要求、通していいか」と尋ねる。 この問い合わせはネットワークを通らない。同じサーバーの中での呼び出しである。
技術的には HTTPではなく関数呼び出し。 通信が起きないので、形式の変換も、認証のやり直しも、待ち時間もほぼ発生しない。 この確認係は12本すべてから同じ形で呼ばれ、通れば何も返さず、通らなければ例外を投げる。 「値を返して呼び出し側に判定させる」のではなく例外にしているのは、 呼び出し側が確認結果を見忘れても、処理が先へ進めないようにするため。
1-3 確認係はまず「いま自分が本番で動いているのか」を見る。 本番でなければ、そこで確認は終わり。誰でも通る。
技術的には 実行環境の種類を示す環境変数を見て、 本番以外なら即座に戻る。手元での開発中に、毎回サインインの仕組みを立ち上げなくて済むようにするための逃げ道。 裏を返すと、この分岐を持つアプリを本番以外の設定でインターネットに公開すると、認証が完全に無くなる。 公開時の環境設定が、そのままアクセス制御の一部になっている。
1-4 本番なら、要求に付いてきた「あなたは誰々です」という札が有るか無いかだけを見る。 札の中身が誰なのかは読まない。
技術的には 要求ヘッダの oai-authenticated-user-id を取り出し、
空でなければ通す。この札はアプリが作るのではなく、
アプリを載せている土台(ホスティング側)がサインイン済みの要求に差し込むもの。
アプリ側は署名の検証も有効期限の確認もしない。「土台が差し込んだ札は正しい」という前提に全面的に乗っている。
PINの照合、パスワードのハッシュ、Cookieへの署名、OAuth 2.0 のような外部の認可の流れは、
このアプリのコードには一切入っていない。サインイン用の補助コード一式はファイルとして置かれているが、
どのページからも呼ばれていない未使用のコードである。
1-5 通れば何も起きず、APIの本体が自分の仕事を始める。通らなければ、 その場で処理が中断されて、外側へ飛ばされる。
技術的には 例外には 401 という数字と
「このアプリを利用するにはサインインが必要です。」という日本語が乗っている。
投げられるのは要求本文を読む前なので、
サインインしていない相手から大きな画像を送りつけられても、受け取り切らずに済む。
1-6 APIの本体は、自分の中で起きた失敗を全部ひとまとめに受け止めて、 応答づくりの係へ渡す。認証の失敗も、Notion側の失敗も、想定外の不具合も、同じ入口を通る。
技術的には 12本すべてが処理全体を try で囲み、
例外を1か所に集めてから応答づくりの関数へ渡す。
途中で個別にエラー応答を組み立てている場所は、入力検査(400)だけ。
それ以外は必ずここへ落ちてくる。
1-7 応答づくりは、渡された失敗が「サインインの問題」か「Notion側の問題」か 「それ以外」かを見分けて、返す番号を決める。
技術的には 3段の分岐になっている。
(1) 認証の例外なら 401。
(2) Notion由来の例外なら、Notionが返したコードをそのまま使う――
Notionが 429 なら 429、404 なら 404、403 なら 403。
メッセージも日本語に置き換えたうえで、Notionの言い分を伝える
(「Notion APIキーが無効です。」「対象のNotionデータベースが連携に共有されていません。」など)。
(3) それ以外は、詳細をサーバー側の記録にだけ出して、利用者には 500 と
当たり障りのない一文を返す。内部の作りが漏れる情報を外に出さないための定石。
1-8 どのAPIで何が起きても、ブラウザには同じ形のエラーが届く。 だから画面側は、エラーの読み方を1つ覚えるだけで全部に対応できる。
技術的には 本文は必ず application/json で
{"error": "日本語の一文"} の形。画面側は
「error という文字列の項目があればそれを表示、無ければ画面ごとの既定文」という
共通の関数1つで処理している。
← 図は横にスクロールできます
401 +
{"error": "このアプリを利用するにはサインインが必要です。"}。
画面には「Notionを読み込めませんでした」と再読み込みボタンが出る。503 +「Notion接続情報が設定されていません。」。
Notionへは1度も要求が飛ばない。Retry-After の秒数を1〜5秒に丸めて待ち、
最大2回まで自動で送り直す。それでも駄目なら 429 +
「Notionへのアクセスが混み合っています。少し待って再実行してください。」500。詳細はサーバー側の記録にしか残らない。
利用者側からは何が起きたか分からない。500 で隠した詳細が、実際にどこへ記録され、誰が読めるのか(未確認)※ 2026年8月14〜15日時点の zettelkasten-inbox のプログラムを読んで作成しました。過去の設計資料は参照していません。
※ 番号は全体図・この解説・シーケンス図で共通です。工程番号は「ブロック番号-連番」で書いています。