IEでaタグのhrefを空にすると親ディレクトリへ強制リロードされる問題|onclick内エラー時の仕様外挙動と回避策

概要

今回は、Internet Explorer(IE11)で遭遇した、aタグのクリックで突然起きる謎の強制リロード現象について紹介していきます。

業務システムでaタグをボタン代わりに使う実装を調査していたら、あるボタンをクリックした瞬間に画面が突然リロードされ、しかも遷移先がなぜか親ディレクトリになっているという奇妙な現象に行き当たりました。
ChromeやEdge(Chromium)では一切再現せず、コードのどこにも「親ディレクトリへ飛べ」なんて記述はないのに、IE11でだけ起きるんです。

調べていくと、href属性の仕様とonclick内のちょっとした書き方が、思いがけない形で絡み合っていました。
正体がわかった瞬間、思わず「そこ!?」と声が出ました(^^

それではやっていきましょう!

目次

発生した現象

まず、遭遇した現象から振り返ります。

項目 内容
ブラウザ Internet Explorer 11(EdgeのIEモードでも同様)
現象 aタグクリック時に意図しない親ディレクトリへのリロード
影響 画面状態の消失・フォーム入力のリセット・二重リクエストなど

具体的には、以下のようなコードでボタンをクリックすると発生します。

1
<a id="ja_btn" href="" onclick="window.callWindow('AAA',);">ボタン</a>

onclickの中身をよく見るとwindow.callWindow('AAA',)という末尾カンマ付きの呼び出しになっていて、これがIEでは構文エラーになります。
このエラーによってonclick内の処理が中断し、return falseevent.preventDefault()に到達しないまま、aタグ本来のデフォルト動作(href属性の解決)が実行されてしまう、という流れです。

再現コードと再現手順

実際に手元で確認したところ、href属性の書き方によらず同じ現象が発生しました。

ケース1: href属性なし

1
<a id="ja_btn" onclick="window.callWindow('AAA',);">ボタン</a>

ケース2: href=””(空文字)を指定

1
<a id="ja_btn" href="" onclick="window.callWindow('AAA',);">ボタン</a>

ケース3: 属性の記述順序を変えた場合

1
<a id="ja_btn" onclick="window.callWindow('AAA',);" href="">ボタン</a>

再現手順は次の通りです。

  1. 上記コードを含むページをIEで開く(例: http://example.com/app/list/hoge.do)
  2. ボタン(#ja_btn)をクリックする
  3. onclick内のJavaScriptが構文エラーまたは実行時エラーで中断する
  4. IEがデフォルト動作(href属性の解決)を実行する
  5. カレントディレクトリ(http://example.com/app/list/)へリロードされる

3つのケースはいずれも同じ現象になります。
hrefの有無や記述順序を変えても回避できないのがこのバグの厄介なところです。

仕様上の期待値とIEの実際の挙動

RFC3986やHTML Living Standardの仕様に基づくと、hrefの状態によって次のように解決されるはずです。

hrefの状態 仕様上の解決先
属性なし リンクとして扱わない(遷移しない)
href=”” 現在ドキュメントのURL(現在ページ)
href=”#” 現在ページ + フラグメント

ChromeやFirefox、Edge(Chromium)はこの仕様通りに動作します。
ところがIE11だけは違いました。

現在URLをhttp://example.com/app/list/hoge.doとした場合の、実際の遷移先を比較したのが次の表です。

hrefの状態 仕様上の遷移先 IE11の実際の遷移先 Edge(Chromium)の遷移先
属性なし (遷移しない) .../list/(親ディレクトリ) 遷移しない
href=”” .../list/hoge.do(現在ページ) .../list/(親ディレクトリ) .../list/hoge.do(現在ページ)
href=”#” .../list/hoge.do# .../list/hoge.do# .../list/hoge.do#
href=”javascript:void(0)” 遷移なし 遷移なし 遷移なし

href=””の行がまさに仕様違反の挙動で、現在ページに留まるはずが親ディレクトリへ飛ばされてしまいます。

なぜこんなことが起きるのか

原因を辿ると、大きく3つの要素が絡んでいることがわかりました。

HTMLパースとJavaScript実行は独立している

onclick属性内のJavaScriptは、HTMLパース時点では単なる文字列としてDOMに格納されているだけで、クリック時に初めて評価されます。
そのため、onclick内の構文エラーがhref属性の読み込み自体に影響することはありません。
あくまで「エラーで処理が止まり、return falseに到達できない」ことが問題の起点になります。

IE固有の挙動

IE11では、href=""(空文字)を「値なし」と同等に扱い、次のような順序でURL解決が進みます。

IE11がhref=""を親ディレクトリへ解決してしまう流れ
href属性が空文字の場合にIE11が値なしとして扱い、カレントディレクトリ基準で相対URL解決した結果、親ディレクトリへ強制リロードされるまでの4ステップの流れ図

この動作はRFC3986に反しており、他のモダンブラウザとは非互換な挙動です。

onclickエラーとの複合

onclick内でエラーが発生すると、return falseevent.preventDefault()が呼ばれないため、上記のhrefに基づくデフォルト動作がそのまま実行されます。
結果として、意図しない親ディレクトリへのリロードが発生する、という仕組みでした。

Edge(Chromium)との挙動比較

同じコードでも、Edge(Chromium)では次のような差があります。

項目 IE11 Edge(Chromium)
callWindow('AAA',)(末尾カンマ) 構文エラー 正常実行(ES2017以降で許容)
href属性なしのクリック 親ディレクトリへ遷移 遷移しない
href=””のクリック 親ディレクトリへ遷移(仕様違反) 現在ページへリロード(仕様通り)
エラー発生時の可視性 画面リロードで気付きやすい サイレント失敗で気付きにくい

ちなみに、Edge(Chromium)ではエラーが起きても画面が動かないぶん、逆にエラーに気付きにくいという側面もあります。
IEの挙動は仕様違反ではあるものの、結果的に「バグの存在に気付きやすい」という皮肉な一面もありました。

回避策・修正方法

原因がわかったので、ここからは実際に取れる対策を紹介していきます。

推奨: href=”javascript:void(0);”を明示的に指定

1
<a id="ja_btn" href="javascript:void(0);" onclick="window.callWindow('AAA');">ボタン</a>

IEでもEdgeでも一切遷移しなくなります。
あわせて末尾カンマなどの構文エラーも修正しておくと、onclickの実行そのものが保証されます。
一番手軽で即効性のある対策で、既存コードへの影響範囲も最小で済むので、まずはここから直すのが現実的だと思います(^^

最も堅牢: addEventListenerによる外部スクリプト化

1
2
3
4
5
6
7
8
9
10
11
<a id="ja_btn" href="javascript:void(0);">ボタン</a>
<script>
document.getElementById('ja_btn').addEventListener('click', function(e) {
e.preventDefault();
try {
window.callWindow('AAA');
} catch (err) {
console.error('callWindow実行エラー:', err);
}
});
</script>

preventDefault()で確実にデフォルト動作を抑止しつつ、try-catchでエラー発生時のリロードも防げます。
HTMLとJavaScriptを分離できるので、保守性の面でもおすすめです。
onclick属性への直接記述に比べると手間は増えますが、エラー処理を一箇所にまとめられるのが大きいところで、同じような<a>疑似ボタンが複数ある実装なら共通関数化してここに寄せるのがよいと思います。

非推奨: onclick末尾にreturn false;を追加

1
<a id="ja_btn" href="" onclick="window.callWindow('AAA'); return false;">ボタン</a>

一見手軽に見えて、実は一番危険な対策です。
callWindow内でエラーが発生するとreturn falseに到達できず、リロードを防げません。
「エラーが起きなければ動く」対策は、まさにエラーが起きたときに機能しないので本末転倒なんですよね。
エラー耐性がないため、この方法はおすすめしません。

根本解決: button typeへの置き換え

1
<button type="button" id="ja_btn" onclick="window.callWindow('AAA');">ボタン</button>

<button type="button">はデフォルトで遷移動作を持たないため、本問題は根本的に発生しません。
キーボード操作やスクリーンリーダー対応も自然とよくなるので、副次的な効果も大きい対策です。
そもそも<a>タグを疑似ボタンとして使うこと自体、アクセシビリティやセマンティクスの観点で気になるポイントでした。
新規実装であれば、最初から<button>を使うのが一番シンプルだと感じます。

影響範囲

正直なところ、この手のバグは今どき影響を受ける環境がかなり限られていると思います。
IE11自体がとっくにサポート終了していますし、そもそもの原因になった末尾カンマも今どきのビルド環境ならESLintあたりで弾かれるはずです(^^b

とはいえ、社内システムなどでEdgeのIEモードを使い続けているケースはまだ見かけます。
<a>を疑似ボタンにしてonclickでJSを呼ぶ実装や、hrefを空文字のままにしているレガシーコードが残っている場合は、画面状態の消失や二重送信につながるので、心当たりがあれば一度見直しておくと安心です。

まとめ

IE11には、href=""を「属性なし」と同等に扱い、親ディレクトリへリロードするバグ的挙動があります。
この挙動はHTML/URL仕様(RFC3986)に違反していますが、IEの長年の実装として存在し続けてきました。

onclick内のエラーでreturn falseに到達しない場合、このIEの挙動が発火し、意図しない親ディレクトリリロードが発生します。
Edge(Chromium)をはじめとするモダンブラウザは仕様通りに動作するため、この現象自体は発生しないはずです。

根本解決にはhref="javascript:void(0);"の指定、event.preventDefault()の使用、または<button>タグへの置き換えが有効です。

限定的とはいえ現実として自分はかなりここに苦戦したので、誰かのためになったら嬉しいです!

以上となります。
IEからの移行は非互換だらけで地獄を見ます(- -;
それではお疲れさまでした。