【JavaScript】input[type=”file”]にセットされているファイルが更新、削除された後にsubmitする際のブラウザ毎の挙動と問題あるsubmitの予防方法

 input 要素にユーザーが入力を行い、その input 要素の中身を送信することでユーザーが自由なデータをサーバーに送信するという手法がよくとられます。この input 要素ですがファイルも入力できます。このファイルが入力された後にローカルのファイルに更新、あるいは削除があった場合、どの様になるかを紹介します。

 手元で確認できた Windows の主要ブラウザの結果が次表です。submit は HTML としてフォームの送信を行ってページ遷移を伴う通信をした場合、axios は XHR を使ってページ遷移を伴わない通信をした場合です。
XMLHttpRequest – Web API | MDN

Google Chrome Firefox Microsoft Edge
更新後 submit 一度中身を参照した後に更新されるとERR_UPLOAD_FILE_CHANGED 発生。エラーページ 更新後のファイルを送信 Google Chromeと同じ
削除後 submit ERR_FILE_NOT_FOUND 発生。エラーページ 空フォームを送信 Google Chromeと同じ
更新後 axios 一度中身を参照した後に更新されるとnet::ERR_UPLOAD_FILE_CHANGED 発生。JavaScriptの例外 更新後のファイルを送信 Google Chromeと同じ
削除後 axios net::ERR_FILE_NOT_FOUND 発生。JavaScriptの例外 空フォームを送信 Google Chromeと同じ

 Chromium を共通で使っているだけあって Google Chrome と Microsoft Edge は同じ結果です。Firefox は異なります。なぜ異なるかというと恐らくブラウザの仕様として未定義で、思い思いに作っているからです(少なくとも自分は該当の仕様の記述を見つけられませんでした)。試していませんが Safari もまた違った挙動をするのではないでしょうか。
HTML Standard

 ブラウザは前述の様にファイルの更新と削除によって挙動を変えます。この機能が発動すると異常なリクエストを送信したり、ブラウザ組み込みのエラーページが表示されたりしてしまいます。その様な状態を避けるために、ローカルファイルの更新と削除をバリデーションとエラーメッセージで表現することによって、ユーザー体験をいい感じに保ちたくなります。上記の3ブラウザについては次のコードでこれを実現できます。

document.querySelector('#送信ボタン').addEventListener('click', async function () {
    // ファイルオブジェクトを取得
    const f = document.querySelector('input[type="file"]').files[0];
    // Chromium について更新、削除についてのエラーはファイルの中身参照で引き起こせるので、ファイルの中身を読み取る機能の成否でエラーの有無をチェックします
    // Firefox についてファイルが削除済みなら参照できずにエラーになります。更新済みならそのまま参照します。これによりファイルを送信できるならば true,できないならば false になります。
    const hasError = await f.slice(0, 1).text().then(() => false).catch(() => true)
    // エラーの有無で処理をハンドリング
    if (!hasError) {
        // ファイル送信処理
        console.log('ファイル送信処理')
    } else {
        // エラーメッセージを表示
        alert('ファイルの更新か削除が行われました。ファイルを再度選択してください');
    }
})

 ざっくばらんな作りですが大筋はこれでできます。後は肉付けするなりバリデーション部分だけ抜き出してどうこうするなりです。
追記:chromium の初期参照が削除状態だとサイズ 0 のファイルを読み取る様なのでその処理も必要でした

// 修正版
document.querySelector('#送信ボタン').addEventListener('click', async function () {
    // ファイルオブジェクトを取得
    const f = document.querySelector('input[type="file"]').files[0];
    if(f.size === 0 && Math.abs((new Date()).getTime() - f.lastModified) < 1000){
        // 空ファイルかつファイルの最終更新日時が現在日時から1秒以内なら読み取れなかったとする

        // FireFox ならファイルのメタ情報を input にセットされた時点で読み取っているので
        // セットした時点でファイルが空でなければこの if の中には入らない
	  
        // Edge, Chrome のメタ情報は最初のプロパティ参照時点でキャッシュされる
        // この時点でもし読み取り対象が存在しない場合、セットされるのは{size:0, lastModified: 現在日時}の File オブジェクト
        // このため Edge, Chrome で読み取り対象のファイルが存在しない場合はこの if の中に入る
        alert('ファイルの削除が行われました。ファイルを再度選択してください');
        return;
    }
    // Chromium について更新、削除についてのエラーはファイルの中身参照で引き起こせるので、ファイルの中身を読み取る機能の成否でエラーの有無をチェックします
    // Firefox についてファイルが削除済みなら参照できずにエラーになります。更新済みならそのまま参照します。これによりファイルを送信できるならば true,できないならば false になります。
    const hasError = await f.slice(0, 1).text().then(() => false).catch(() => true)
    // エラーの有無で処理をハンドリング
    if (!hasError) {
        // ファイル送信処理
        console.log('ファイル送信処理')
    } else {
        // エラーメッセージを表示
        alert('ファイルの更新か削除が行われました。ファイルを再度選択してください');
    }
})
>株式会社シーポイントラボ

株式会社シーポイントラボ

TEL:053-543-9889
営業時間:9:00~18:00(月〜金)
住所:〒432-8003
   静岡県浜松市中央区和地山3-1-7
   浜松イノベーションキューブ 315
※ご来社の際はインターホンで「316」をお呼びください

CTR IMG