🌦️

空世界 〜HTMLの永遠仕様探訪記、或いは、文字なきsrcにまつわる寥々たる考察について〜

2024/07/12に公開

問題

<img src=""> をブラウザで表示した時、どうなるか知っていますか?わざわざimg要素のsrc属性を空文字列にする機会がないので意外と知らないかもしれません。

もちろん画像は表示されず、(指定していれば)altが表示されます。

src属性が空文字列のimg要素を表示しようとした画面のスクリーンショット。壊れた画像のアイコンとaltテキスト「srcが空だよ」が代わりに表示されている

img要素のsrc属性を空文字列にすると、リンク切れになることがわかりました!いかがでしたか?(?)

そのときHTMLImageElementは

JavaScriptでsrcが空文字列のimg要素のDOMインスタンスを確認してみましょう。例として https://zenn.dev/stin を開き、Chrome開発者ツールを使ってsrc属性に空文字列を指定したimg要素を埋め込んでおきます。

そして次のJavaScriptを実行します。

const element = document.querySelector('img[src=""]');
console.log(element.src);

直前のコードブロックを実行した結果、「https://zenn.dev/stin」と表示されていることを示したスクリーンショット

なんとそのHTMLを取得するURLが表示されました。

空文字列だったはずなのになぜこのような挙動になるのでしょうか。これに疑問を感じ、どのように仕様が定義されているのか調べるため、HTML Living Standardを探ったお話です。

HTML仕様書を開く

HTML仕様書のimg要素について記載されているページは次です。

https://html.spec.whatwg.org/multipage/embedded-content.html#the-img-element

MDNで探している項目の説明ページを開くと仕様書へのリンクが貼ってあるのでたどり着きやすいです。

src属性とは

先程のリンクを開いて少し下にスクロールした箇所に求めていそうな記述がありました。

The src attribute must be present, and must contain a valid non-empty URL potentially surrounded by spaces referencing a non-interactive, optionally animated, image resource that is neither paged nor scripted.

(DeepL訳、一部筆者により修正。以下翻訳は同様)
src属性は存在しなければならず、ページングもスクリプトもない、非インタラクティブでアニメーションも可能な画像リソースを参照する、valid non-empty URL potentially surrounded by spaces を含む必要があります。

URLが指すコンテンツの性質はさておき、srcはvalidでnon-emptyなURLである必要があるとのこと。空文字列である時点でnon-emptyを満たしていません。オワタ…。

空文字列はURLと言えるのか

non-emptyは一旦見なかったことにして、空文字列はURLと言えるのかを追っていくことにしました。先程の引用の valid non-empty URL potentially surrounded by spaces から順番に用語定義をたどります。

するとvalid non-empty URL potentially surrounded by spacesとは、valid URL stringであることがわかります。

さらにリンクをたどるとHTML仕様書からURL仕様書(URL Standard)に移ります。それによれば、valid URL stringとはrelative-URL-with-fragment string か absolute-URL-with-fragment string のどちらかであるとのこと。

absolute-URL-with-fragment stringはおそらくURLと聞いて思い浮かぶ形式のことですね。定義的には少なくともURL-scheme stringとコロン(:)を持つ必要があるようです。http:とかwss:から始まっている文字列をabsolute-URL stringとみなすということですね。つまり空文字列はabsolute-URL stringではないことがわかりました。

ではrelative-URL-with-fragment stringはどうでしょうか。relativeというからにはベースになるURL(base URL)があり、そのscheme次第で満たすべき内容が変わるようです。img要素のsrcのbase URLが、それを埋め込んでいるHTMLドキュメントのURLであることは一般的に知られているので、http or httpsのケース(file以外のspecial schemeのケース)だけを考えてよいでしょう。base URLのschemeがhttp or httpsのとき、relative-URL stringが取り得る形態は次の3つです。

scheme-relative-special-URL string//が先頭に付く必要があるので空文字列は満たしません。path-absolute-URL string/が先頭に付く必要があるのでこれも空文字列は満たしません。最後の path-relative-scheme-less-URL stringゼロ個以上の URL-path-segment string で先頭スラッシュを含まないということで、URL-path-segment string がゼロでも良い、つまり空文字列でもpath-relative-scheme-less-URL stringを満たします。

ということで、base URLが指定されていれば空文字列はURLと言えますね。

base URLは本当にHTMLのURLなのか

先程は「img要素のsrcのbase URLが、それを埋め込んでいるHTMLドキュメントのURLであることは一般的に知られている」と書きましたが、それは本当なのでしょうか。それが記載されている箇所を探します。

srcvalid non-empty URL potentially surrounded by spaces であると定義されていました。このリンクをたどった先に、HTML標準におけるParsing URLsについて記載がありました(URLのパースはURL標準の範疇ですが、HTMLではさらにbase URLとエンコーディングが絡むため追加で定義しているとのこと)。そこでは、base URL について

Let baseURL be environment's base URL, if environment is a Document object; otherwise environment's API base URL.

(訳)

環境が Document オブジェクトの場合は baseURL を環境のベース URL とし、そうでない場合は環境の API ベース URL とする。

