受け皿が、失敗の種類を見ていなかった
共有ページは、作ったページを大切な人に見せる入口で、URL と合言葉を渡して、合言葉が合っていれば中が見られます。受け手はアカウントもいりません。画面の文言は「パスワード」だが、渡す側の感覚に近いので、この記事では「合言葉」と書きます(同じものです)。
その解錠処理が、こうなっていた。
async function handleUnlock() {
try {
const p = await fetch_verifyShare(token, input); // 合言葉をサーバで照合
setPayload(p);
} catch (e) {
// どんな失敗もここに来るのに、種類で分けていない
// 第2引数は「サーバが理由を何も言ってこなかったとき」の予備の文言
setError(formatApiError(e, 'パスワードが違います'));
}
}合言葉の照合に失敗したら「パスワードが違います」。ここまでは正しい。問題は、catch(失敗したとき必ず通る受け皿)が失敗の種類を一切見ていなかったことだ。
この画面で起こりうる失敗は、少なくとも三種類あります。
つまり「全部が合言葉違いになる」のではなく、受け手には、自分の入力が悪かったのか通信が悪かったのかが分からない、という壊れ方をしていた。
失敗の種類で、返す言葉を分ける
直し方は、失敗を「ネットに届かなかった」「待たされている」「合言葉が違う」に分けて、それぞれに合った言葉を返すこと。分岐は AI に書いてもらい、どこで線を引くかを自分で決めた。
export function messageForUnlockError(error: unknown): string {
const msg = error instanceof Error ? error.message : "";
// ネットに届いていない(HTTPステータスを持たない)= 通信側の問題
if (!/HTTP \d/.test(msg) && /failed to fetch|networkerror/i.test(msg)) {
return "うまく接続できませんでした。通信状況を確かめて、もう一度お試しください。";
}
// 400(合言葉違い) / 429(待たされ) は detail と throttle 日本語化に委ねる
return formatApiError(error, "合言葉が違うようです。もう一度確かめてみてください。");
}届いている 400・429 は、サーバが返した中身に任せます。
もう一つ引っかかったのは、待たされたとき(429)に返ってくる文章が英語の生メッセージ(Request was throttled...)のままだったこと。世界観の中に急に英語の技術文が出るのも、受け手を置いてけぼりにします。これも、英語の throttle 文を見つけたら日本語に差し替えるようにした。
if (/throttl/i.test(parsed.detail)) {
return "アクセスが集中しています。少し時間をおいてからお試しください。";
}弾かれた受け手には、確かめる手段がない
この画面で弾かれたとき、受け手の側には確かめる手段がありません。合言葉は口頭かメッセージで渡してあるだけで、それが合っているかを本人が検証できません。だから打ち間違えた側だと受け取るしかありませんし、何度か試してダメなら閉じます。閉じたことは送った側には届かないので、この失敗は自分の手元では観測できません。実際にそうやって閉じられた回があったかどうかも、分からないままだ。
共有ページは、アカウントを持たない人が綴Photo に触る唯一の面になります。渡す相手は大切な人で、こちらから操作を教えられません。だからここのエラーは、原因の種類は正直に分けたうえで、言い回しだけ受け手の言葉に置き換えることにした。Request was throttled をそのまま出すのは正直だが伝わりませんし、全部を「パスワードが違います」に丸めるのは伝わるが嘘になります。正直さと分かりやすさは、別々に用意する必要があります。
この記事は2026年6月時点の自分の理解で書いています。仕様や認識が変わってる可能性があります。