静的出力で数百のロケーション+サービスページと、数千のプロフィールページを持つ、二言語(フランス語/アラビア語)のマーケットプレイスサイトをAstroで運用しています。各プロフィールページにはFirebase Storageから写真が取得されます。特別なことは何もありません。ある日、突然本番ビルドが失敗しました。ローカル開発やテスト、リントでは何も問題は起きていませんでした。npm run buildは非ゼロで終了し、スタックトレースはastro:assetsを指していました。
原因を調べたところ、あるプロフィールの写真URLが無効になっていました。Firebase Storageのダウンロードトークンが更新されたため、以前はJPEGを返していたリクエストが403を返すようになっていました。それだけです。数千件のうち1件の無効なURLが、ビルド全体を止めてしまったのです。
驚いたのは、Astroの画像パイプラインがリモート画像の不正に対して警告をログに残しスキップして続行すると思っていたことです。しかし、そうではありませんでした。理由と対処法を以下に示します。
AstroのコンポーネントとgetImage()関数は、どちらもImage Serviceに処理を委ねます。Image Serviceの仕事はtransform()で画像を変換(リサイズ、フォーマット変換など)することです。この部分はフックしてカスタマイズ可能です。
問題は、transform()が受け取るのはすでにフェッチ済みの生バッファのみで、リモートsrcに対する実際のネットワークリクエストはAstro内部のloadRemoteImageステップで行われる点です。このステップは公開されているImage Service APIの一部ではなく、リモートURLからバッファ取得までの間に割り込んで失敗をハンドルする公式のフックは存在しません。
したがって、フレームワークから見れば、リモート画像に対する403やタイムアウトは「優雅に処理する」状況ではなく、静的生成中の致命的エラーになります。データセット内の画像URLが1つでも無効なら、ビルド全体が停止します。
2分の応急処置(十分ではない理由)
最も手っ取り早い方法は、を通常のタグに置き換えることです。これにより、Astroのビルド時フェッチ&変換ステップが完全にスキップされ、通常のはURLをそのまま出力し、ブラウザが実行時に処理します。
これでデプロイは即座に復旧しましたが、トレードオフであり根本解決ではありません。自動リサイズ、WebP/AVIF変換、レスポンシブsrcsetといったを使う本来の利点が失われます。つなぎとしては有効ですが、恒久的な解決策ではありません。
本当の修正:ビルド中ではなくビルド前にチェックする
根本的な問題は、Astroが失敗をうまく扱えないことではなく、検証していないURLを渡していたことにあります。そこで、検証をAstroが画像に触れる前のprebuildステップに移動しました。
スクリプトはシンプルです。ページが参照するすべてのリモート画像URLを収集し、タイムアウト付きでHEADリクエストを行い、失敗したURLをJSONマニフェストに書き出します。
js
// scripts/validate-image-urls.mjs
import fs from "node:fs";
const TIMEOUT_MS = 8000;
const CONCURRENCY = 10;
async function checkUrl(url) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
try {
const res = await fetch(url, { method: "HEAD", signal: controller.signal });
return res.ok;
} catch {
return false; // タイムアウト、DNS失敗、接続リセットなどはすべて「不良」とみなす
} finally {
clearTimeout(timer);
}
}
コード自体よりも2つの設計判断が重要でした。
URL単位ではfail closed、全体としてはfail open。単一のURLがタイムアウトまたはエラーになった場合は不良とみなします。ただし、70%以上のURLが同時に失敗した場合は、チェッカー自体が壊れている(CIランナーオフライン、DNS障害、レート制限など)と判断し、既存のマニフェストを上書きせずそのままにします。検証ステップでの部分的なネットワーク障害が本番コンテンツ全体を破壊しないようにするためです。
Astro integrationではなくnpmのprebuildでフックする。npmのライフサイクル(package.jsonの"prebuild": "node scripts/validate-image-urls.mjs")を通じて自動実行されるようにしました。Astro integrationとして書くことは意図的に避けました。シンプルでAstroの内部ビルド順序から完全に分離でき、フレームワーク移行時にも影響を受けないためです。
コンポーネント側では1行のチェックで済みます。
astro
import { isBadImageUrl } from "../utils/badImageUrls";
const showFallback = isBadImageUrl(profile.thumbnail);
{showFallback
? /* イニシャルアバターを描画する代わりに / null
: / profile.thumbnailをソースとする最適化済みImageコンポーネントを描画 */ null}
既知の不良URLはに到達する前に、優雅なフォールバック(イニシャルアバター)に置き換えられます。それ以外は通常どおり最適化パイプラインを通ります。
実際に大規模で機能するか?
最初の本番実行では、数千件のユニークな画像URLを並列処理で数秒でチェックし、意図したとおりに機能しました。本当に無効なURLをいくつか検出し、残りは untouched のまま、ビルドがすべての画像ホストの稼働率に依存しなくなりました。
この作業で効果的だった小さな習慣:バリデータが導入された後、緊急用のフォールバックをに戻す際に、自分の記憶を頼りにするのではなくgit履歴(git show HEAD:path/to/file)と差分を取りました。時間的制約の中で緊急修正を元に戻す際には安価な保険になります。
要点
ユーザーのアップロード、CMS、クラウドストレージなど、完全に制御できないリモートソースでAstroのやgetImage()を使用する場合、不正なURLが優雅に失敗するとは想定しないでください。フェッチはフックできないパイプライン内部で行われるため、設計上そうなりません。ビルド中ではなく、ビルド開始前に検証してください。
私たちはモロッコとエジプト向けの二言語ホームサービスマーケットプレイス「bricoleapp.com」でこれを運用しています。役立つ部分があればさらに詳しくお答えします。
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.