と書かれています。HTMLページはDocumentでしょう。Documentの base URL には、href属性を持つbase要素がなければ(かつ普通のHTMLページなら)ドキュメントのURLが採用され、そうでなければ最初のbase要素のhrefが採用されるとのことでした。

ということで、base要素が存在しなければ、src属性のbase URLはHTMLドキュメントのURLであることは本当だとわかりました。

また、「空文字列はURLである」と「srcのbase URLはHTMLドキュメントのURLである」の2点から、<img src=""> をブラウザで表示したときHTMLImageElementのsrcプロパティがHTMLドキュメントのURLを指していることがわかり、最初に提示した挙動の説明が付きましたね。

non-emptyのこと忘れてないか?

src属性は valid non-empty URL potentially surrounded by spaces でなければなりません。non-emptyとあるように、そもそも空文字列は仕様違反です。しかし違反と言えどその状態にできてしまうのがHTMLです。src属性を空文字列にした場合どうなるかを調べます。

HTMLImageElementで画像がダウンロードされたかどうかを確認できる complete プロパティの挙動について記載がありました。

the srcset attribute is omitted and the src attribute's value is the empty string;
then return true.

(訳)
srcset属性が省略され、src属性の値が空文字列の場合、trueを返す

completeプロパティはsrc属性が空文字列のときは強制的にtrueになるようですね。

試すとわかりますが、<img src="">をHTMLに埋め込んでも、element.srcはHTMLのURLを指すものの、実際にリクエストが飛ぶわけではありません。Updating the image data という節を見ると、実際に画像を取得する流れが書いてありました。そこには

  1. If selected source is null, then:
    (略)

    Return.

(略)

  1. Fetch the Image.

とあり、seleceted sourceがnullならば、26. Fetch the Image に到達することなく処理を終了することがわかります。

seleceted sourceを決めるための Selecting an image source というフローもあり、source setがemptyのときにnullを返すそう。source set は画像要素が内部で持っている画像ソースのOrdered Set(コレクション)のようです。source setがemptyとなるかを確認するために Updating the source set を見てみると、

If el is an img element that has a src attribute, then set default source to that attribute's value.
(略)
Let el's source set be the result of creating a source set given default sourcesrcset, and sizes, and img.

(訳)
el が src 属性を持つ img 要素の場合、default source をその属性の値に設定します。
(略)
el のsource set を、default sourcesrcsetsizesimg が与えられた creating a source set の結果とします。

とのことで、src属性はdefault sourceとして扱われます。なので、default sourceに空文字列が入っている前提で Creating a source set from attributes を見てみます(srcset, sizes は考えません)。

If default source is not the empty string and source set does not contain an image source with a pixel density descriptor value of 1, and no image source with a width descriptor, append default source to source set.

(訳)
default source が空文字列ではなく、source set にピクセル密度記述子値が 1 の画像ソースがなく、幅記述子を持つ画像ソースもない場合、source set に default source を追加します。

「default source が空文字列ではなく」という条件から、生成されたsource setには空文字列のdefault sourceは渡されないことがわかりました。

つまり遡ると、img要素のsrc属性が空文字列の場合、source setはemptyになり、selected sourceはnullとなり、画像取得リクエストが送信されないことがわかりました。

なぜこんなことを調べたのか

html-to-imageというライブラリを使ってちょっとした開発をしていました。このライブラリはブラウザ上でDOMを画像として保存する処理を提供しています。

https://github.com/bubkoo/html-to-image

このライブラリは対象のDOM内部にimg要素を見つけると、そのHTMLImageElementのsrcプロパティを取り出して、そのURLに対してfetchを実行します。下のGitHubコードブロックはその該当箇所です。

https://github.com/bubkoo/html-to-image/blob/128dc3edfde95d6ac636f2756630f5cbd6f7c8df/src/dataurl.ts#L15-L38

コードを見ると、エラー扱いするのはfetch結果が404 Not Foundのときだけです。

しかし僕のアプリケーション都合で、画像化したいDOMに<img src="">が含まれていました。仕様書を読み漁ってわかったように、src属性が空文字列のときはHTMLImageElementのsrcプロパティはそのHTMLのURLになります。つまりfetchが200 OKになってしまうのです。

その結果、画像データが含まれているはずのblobにhtml文字列が混入し、後続の処理の途中に予期せぬエラーで失敗していました。

エラー内容もよくわからず、調査に時間を費やしました。その調査の過程で、srcが空文字列の場合はHTML URLを指していることに気づき、仕様を調べることにしたのです。

まとめ

  • img要素のsrc属性を空文字列にすると、そのHTMLImageElementのsrcプロパティはHTMLのURLを指す
  • HTML仕様ではsrc属性を空文字列にするのは仕様違反
  • 空文字列自体はbase URLがあればvalid URL stringとみなせる
  • html-to-image使うときは仕様違反のimg要素に気をつけて

仕様違反のコードは書かないように気をつけよう。仕様書読むのめちゃ疲れた。

それでは良いHTMLライフを。この両の手が死に届くその日まで。

GitHubで編集を提案
chot Inc. tech blog

Discussion