「確認済み」の印が付いていたのに、リンク先を一度も読めていなかった:t.co の中継ページ
トレカダッシュ 開発の記録 / 2026年10月
iOS アプリ「トレカダッシュ」は、お店の X(旧 Twitter)の告知から、トレカの抽選販売の情報を集めています。告知には、応募用のフォームへのリンクが付いています。アプリは、そのリンク先も読みに行って、締切の日時や、もう受付が終わっていないかを確かめています。
ある日、「締切の日時が違う」という報告が来ました。調べると、締切の日時どころではなく、リンク先のページを、一度も読めていませんでした。しかも、読めていないのに、「確認済み」の印が付いていました。
この記事は、2026 年 10 月 16 日の作業の記録(調査の内容と、数字)から書いています。
症状:閉じたフォームが、受付中のまま出ていた
報告された抽選は、ゲームショップの「30th CELEBRATION カードセット 9 種」の X の告知でした。応募先の Google フォームを開くと、「回答の受け付けは終了しました」と出ます。ところが、アプリでは、この抽選が表示中のままで、締切の日時も空でした。
おかしいのは、この記事に「受付が終わっていないか確認した」という印(日時)が付いていたことです。確認したのに、終わっていることに気づかなかった。確認が、中身を見ていなかったことになります。
原因:t.co は、ブラウザのふりをすると、別のページを返す
X の投稿に貼られたリンクは、t.co という短縮 URL を通ります。普通は、開くと、本来のリンク先へ転送されます(HTTP の 301)。
ところが、ブラウザの User-Agent(「私は Chrome です」という名乗り)を付けて開くと、転送されず、JavaScript で飛ばす、約 500 バイトの中継ページが返ってきます。アプリの読み取り処理は、2 か所とも、ブラウザの名乗りを付けて開いて、「転送に従う」設定にしていました。その結果、次のことが起きていました。
- 転送されないので、最後の URL は、
t.coのままです。 - 読んだ本文は、応募フォームではなく、中継ページの HTML でした。
- つまり、応募フォームの中身を、一度も読んでいませんでした。
ブラウザの名乗りを足したのは、8 月 6 日のことでした。今回の不具合は、その変更の副作用でした。
実際に、どれくらい読めていなかったか
報告された 1 件だけの問題かを確かめるために、表示中の X の抽選・予約の記事のうち、t.co のリンクを持つものを全部調べました。
| 調べたこと | 結果 |
|---|---|
t.co を持つ、表示中の記事 | 220 件 |
| そのうち、中継ページが返ってきた記事 | 220 件(全部) |
| そのうち、実は閉じたフォームなのに、表示中だった記事 | 9 件 |
9 件は、複数の店にまたがっていました。1 つの店の問題ではなく、仕組みの問題です。
もう 1 つ、見逃せない手がかりがありました。毎日動いている、「締切が空の記事を直す」処理が、「対象 126 件、更新 0 件」を、毎日繰り返していたことです。対象が 126 件あるのに、直せたのが 0 件。これは、「直すものが無かった」のではなく、「読めていないから、直せなかった」の結果でした。
直し方
転送先を調べる処理を、fetchTcoTarget という 1 つの関数にまとめました。手順は、こうです。
- 名乗りを付けずに、
t.coを開く。転送には従わず(redirect: "manual")、最初の転送先だけを受け取る。 - 転送先が返ってきたら、それを使う。
- 中継ページが返ってきたら、その中の自動転送の指定(
<meta http-equiv="refresh">)を読んで、本当の転送先を取り出す。 - その転送先を、今度はブラウザの名乗りを付けて開く。
名乗りを付けるか付けないかで、返ってくるものが違う。だから、「短縮 URL を解く」ときは名乗りなし、「本来のページを読む」ときはブラウザの名乗りあり、と使い分けました。同じ失敗を繰り返さないよう、テストも足しています。
すでに付いていた、「確認済み」の印をどうするか
直しただけでは、足りません。誤って付いた「確認済み」の印が、記事に残っているからです。印がある記事は、「もう確認した」と見なされて、再確認の対象から外れます。コードを直しても、既存の記事は直りません。
そこで、条件に合う 195 件(X の抽選・予約で、投稿へのリンクを持ち、t.co を持つもの)から、印を外しました。そのうえで、受付終了の確認と、締切の再抽出を、手で実行しました。
| 処理 | 直す前 | 直した後 |
|---|---|---|
| 締切の再抽出(対象になった件数/更新できた件数) | 対象 126 件・更新 0 件(毎日) | 対象 123 件・更新 14 件 |
同じ日に、受付終了の確認の頻度も見直しました。締切の無い記事は 6 時間おき(締切のある記事は 24 時間おき)、1 回に見る件数は 40 件から 80 件、1 回の上限は 7 分です。t.co が読めるようになったので、閉じたフォームを丸 1 日出し続けないようにするためです。
分かったこと
「確認した」の印は、読めたときだけ付ける。
- 印は、成功したときだけ書く。読めなかったのに「確認した」と書くと、直した後も、その記事は二度と見直されません。
- 外部のページを読んだら、目的のページかを確かめる。最後の URL が、短縮 URL のままではないか。本文が、期待した種類のページか。
- 「更新 0 件」が続くのは、異常かもしれない。対象があるのに、毎日 0 件。「ない」のか「できない」のかを、区別します。
- 通信の設定を変えたら、それを使う全部の通信を見直す。8 月に足したブラウザの名乗りが、2 か月後に、
t.coの読み取りを壊していました。
この話に出てくるもの
- トレカダッシュ — トレカの抽選・予約・先着の情報を集める iPhone アプリ
- 「取れた」と読んでいたのは、ただの案内ページだった — 同じ、「取れたことにしていた」失敗の記録
- 確認できなかったときに、通していた — 「確認できなかった」の扱いで起きた、別の失敗
ほかの記録
- Claude API から Gemini API(gemini-3.1-flash-lite)へ移行した:16 か所の呼び出しを 1 つの関数にまとめた — Anthropic の利用上限で予定外の課金が出たため Gemini へ移行。モデルの選び方(廃止予定日)、呼び出しの一本化、Web 検索の回数上限が無い問題への回路遮断器、止めるスイッチの記録。
- Xcode 15.2 で Firebase 12 が使えない(Swift 6.0 必須):Firebase 11.4.0 と AdMob 11.5.0 に固定した理由 — 古い Mac の Xcode 15.2 では、Firebase 12 以降と 11.12.0 以降は Xcode 16.2 が必須、AdMob 11.6.0 以降は MarketplaceKit で Xcode 15.3 が必須。Exact Version で固定した記録。
- ITMS-91064「無効なバイナリ」:NSPrivacyTracking が true なのに NSPrivacyTrackingDomains が空だった — ビルドが Apple の自動審査で ITMS-91064 により却下された。NSPrivacyTracking が true で、NSPrivacyTrackingDomains が空の配列だったのが原因。ドメインを補った経緯と、確認できていないこと。
コメント
感想や、記事の間違いの指摘は、ここへ。書くには、GitHub のアカウントが要ります。
押すと、giscus(giscus.app)と GitHub に接続します。押さなければ、接続しません。詳しくはあつかいに書いています。