タグ: トラブルシューティング

  • リンク先は存在した。それでも404だった

    リンク先は存在した。それでも404だった

    連載の記事に「次回:〇〇」というリンクを書いています。そこを押すと404でした。

    リンク切れは事前に確認していました。結果は0件でした。確認の仕方が間違っていたのです。

    この記事で扱うこと

    存在するのに辿り着けないリンクの話です。確認が通ってしまった理由、直し方を3つ比べて1つを選んだ過程、そして表示するときに判定する実装を書きます。

    何を数えていたか

    確認の中身はこうでした。全記事の本文から /blog/ で始まるリンクを抜き出し、その行き先のスラッグがCMSに存在するかを見る。存在しないものがあれば、それがリンク切れです。

    この方法で、112本ぶんを突き合わせて0件でした。

    存在するかどうかを見ていた

    CMSの中には、公開済みの記事と、予約投稿の記事が両方入っています。

    連載の第1回を公開した時点で、第2回はまだ予約投稿の状態です。CMSには在るので、確認は通ります。しかしサイトに出ているのは公開済みのものだけなので、押すと404に着きます。

    2つの集合がずれている

    整理すると、こうなっていました。

    含まれるもの
    CMSにある記事 公開済み + 予約投稿 + 下書き
    サイトに出ている記事 公開済みだけ

    確認していたのは上の集合で、読む人が見ているのは下の集合でした。差の部分が、そのまま404になります。

    書いている側からは見えません。管理画面では第2回が普通に並んでいますし、編集もできます。書き手の視界には、公開前の記事も入っているからです。

    直し方を3つ比べた

    案 やめた理由 / 選んだ理由
    公開のたびに本文からリンクを外す 連載は5本あり、回が進むたびに手を入れる。忘れた回だけ404が残る
    「次回」を書かない 404は消えるが、次があることも伝わらなくなる。連載として損
    表示するときに判定する 採用。書く側は常に「次回」を書ける。公開日が来れば自動でリンクに戻る

    1つめを外したのは、人が覚えていることを前提にした手順だからです。今回そもそも見落としたのは、覚えていることを前提にした確認をしていたからでした。同じ形の対策を足すと、同じ理由で破れます。

    本文を通す場所が、もう1段増えた

    このサイトの本文は、表示する前にいくつかの処理を順番に通ります。商品カードを差し込み、リンクの末尾の形を揃え、見出しに目印を付ける。そこに1段足しました。

    const posts = await getPosts();
    
    // getPosts() は公開済みのものだけを返す。
    // ここに無いスラッグは「CMSには在るが、まだ出ていない」記事になる。
    const publishedSlugs = new Set(
      posts.map((p) => p?.slug).filter((s): s is string => Boolean(s)),
    );
    
    const rawContent = softenPendingLinks(
      normalizeInternalLinks(injectProducts(post.content)),
      publishedSlugs,
    );
    

    判定の材料はもともと手元にありました。前後の記事と関連記事を出すために、記事一覧をすでに取っています。取得の順番を入れ替えただけで、通信は1回も増えていません。

    リンクを消さずに、文字にした

    return html.replace(POST_LINK, (whole, _before, slug, _after, text) => {
      if (publishedSlugs.has(slug)) return whole;
      return `<span class="pending-link">${text}<span class="pending-link-note">(近日公開)</span></span>`;
    });
    

    行き先が無いなら丸ごと消す、という手もありました。選ばなかったのは、予告そのものには意味があるからです。

    押せないことと、無いことは違う

    「次回:打ち方が違っても、同じ言葉として扱う」という一文は、続きがあることと、その内容を伝えています。押せるかどうかとは別の仕事です。

    消すと、その情報まで一緒に消えます。薄い文字にして「(近日公開)」を添えれば、押せないことは分かり、何が来るかは残ります。

    「(近日公開)」をCSSの装飾ではなく本文の一部として出しているのは、読み上げや、文字を選んでコピーしたときに落ちないようにするためです。押せない理由は、飾りではなく中身として持たせることにしました。

    置き換えで壊しそうな場所

    本文のHTMLを書き換える処理は、意図しないものまで巻き込みます。6つ試しました。

    試したこと 期待
    公開済みへのリンク そのまま残る
    1つの行に「前回」と「次回」が並ぶ 前回は残り、次回だけ文字になる
    class や title が前後に付く 拾える
    連載の目次(/series/)へのリンク 触らない
    外部サイトのURL 触らない
    リンクの文字に <strong> が入る 中のタグを保つ

    5つめは見落としやすい形をしています。外部のURLにも /blog/ という並びが現れることがあるので、行き先がサイト内かどうかを先に判定する必要がありました。

    確認そのものを直す

    今回いちばん効いたのは、コードの修正ではありません。確認の条件を書き直したことです。

    見ていたもの
    前 リンク先のスラッグが、CMSに存在するか
    後 リンク先のスラッグが、公開済みの一覧に在るか

    条件を書き直して数え直したら、公開中の57本のうち、公開前を指しているリンクは1件でした。指摘を受けたその1件です。数が合ったことで、見落としの範囲が確定しました。

    公開日が来たら、本当に戻るのか

    「公開日が来れば自動でリンクに戻る」と書きましたが、これは作った側の見込みでしかありません。確かめる機会は、2日後に来ました。

    第2回の公開日に、第1回と第2回を両方開いて、連載の案内を見比べました。

    記事 「次回」の行き先 表示
    第1回 第2回(その日に公開) リンクに戻っていた。押すと開く
    第2回 第3回(まだ予約) 文字のまま、「(近日公開)」付き

    第1回の「次回」は、2日前まで押せない文字でした。その日の朝、誰も何も触らないまま押せるリンクになっていて、代わりに第2回の「次回」が押せない文字になっていました。1本ずつ、順送りに開いていく形です。

    戻ることまで確かめて、はじめて直ったと言える

    押せなくする側だけを見て終わりにしていたら、「公開後もずっと押せないまま」という別の壊れ方に気づけませんでした。

    表示を止める仕組みは、止めたものを再開させる側まで確認してようやく半分ではなくなります。

    今回の学び

    まず、存在することと、辿り着けることは別だということ。手元のデータで「在る」と確認できても、読む人に配っている集合に入っていなければ意味がありません。確認は、読む人が見るほうの集合でやる必要がありました。

    次に、書き手の視界は広すぎること。管理画面には公開前のものも並びます。そこを基準に考えると、自分にだけ見えているものを、全員に見えていると錯覚します。

    そして、人が覚えていることを前提にした対策を足さないこと。見落としの原因が「覚えていること前提の確認」だったのに、対策も同じ形にしたら、同じ理由で破れます。表示するときに判定する形にしたのは、そこを繰り返さないためでした。

  • 除外しているつもりで、1件も除外していなかった

    除外しているつもりで、1件も除外していなかった

    アクセス解析に、自分のアクセスを入れないための設定があります。ずっと「テスト」のままになっていたので、「有効」に切り替えました。

    それでも、何も除外されません。設定がもう一段、奥にありました。

    この記事で扱うこと

    自分のアクセスを除外していたつもりで、1件も除外できていなかった話です。設定が2段構えになっていること、片方だけでは何も起きないこと、そしてそれが画面からは分からない理由を書きます。

    印を付ける側と、捨てる側

    この除外は、2つの設定が噛み合ってはじめて動きます。

    置き場所 役割
    内部トラフィックの定義 指定したIPからのアクセスに「内部」という印を付ける
    データフィルタ その印が付いたものを捨てる

    片方だけでは何も起きません。印を付けても捨てる設定が無ければ残りますし、捨てる設定があっても印が付いていなければ、捨てる対象が存在しません。

    捨てる側だけがあった

    確認したところ、こうなっていました。

    設定 状態
    データフィルタ あり(内部の印が付いたものを除外)
    内部トラフィックの定義 ルールが1件も作られていない

    つまり、どのアクセスにも「内部」の印が付いていませんでした。フィルタは、一致するものが1件も無い状態で回っていたことになります。

    「テスト」のままだったことを問題だと思っていましたが、有効にしたところで結果は変わりません。除外対象がゼロだからです。

    なぜ気づかなかったか

    一覧の画面には、フィルタが1件、名前も種類も状態も表示されていました。

    設定が「ある」ことと、「効いている」ことは別

    見えるのは、フィルタという入れ物があることだけです。その中身が参照している印を、誰が付けているのかまでは出てきません。

    設定の一覧は、設定した本人が覚えている前提で作られています。半年前の自分が何をしたかは、そこには書いてありません。

    もうひとつ、状態の表示も助けになりませんでした。「テスト」は評価はするが捨てないという意味で、一致が0件かどうかは区別されません。一致が無いことと、一致したが捨てていないことが、同じ表示になります。

    効いていないことを、どう確かめるか

    この種の設定は「何も起きないこと」が正常な状態なので、確かめ方を別に持っておく必要があります。

    噛み合っているかを、1本の線で追う

    除外の設定を確認するときは、フィルタの画面を見るだけでは足りません。

    「誰が印を付けるのか」から順に辿る必要があります。今回で言えば、IPのルールがあるか、そのルールが付ける印の名前が、フィルタが探している名前と一致しているか、という2点です。

    実際に確認したら、印を付ける側の画面には「ルールはまだ作成されていません」と1行だけ書かれていました。探しに行かなければ、ずっと見ない場所です。

    直すときに気をつけたこと

    ルールを作るには、自分のIPアドレスを登録します。ここで、手元のメモに残っていた古い値を使うのはやめました。

    古いIPは、いま別の人が使っている

    家庭の回線のIPは、契約が同じでも変わることがあります。

    変わったあとの値を登録すると、自分は除外されず、代わりに無関係な誰かが除外されます。しかも、どちらも画面からは分かりません。

    設定画面に現在のIPを表示するリンクがあるので、そこで確認した値を使いました。複数の回線から見るなら、条件を並べて足せます。

    さかのぼっては、消えない

    最後にひとつ。この設定が効くのは、これから溜まるデータだけです。

    これまでに入ってしまった自分のアクセスは、あとから消えません。過去の数字は、自分が混ざったまま読むことになります。どのくらい混ざっているかも分からないので、公開直後の数字は参考程度に扱うのが正しい、ということになりました。

    今回の学び

    まず、設定が2段構えのときは、片方だけ直しても何も変わらないということ。「テストを有効にする」は正しい操作でしたが、直す場所がそこではありませんでした。

    次に、一覧に出ているのは入れ物だけだということ。名前も種類も状態も表示されますが、それが誰と噛み合う想定なのかは出てきません。設定を確認するときは、一覧ではなく経路を辿る必要がありました。

    そして、古い値を使い回さないこと。合っていれば自分が消え、間違っていれば他人が消えます。どちらの結果も画面には出ないので、確かめられる方法で取り直すほうが安全でした。

  • 設定を足しただけでは、変わらなかった

    設定を足しただけでは、変わらなかった

    問い合わせフォームの送信先を設定して、保存しました。画面を開き直しても、フォームは「準備中」のままでした。

    設定は間違っていません。読まれるタイミングが違っただけでした。

    この記事で扱うこと

    問い合わせの受け取りを外部サービスに任せるまでの判断と、その設定でつまずいた3点です。環境変数がいつ読まれるか、保存したら見られなくなる値のこと、そして「正しく弾けているか」の確かめ方を書きます。

    なぜ自分でメールを送らないのか

    このサイトには、メールを送る手段がありません。足すこと自体はできますが、そこから先が長い。

    迷惑メール扱いされないためには、そのドメインから送ってよいという宣言をDNSに置く必要があります。SPFとDKIMと呼ばれるものです。

    SPFは1ドメインに1本しか置けない

    すでに何かの設定が入っている状態で足すと、書き方を誤った瞬間にメールが届かなくなります。

    しかも壊れたことは、こちらからは分かりません。誰かに「返信が来ない」と言われるまで、正常に見え続けます。

    問い合わせを1本受け取るために、この重さを引き受けるのは割に合わないと判断しました。受け取りだけ外に出すことにします。

    求めているのは、転送先1つだけ

    ボット対策も、エラーの出し分けも、入力の確認も、すべてサイト側に置いてあります。外のサービスに任せているのは「受け取って、メールで転送する」という一点です。

    この形にしておくと、乗り換えの費用が小さくなります。別のサービスに移すときに変えるのは送信先のURL1つで、コードには触りません。

    最初から小さく繋いでおく

    外部サービスの機能を使い込むほど、そのサービスの形に合わせた作りになります。便利ですが、離れにくくもなります。

    「何を任せないか」を先に決めておくほうが、あとから選び直せます。

    つまずき1:環境変数は、ビルドのときに読まれる

    送信先のURLは、コードに書かずに環境変数として置いています。保存して、サイトを開き直しました。変わりません。

    思っていたこと 実際
    設定を保存すれば、次のアクセスから反映される 作り直すまで、前の状態のまま

    このサイトはページを先に作ってしまう構成なので、環境変数を読むのはページを作るときです。あとから設定を足しても、すでに作られたページは古い状態を抱えたままになります。

    配信サービスの画面にも「変更を反映するには新しいデプロイが必要です」と出ていました。読んでいませんでした。保存した時点で終わったつもりになっていたからです。

    つまずき2:保存すると、見られなくなる値がある

    環境変数を保存するとき、種別を2つから選べました。

    種別 あとから値を見られるか
    設定値 見られる
    秘密 見られない

    今回は「秘密」を選びました。訪問者に配るファイルへ混ざってほしくない値だからです。

    ただしこれは、自分でも二度と確認できないという意味でもあります。今回は発行元の画面でいつでも見られるURLなので困りませんが、そうでない値なら、保存する前に控える必要がありました。

    つまずき3:相手側の防御が、こちらの正常な送信を弾く

    受け取り側のサービスにも、独自のボット対策が用意されています。画像の文字を読ませるあれです。

    ブラウザ前提の仕組みは、サーバー間では通らない

    この対策は「人がブラウザで操作している」ことを前提にしています。

    今回はサイトのサーバーから相手のサーバーへ送る形なので、有効になっているとこちらの正常な送信まで弾かれます。

    今回は既定で無効になっていたので何もせずに済みましたが、有効な状態で繋いでいたら「設定は合っているのに届かない」で長く悩むところでした。二重に掛けた対策が、互いを打ち消すことがあります。

    正しく弾けているかは、画面では分からない

    繋いだあと、4つ試しました。

    送ったもの 期待 結果
    普通に書いて送る 通る 通った
    開いた直後に送る 断る 断られた
    アドレスの形が違う 断る 断られた
    見えない欄を埋めて送る 捨てる 「送信しました」と出た

    最後のものは、わざとそうしています。機械に「その欄を避ければ通る」と分からせないためです。

    そのぶん、画面を見ているだけでは、弾けたのか通ったのかが区別できません。確認するには受信箱を見て、届いていないことを確かめる必要があります。実際に届いていたのは4件のうち1件だけでした。

    「何も起きないこと」を確かめる手順を用意しておく

    正しく動いた証拠が「画面に出ない」「メールが来ない」という形をしている場合、確認の手順を別に決めておかないと、確認そのものを忘れます。

    今回の学び

    まず、設定を保存した時点では、まだ何も変わっていないことがあるということ。読まれるのがページを作るときなら、作り直すまでは古いままです。保存できたことと、効いていることは別でした。

    次に、隠すと、自分からも見えなくなること。訪問者に見せたくない値を隠すのは正しい判断ですが、その値を次に必要とするのは自分かもしれません。保存する前に控えるかどうかを決めておく必要がありました。

    そして、対策は二重に掛けると打ち消し合うことがあること。自分の側で弾いているのに相手の側でも弾いていると、通したいものが通りません。どこで何を見ているかを1回並べて書き出すのが、結局早い道でした。

  • 勉強法(6) 効いたのは、技術そのものではなかった

    勉強法(6) 効いたのは、技術そのものではなかった

    この連載の最後に、身も蓋もない話を書きます。

    仕事につながった場面を振り返ると、効いていたのは技術そのものではありませんでした。

    この記事で扱うこと

    実際に役に立った3つ。どれも技術の名前ではなく、技術の扱い方の話です。このサイトで実際にやったことと対応させて書きます。

    1. 説明できること

    「なぜそうしたのか」を聞かれて答えられるかどうか。ここでいちばん差が出ました。

    動くものを作れる人は多い。ところが「その方法を選んだ理由」になると、急に答えが揃わなくなります。

    このサイトで言えば、同意の仕組みがそうでした。先に読み込んでおいて送信の可否だけ切り替える方法と、同意まで読み込まない方法がある。後者を選んだ理由は、前者だと自分のコードを全部読んでも「送っていない」と言い切れないからです。

    説明できる形を選ぶ、という選び方がある

    この判断は、技術的な優劣で決めていません。あとで説明を求められたときに答えられるかで決めています。

    仕事だと、この視点が要る場面のほうが多い。「動きます」で終わらせられる相手ばかりではありません。

    2. 引き継げること

    自分で書いていないコードを直す仕事が続いた時期があります。書いた人はもういない。仕様書もない。あるのは動いているコードだけ。

    そのとき欲しかったのは、常に判断の理由でした。

    コードから分かること コードから分からないこと
    何をしているか なぜその方法を選んだか
    どういう順番で動くか なぜこの順番でなければならないか
    この値を除外していること 除外しないと何が起きるのか

    右の列が分からないと、触っていいかどうかが判断できません。一見無駄に見える処理が、過去の不具合への対処だったということは珍しくない。理由が書いていないと、消して初めて分かります。

    いま自分が書くコードには、処理の説明ではなく理由を書いています。引き継ぐ側に回った経験が、そのまま書き方に効きました。

    3. 止まらないこと

    3つめは、壊れたときにどうなるかを先に決めておくことです。

    このサイトのCMSは共用サーバーにあって、混むと応答しません。最初はそれで公開作業ごと止まっていました。

    直したのは速度ではなく、失敗する前提で書き直したことです。

    場面 決めておいたこと
    CMSが500を返す 間を置いて投げ直す。ただし要求が違う場合は投げ直さない
    記事が取れない 0件のフィードを返す。エラーにしない
    同意の記録が読めない 「同意していない」扱いにする

    どれも「うまくいく道」ではなく、うまくいかなかったときにどちらへ倒れるかの話です。

    止まらない作りは、能力ではなく事前の判断

    特別な技術は使っていません。分からないときにどちらへ倒すかを、書く時点で決めただけです。

    決めていないと、そのときの都合で倒れます。都合で倒れたものは、あとから説明できません。

    3つに共通していたこと

    並べてみると、全部「あとから来る人」への態度でした。

    誰のためか
    説明できること 判断を求める相手
    引き継げること 次に触る人
    止まらないこと 壊れたときに困る人

    3つとも、書いている自分には直接の得がありません。書いた時点では、誰も困っていないからです。

    効いてくるのは、いつも後からでした。そして後から効いたものだけが、次の仕事につながっていました。

    技術が要らない、という話ではない

    誤解のないように書いておくと、土台としての技術は要ります。読めないコードは引き継げないし、仕組みを知らなければ壊れ方も予想できません。

    ただ、そこは足りていないと何も始まらないが、足りたからといって差がつく場所ではないという感覚です。差がついていたのは、同じものを作れる人どうしの間でした。

    今回の学び

    まず、動かせる人は多いということ。差がついていたのは動くかどうかではなく、なぜそうしたかを説明できるかどうかでした。

    次に、引き継ぐ側の経験が、書き方を変えること。読めないコードに困った経験がないと、理由を残す動機が湧きません。困った側に一度回ると、書き方が変わります。

    そして、効いてくるのは全部あとからだということ。3つとも、書いた時点では誰も困っていません。得が見えないうちにやっておいたものだけが、後で効きました。

  • あと34本で、記事が消え始める作りだった

    あと34本で、記事が消え始める作りだった

    サイト全体を見直していて、あと34本で記事が消え始める作りになっていることに気づきました。

    エラーは出ません。ビルドも通ります。ただ、書いた記事が一覧にもサイトマップにも出なくなります。

    この記事で扱うこと

    件数の上限を置いたまま忘れると何が起きるか。なぜ気づけないのか。そして上限を上げるのではなく、外した話です。

    1行だけの問題だった

    CMSへの問い合わせに、こう書いてありました。

    posts(first: 100) {

    「最初の100本を取ってくる」という意味です。書いた当時は記事が10本もなく、100は十分に大きい数でした。

    いま66本あります。残り34本。

    101本目に何が起きるか

    ここが厄介なところです。何も起きません。

    確かめる場所 101本目を書いたあと
    CMSの管理画面 記事はある。公開済みになっている
    サイトの表示 正常。崩れない
    ビルド 成功する
    エラーの通知 来ない
    一覧・サイトマップ その記事だけ載らない

    気づく手段が、「あれ、あの記事どこへ行った」と思うことだけです。しかも消えるのは一番古い記事か新しい記事かも、並び順次第で変わります。

    上限を書いた本人は、上限のことを覚えていない

    この行を書いたのは半年近く前で、そのときは「100もあれば当分平気」と思って書いています。実際そのとおりでした。

    問題は、平気な期間が長いことです。長く問題が出ないと、その数字があること自体を忘れます。そして忘れたころに到達します。

    上げるか、外すか

    いちばん簡単なのは、100を500にすることです。1文字の修正で済みます。

    ただ、それだと同じ壊れ方が先に延びるだけです。500本書いたときに、同じように何も知らせずに消え始めます。そのときの自分は、いまより確実に忘れています。

    そこで、区切りそのものを外しました。

    posts(first: 100, after: 前回の続きの位置) {
      pageInfo { hasNextPage endCursor }
      ...
    }

    100本ずつ取って、「まだ続きがある」と言われているあいだは取りに行き続けます。記事が何本になっても、全部取れます。

    念のための打ち切りは、別の理由で置いた

    実は上限を1つ残しています。30回まで、という繰り返しの回数です。

    これは記事数のためではありません。相手が壊れていて「まだある」と言い続けたときに、公開作業が終わらなくなるのを防ぐためです。100本ずつなので、ここに達するのは3000本を超えてからになります。

    同じ「上限」でも、普通に使っていて到達するものと、異常時にしか到達しないものは別ものです。

    同じ数字が、もう1か所にあった

    直したあと、他にも同じ書き方が無いか探しました。サイトマップを作る処理にも、同じ100が入っていました。

    こちらのほうが影響は大きい。一覧に出ないだけなら内部リンクからは辿れますが、サイトマップから漏れると検索エンジンに存在を知らせる手段が減ります。

    両方とも同じ形に直しました。

    気づけたのは、数えたから

    この問題は、使っていても出てきません。見つかったのは、サイト全体を機械的に確かめていたときでした。

    やったのは単純で、数を数えることです。記事が何本あるか、サイトマップに何件載っているか、一覧に何枚並んでいるか。

    数えたもの 分かったこと
    記事の本数(66) 上限100まであと34本
    一覧のHTMLの大きさ 202KB。ほとんどが画面外のカード
    代替テキストの有無 62枚中21枚が未設定で、混ざっていた

    どれも見た目には出ていません。数えないかぎり、全部「問題なし」に見えます。

    今回の学び

    まず、「当分平気な数字」は、平気なあいだに忘れられるということ。書いた時点では正しい判断でしたが、正しさが長く続くほど、その存在を思い出すきっかけが無くなります。

    次に、上限を上げるのは、同じ壊れ方を先送りにするだけだということ。500にしても、500本目に同じことが起きます。そのときの自分が覚えている見込みは、いまより低い。

    そして、使っていて気づけない問題は、数えるしかないこと。表示は正常、ビルドも成功、エラーも無し。この状態で問題を見つける手段は、動かすことではなく数えることでした。

  • 勉強法(5) 手順は残らない。判断は残る

    勉強法(5) 手順は残らない。判断は残る

    このサイトの作業は、かなりの部分をAIと一緒に進めています。

    気になっていたのは、それで自分に何か残るのかということでした。60本を超えた時点で、残ったものと残らなかったものがはっきり分かれています。

    この記事で扱うこと

    何が残って何が残らなかったか。そして、残るかどうかを分けていたのが何だったかです。

    残らなかったもの

    正直に書くと、手順は覚えていません。

    やったこと いま書けるか
    DNSにTXTレコードを足す 画面を見れば分かるが、手順は覚えていない
    構造化データの書き方 項目名を覚えていない
    日付を決まった形式に直す 曜日と月名の並びは調べ直す

    ここは開き直っています。調べれば出るものを覚えておく理由が薄いからです。AIと組む前から、この手のものは毎回調べていました。

    残ったもの

    残ったのは、「なぜそれを選んだか」のほうでした。

    たとえば、同意を取る仕組みを作ったとき。先に読み込んでおいて送信の可否だけ切り替える方法と、同意まで読み込まない方法があります。後者を選びました。

    理由は、前者だと自分のコードを全部読んでも「送っていない」と言い切れないからです。判断が相手の設定に移ってしまう。

    この判断は覚えています。手順は忘れても、何を基準に選んだかは残りました。

    残っているのは、迷った場所だけ

    並べてみると、覚えているのは全部迷った場所でした。

    すんなり決まったところは覚えていません。2つの案を比べて、片方を捨てた場所だけが残っています。捨てた理由が、そのまま基準になっているからだと思います。

    分かれ目は「確かめられるかどうか」だった

    残るものと残らないものを分けていたのは、時間でも回数でもありませんでした。自分で確かめたかどうかです。

    分かりやすい失敗があります。

    読了の判定が正しく動いているかを、ブラウザを自動で動かして確かめました。記事を最後までスクロールさせても、読了として記録されない。「不具合がある」と結論を出しかけました。

    念のためスクロールのイベント回数を数えたら、0回でした。自動で動かしているタブでは、そもそもイベントが発生していません。不具合が無くても同じ結果になります。

    確かめずに直していたら、動いているものを壊していた

    ここで分かったのは「読了が記録されない」ではなく、「この方法では確かめられない」ということだけでした。

    AIと組んでいると、答えは早く出ます。早く出た答えを確かめる手段を持っていないと、間違いも同じ速さで進みます。速いぶん、戻る距離も伸びます。

    この一件は強く残りました。自分で数えて、自分で結論を取り下げたからです。

    残す作業を、意識的にやっている

    放っておくと残らないので、2つだけ決めています。

    1つめは、コードに「なぜ」を書くこと。処理の説明ではなく、その選択をした理由と、選ばなかった案を書きます。書こうとすると、自分が理由を説明できるかが分かります。書けないときは、分かっていません。

    2つめは、記事にすること。このサイトの記事は、ほぼ全部が詰まった話です。人に読める形にするには、何が起きて、なぜそうしたかを順に並べる必要があります。並べられない部分が、理解の穴です。

    やらないと やると
    コードの「なぜ」 半年後に同じ場所で悩む 判断ごと残る
    記事にする やった記憶だけ残る 説明できない部分が見つかる

    どちらも、AIには代わりにやってもらえません。説明できるかどうかを確かめているのは自分の理解なので、代わりに書いてもらった時点で意味が無くなります。

    向き不向きはある

    全部が同じようにうまくいったわけではありません。

    手を動かして覚えるもの——たとえば攻撃の手順を実際に試すような学び方は、読んだだけでは半分しか入りませんでした。体で覚える部分は、代わりにやってもらうと何も残りません。

    逆に、調べれば分かることを大量に集めてくる作業は、任せたほうが早い。分ける基準は「自分の判断が要るかどうか」でした。

    今回の学び

    まず、手順は残らないが、判断は残るということ。覚えているのは全部、2つの案を比べて片方を捨てた場所でした。捨てた理由が基準として残っています。

    次に、確かめる手段を持っていないと、間違いも同じ速さで進むこと。早く答えが出ること自体は良いのですが、確かめずに進むと戻る距離が伸びます。速さの価値は、確かめ方とセットでした。

    そして、残す作業だけは自分でやる必要があること。理由を書く、記事にする。どちらも「説明できるか」を試す作業なので、代わりにやってもらうと試すもの自体が無くなります。

  • Criticalが4件並んだ日にやったこと

    Criticalが4件並んだ日にやったこと

    朝、通知を見たらCriticalが4件並んでいました。どれも同じ相手、Next.js本体です。

    この日にやったことを順に書きます。結論から言うと、上げました。ただ、上げる前にやったことのほうが本題です。

    この記事で扱うこと

    脆弱性の通知が来たときに、何をどの順で確かめたか。自分のサイトが実際に影響を受けるのかを切り分ける手順と、それでも上げた理由です。

    まず、中身を読む

    4件は、実は2種類でした。同じものが2つのファイルで検出されているので、件数が倍に見えていました。使っているものを書いた一覧と、実際に入った版を固定した一覧の2つです。

    種類 内容
    A Windowsで動かしているサーバーでの、認証不要の遠隔コード実行
    B 画像最適化の機能で、AVIF形式を扱ったときの遠隔コード実行

    どちらも「認証不要」で「遠隔からコードを実行される」。言葉としては、これ以上ないくらい重い部類です。

    Aは、条件に当てはまらなかった

    Aの説明には、条件がはっきり書いてありました。WindowsのファイルシステムでNext.jsのサーバーが動いている場合に成立する、と。

    そして、LinuxとmacOSは影響を受けないと明記されていました。

    このサイトの本番はVercelで動いています。Linuxです。開発しているのもMacです。どちらも対象外でした。

    「Critical」は自分への影響度ではない

    重大度は、その脆弱性が最悪の場合にどれだけひどいかを表しています。自分の環境で成立するかどうかは、また別です。

    色と数字だけ見て慌てると、条件を読まずに動くことになります。読めば1分でした。

    Bは、入口の広さを自分で確かめた

    Bのほうは、条件が「AVIF形式の画像を最適化したとき」です。攻撃者が用意した画像を最適化させられると成立します。

    このサイトは画像の最適化機能を使っています。ここで確かめるのは、「誰でも好きな画像を最適化させられる状態か」です。

    設定を見ると、最適化を許可している場所が2つだけ指定してありました。

    remotePatterns: [
      { hostname: "localhost", port: "8080", ... },
      { hostname: "cms.kisaku.site",
        pathname: "/wp/wp-content/uploads/**" },
    ]

    つまり、CMSの画像置き場にあるファイルしか最適化できません。任意のURLを渡しても弾かれます。

    そこに置くには、CMSにログインして画像をアップロードする必要があります。入口はかなり狭いと言えます。

    「狭い」は「無い」ではない

    ここで止めたくなりますが、狭いだけで塞がってはいません。

    CMSのログインが破られれば成立します。そしてCMSは共用サーバー上のWordPressで、攻撃対象としてはありふれた構成です。「入口が狭いから大丈夫」は、入口が破られない前提に乗っています。

    結局、上げた

    Aは対象外、Bは入口が狭い。それでも上げました。

    理由は単純で、上げない理由のほうが薄かったからです。

    上げる 上げない
    作業量 修正版への更新1回 0
    壊れる可能性 細かい版の更新なので低い —
    放置した場合 — 入口が破られた時点で成立する

    大きな作り替えを伴う更新なら、慎重になる理由があります。今回は末尾の数字が1つ上がるだけの修正版で、その中身も「問題のある機能を止める」というものでした。

    本番に入るものと、そうでないものを分けて数える

    更新したあと、残りを確かめました。ここで1つ、見方を変えています。

    そのまま数えると12件残っていました。ところが「本番に配るものだけ」で数え直すと0件です。

    npm audit --omit=dev
    found 0 vulnerabilities

    残っていた12件は、全部開発中にしか動かない道具のものでした。型を生成するツールが使っている、さらにその中のライブラリです。

    配られないものは、訪問者に届かない

    これらはビルドのときに自分のマシンで動くだけで、配信されるファイルには入りません。訪問者のブラウザにも、サーバーにも届きません。

    0件にはしたいのですが、直すには型生成ツールを大きく更新する必要があり、そちらのほうが壊れる可能性が高い。今回は見送って、理由を書き残しました。

    数字を0にすることが目的になると、危険度の低いもののために動いているものを壊します。分けて数えると、どちらを急ぐかが決まります。

    やったことをまとめると

    順番 やったこと かかった時間
    1 4件が実は2種類だと確認 1分
    2 条件を読んで、対象外を1つ外した 2分
    3 残りの入口の広さを設定で確かめた 5分
    4 更新して、型検査を通した 5分
    5 本番に入るものだけで数え直した 1分

    手を動かしたのは4だけで、残りは読んで確かめる時間でした。

    今回の学び

    まず、重大度は「最悪の場合」の話であって、自分への影響ではないということ。条件が書いてあるので、読めば当てはまるかどうかが分かります。今回は片方が最初から対象外でした。

    次に、「入口が狭い」で止めないこと。狭いのは事実でしたが、狭さはこちらの設定に依存しています。設定を変えた日に広がるかもしれません。狭いことを理由に放置すると、その理由を覚えている人がいなくなった時点で危なくなります。

    そして、数える範囲を先に決めること。全部で12件と、配信されるものの中で0件では、判断がまったく変わります。件数を減らすこと自体を目的にすると、動いているものを壊しにいくことになります。

  • 生成した画像に、undefinedと書いてあった

    生成した画像に、undefinedと書いてあった

    記事のアイキャッチは、自分で書いたスクリプトで作っています。タイトルと一言、それに題材に合わせた線画を1枚に組んで、画像として書き出す仕組みです。

    連載を続けるために作った5枚を見返したとき、そのうち1枚に「undefined」と書いてありました。

    この記事で扱うこと

    生成した画像に文字化けのような欠損が入っていたのに気づかなかった話です。なぜ止まらなかったのか、なぜ見落としたのか、そして次から止まるようにした変更を書きます。

    線画の代わりに、文字が出ていた

    本来そこには、その回の題材を表す薄い線画が入ります。地図、盾、矢印、電波。背景に溶ける程度の濃さで置いてあります。

    1枚だけ、その位置に undefined という薄い文字が入っていました。

    回 入るはずだったもの 実際
    第1回 地図 地図
    第2回 目次を表す線 undefined
    第3回 盾 盾
    第4回 矢印 矢印
    第5回 電波 電波

    なぜ止まらなかったか

    生成の仕組みは、記事ごとの設定(題名・一言・図柄の名前)を受け取って、HTMLの文字列を組み立てます。その文字列をブラウザに読ませて、写真を撮るように書き出しています。

    図柄は、名前から一覧を引いて取り出します。第2回だけ、その引き当てに失敗していました。

    無いものを文字列に混ぜても、そこでは止まらない

    一覧に無い名前を引くと、返ってくるのは「無い」という値です。

    それをそのまま文字列に埋め込むと、「undefined」という7文字の文字として混ざります。組み立ては成功し、ブラウザも普通に描き、画像も無事に書き出されます。

    どこにも失敗がないので、どこにも警告が出ません。

    なぜ見落としたか

    理由は3つ重なっていました。

    理由 内容
    薄い 背景に溶ける濃さで置いている。意識して見ないと読めない
    1枚だけ 5枚並べて見れば違和感が出るが、1枚ずつ確認していた
    まだ公開前 予約投稿だったので、SNSのカードにも一覧にもまだ出ていない

    気づいたのは、続きの回を作るために元のデザインを見に行ったときでした。同じ見た目に揃えるつもりで拡大したら、書いてありました。

    作った直後には気づかず、別の目的で見返したときに見つかっています。出力を最後にちゃんと見ていなかったということです。

    欠けたら止まるようにした

    直した内容は2つです。

    ひとつは、その1枚を作り直したこと。もうひとつは、生成する前に図柄の指定があるかを確かめて、無ければその場で止めるようにしたことです。

    if (!MARKS[a.mark]) throw new Error('図柄の指定が無い: ' + a.file);
    

    1行ですが、これがあれば今回は画像が書き出される前に止まっていました。

    見て気づく仕組みには、限界がある

    「次から気をつける」は、次に同じことが起きたときの対策になりません。今回も気をつけていなかったわけではなく、薄い文字が1枚だけ混ざっている状態が、気をつけて見つかる範囲の外にありました。

    気をつける代わりに、止まるようにしておく。そのほうが確実でした。

    公開前でよかった、で終わらせない

    今回は予約投稿の段階で見つかったので、公開された画像に混ざることはありませんでした。

    ただ、これはたまたま続きを作る用事があっただけです。無ければそのまま公開され、SNSで共有されるたびにその文字が一緒に流れていました。アイキャッチは、記事の中で一番先に人の目に触れる部分です。

    見つかった理由が偶然である場合、同じ偶然は次に起きないものとして扱ったほうがよいと考えています。

    今回の学び

    まず、欠けた値は、文字として混ざるということ。文字列を組み立てる処理は、中身が欠けていても組み立てには成功します。成功したという事実は、中身が正しいことを何も保証していませんでした。

    次に、1枚ずつ見ると、揃っていないことに気づけないこと。並べて比べれば違和感として出ますが、順番に見ると「そういうもの」として通り過ぎます。

    そして、気をつけるより、止まるようにすること。1行の確認を足すだけで、次からは書き出される前に分かります。注意力に任せていた部分を、仕組みに移し替えられました。

  • 勉強法(3) 調べる場所が決まらない時間

    勉強法(3) 調べる場所が決まらない時間

    手が止まっている時間の中身を振り返ると、書き方が分からない時間はそれほど長くありませんでした。

    長かったのは、どこを調べればいいか決まらない時間のほうです。

    この記事で扱うこと

    「読めない」「分からない」を、調べられる形まで分解する話です。CMSベンダーにいたころの詰まり方と、いまも同じ構造で出てくることを書きます。

    3つのどれかは分かるのに、どれかが分からない

    製品のベースがRailsの会社にいました。仕事は管理画面やフロントの改修です。

    コードは目の前にあって、動いてもいます。それなのに読めない。理由を探していくと、こういう詰まり方でした。

    目の前の1行が、Railsが用意した仕組みなのか、Rubyの文法なのか、製品独自の実装なのかが区別できない。

    その行が何か 調べに行く先
    フレームワークの機能 そのバージョンのドキュメント
    言語の文法 言語のリファレンス
    製品独自の実装 社内のコードと、書いた人

    3つのどれかであることは分かります。どれなのかが分からない。結果として、調べ始める場所が決まりませんでした。

    検索の言葉すら決まらない

    調べるという行為は、探すものの見当が付いていて初めて成立します。

    見当が付いていないと、当てずっぽうで言葉を変えながら探すことになります。これが時間を食う。しかも、進んでいる実感が無いまま食います。

    「読めない」では、対処が決まらない

    この状態を「経験が足りない」で片付けていたうちは、何をすればいいか分かりませんでした。

    分解して「言語かフレームワークかの区別がつかない」まで持っていくと、読むべき本は1冊に決まります。フレームワークの本ではなく、言語そのものの本です。

    読んだあと、知識の量よりも先に変わったのは手が止まる回数でした。「ここは言語の書き方だ」と分かると、調べ先が決まるからです。

    分解の粒度は、対処が1つに決まるところまで

    「分からない」では何も決まりません。「読めない」でもまだ決まりません。

    「区別がつかない」まで下ろすと、やることが決まります。そこが止め時でした。それ以上細かくしても、やることは変わりません。

    いまも、同じ構造で出てくる

    これは過去の話ではなくて、今も形を変えて出てきます。

    最近だと、記事を投稿しようとしてサーバーに弾かれました。403が返るだけで理由は出ません。ここで手が止まる理由は、Railsのときとまったく同じです。原因がどこにあるか分からないので、調べ始める場所が決まらない。

    このときやったのは、本文を半分に割って試すことでした。知識を足したのではなく、「どこ」を絞り込んだだけです。2回割ったら、引っかかっている行が分かりました。

    Railsのとき 403のとき
    止まった理由 調べ先が決まらない 調べ先が決まらない
    やったこと 3つのどれかを見分ける 範囲を半分に割る
    必要だったもの 言語の知識 絞り込む手順

    片方は知識で解決し、もう片方は手順で解決しました。共通していたのは、詰まりを「どこ」の問題に置き換えたことです。

    教えていて気づいたこと

    プログラミングを教えていると、同じ止まり方をよく見ます。

    「動きません」と言われて画面を見ると、エラーは出ています。読めば書いてある。ただ、そのエラーが自分の書いた行の話なのか、道具の設定の話なのかが分からない状態です。

    このとき答えを教えると、その場は進みます。ただ次に同じ形で止まります。渡すべきだったのは答えではなく、「いまのは3つのうちどれだと思うか」という問いのほうでした。

    今回の学び

    まず、いちばん時間を食うのは、探す時間ではなく探す場所を決められない時間だということ。答えを見つけるのは、場所さえ決まればそれほどかかりません。

    次に、分解は「対処が1つに決まる」ところまでやること。「分からない」のままでは動けませんが、細かくしすぎても意味がありません。やることが決まった時点が止め時でした。

    そして、同じ詰まり方は、分野を変えて何度も出てくること。言語とフレームワークの区別も、403の原因の特定も、困っている形は同じでした。1回分解できると、次はそれが型として使えます。

  • 勉強法(2) 測らないと、分かったことにならない

    勉強法(2) 測らないと、分かったことにならない

    「分かった」と思った直後に、間違っていたことが何度かありました。

    いちばん効いたのは、自分の理解を外から確かめる手順を持つことでした。今回はその話です。

    この記事で扱うこと

    このサイトで実際にやった「確かめ方」を3つ。どれも、確かめていなかったら間違ったまま進んでいたものです。

    1. 数字を出したら、始まりと終わりを見る

    記事の読み進み具合を出すバーを作りました。動いています。そう思っていたら、開いた瞬間の数字が目に入りました。

    ページを開いた瞬間に9.5%を指していました。まだ何もしていません。

    調べると、計算そのものは正しかった。画面の下端を基準にしていたので、開いた時点で本文の一部がすでに映っていたのです。「画面に映った本文の割合」としては9.5%が正確な値でした。

    式が答えている問いと、表示が名乗っている問いが違った

    表示が名乗っていたのは「読み進み具合」です。読み進み具合なら、読み始める前は0のはずでした。

    計算間違いではなく、意味のずれです。数字が合っているかを確かめても見つかりません。「0から始まっているか」「最後で100になるか」という、両端を見る確かめ方でしか出てきませんでした。

    2. 試した環境が、確かめたい条件を満たしているか

    同じ読了判定で、もっと危ない間違いをしました。

    ブラウザを自動で動かして、記事を最後までスクロールさせました。読了として記録されません。「記録されない不具合がある」と結論を出しかけました。

    念のため、スクロールのイベントが何回起きたかを数えました。

    発火回数: 0

    自動で動かしているタブでは、スクロールのイベントがそもそも発生していませんでした。不具合が無くても、同じ結果になります。

    結論を取り下げるほうが、直すより先だった

    この時点で分かったのは「読了が記録されない」ではなく、「この方法では確かめられない」ということだけです。

    ここで直しにかかっていたら、動いているものを壊していました。試した環境が条件を満たしているかは、結果より先に確かめる必要がありました。

    そのあとは、ブラウザで試すのをやめて、実際の寸法を使って計算で確かめました。

    3. 原因が分からないときは、半分に割る

    コードを含む記事を投稿しようとしたら、サーバーに弾かれました。403が返るだけで、理由は何も出ません。

    本文は4500字あります。どこが原因かは分かりません。

    そこで、本文を4つに割って1つずつ試しました。

    試した範囲 結果
    1/4ずつ(4回) 2番目と3番目が弾かれる
    そのうち1つを8分割 2か所に絞れた
    怪しい行を単体で 2つの書き方に特定

    最終的に、引っかかっていたのは2つの書き方だけだと分かりました。理由を推測せずに済んだのは、割って試したからです。

    推測で当てにいくと、外れたことにも気づけない

    最初は「コードの記号が多いからだろう」と考えて、記号を別の表記に置き換えました。それでも弾かれました。

    置き換えが効かなかったこと自体が情報で、相手が表記を戻してから見ていると分かりました。推測が外れたと分かったのは、確かめる手順があったからです。

    確かめ方は、覚えるものではなかった

    3つ並べてみると、覚えている知識はほとんど出てきません。

    場面 やったこと
    数字を出した 両端の値を見た
    試したら失敗した 試し方が成立しているか数えた
    原因が分からない 範囲を半分に割った

    どれも技術というより手順です。そして3つとも、自分の理解を疑うところから始まっています。

    今回の学び

    まず、「動いた」は「合っている」ではないということ。進捗バーは最初から最後まで動いていました。動きを見ているかぎり、9.5%から始まっていることには気づけません。

    次に、確かめた結果より先に、確かめ方を確かめること。0回しか起きていないイベントを待っていたら、どんな実装でも同じ結果になります。出た結果を信じる前に、その試し方で違いが出るのかを見る必要がありました。

    そして、分からないときは推測ではなく分割すること。4500字から2つの書き方まで絞るのに、知識は要りませんでした。要ったのは、半分に割って試す根気だけでした。