Macアプリの公証(Notarization)を、丸一日がかりで通しました。証明書の種類を直し、Hardened Runtimeを有効にし、get-task-allow の注入を止め、タイムスタンプを付け、Appleの検査を通し、合格証を貼り付け、DMGを作り、そのDMGにも署名して公証して——。
別のノートPCで起動確認もしました。警告なし。完璧。
「せっかくだから、本番と同じ経路でも確認しておくか」
自分のサイトにDMGを置いて、ノートPCのSafariでダウンロードしてみました。
落ちてきたファイルが、これです。
electronic_bookkeeping_renamer_v1.2.2.man
.man。
.man って何
Unixのマニュアルページ(man ls とかで見るアレ)のファイル形式です。ディスクイメージとは縁もゆかりもありません。
サイズは26.4MBで元のDMGと同じなので、中身は壊れていない。名前だけがおかしい。
でも、これをダブルクリックしても何も起きません。受け取った人から見れば、「ダウンロードしたけど開けない壊れたファイル」です。
原因:サーバーが嘘をついていた
ヘッダを見たら一発でした。
curl -sI https://.../electronic_bookkeeping_renamer_v1.2.2.dmg | grep -i content-type
content-type: application/x-troff-man
サーバーが「これはmanページです」と宣言していました。Safariは素直なので、「サーバーがそう言うなら」と .man を付けて保存する。
.dmg という拡張子でURLを叩いているのに、.man で落ちてくる。HTMLの download 属性も付けていたのに、Content-Type の方が優先されたわけです。
サーバー側で .dmg のMIMEタイプが定義されていないと、こういうことが起きます。直し方は .htaccess に1行。
AddType application/x-apple-diskimage .dmg
「このファイルはmacOSのディスクイメージです」と正しく伝える。それだけ。
ここから、私は2回続けて外した
.htaccess をアップして、確認しました。
content-type: application/x-troff-man
変わらない。
推測1「レンタルサーバーのキャッシュだろう」
管理パネルで高速化機能をOFF→ONして、キャッシュを捨てさせました。変わらない。
推測2「配信経路が違うのでは」
静的ファイルは高速化機能が自前で返していて、.htaccess が読まれていないのでは——それらしい理屈をこねました。クエリ文字列を付けたURLだと正しいタイプが返ることも、その説を補強しているように見えました。
外れ。
そこで、ヘッダを全部見ました。
HTTP/2 200
content-type: application/x-troff-man
server: cloudflare ← !?
cf-cache-status: HIT ← !!
age: 952
cache-control: max-age=14400
Cloudflare。
前段にCDNがいました。レンタルサーバーのキャッシュをいくら消しても、そりゃ変わりません。見ていた場所が違った。
時系列がぴったり合いました。
- 12:31 DMGをアップし、最初にアクセス → この時点の誤ったヘッダをCloudflareが記憶
- 12:40
.htaccessをアップ(サーバー側は正しくなった) - 12:47 でもCloudflareは16分前の記憶を返し続けている(
age: 952)
Cloudflareのキャッシュをパージしたら、一発で直りました。
content-type: application/x-apple-diskimage
cf-cache-status: MISS
沈黙は「問題なし」ではない
いちばん怖いのはここです。
サーバーの設定は最初からこうでした。つまりv1.0.0 から v1.2.1 まで、全部同じ状態で配布していたことになります。
Safariで無料版を試そうとした人には、開けないファイルが届いていた可能性がある。
問い合わせは来ていませんでした。でもそれは、問題が無かったことを意味しません。「ダウンロードしたけど開けなかった」という人は、たいてい連絡してこないからです。 わざわざ開発者にメールを書いて「あなたのアプリが開けません」と教えてくれる人は、ごく一部の親切な人だけ。残りは黙って去る。
Macユーザーの多くはSafariを使います。私は、自分でも知らないうちに、いちばん多い層を取りこぼしていたかもしれません。
.htaccess の1行。それが今日のどの作業より売上に効くかもしれない、というのは皮肉な話です。公証には丸一日かけたのに。
なぜ見つかったか
本番と同じ経路で確認したからです。それだけ。
技術的には、公証が通った時点で「完了」でした。実際、AirDropで送ったDMGは完璧に動いていた。あそこで終わりにしていたら、この問題は見つかりませんでした。
「せっかくだから本番でも確認しておくか」と思ったのは、半分は気まぐれです。でも、その気まぐれが1ヶ月分の取りこぼしを見つけました。
学んだこと
推測する前にヘッダを見る。 私は2回外しました。どちらも「それらしい理屈」でした。curl -sI を全部表示していれば、1回目で server: cloudflare が目に入っていた。部分的に見ることは、見ていないことに近い。
自分のサイトのダウンロード、実際に落としたことがありますか。 個人開発でアプリやファイルを配っている人は、いま一度、購入者と同じ経路で試してみてください。URLを直接叩くのではなく、サイトのボタンから、普段使わないブラウザで。5分で終わります。
動いた ≠ 届いた。 ビルドが通ったこと、起動したこと、公証が通ったことは、ユーザーの手元で開けることを保証しません。最後の1メートルは、自分で歩いて確かめるしかない。
ちなみに調べる過程で、Cloudflareに「開発モード」があることを知りました。3時間だけキャッシュをバイパスするモード。サイトをいじる日は、最初にこれをONにしておけばいい。 今日の回り道の半分は、これで消えていました。
来月の自分のために書いておきます。
