先日、iOS上でファイルを生成し、それをダウンロードしようとした際に、TypeError: load failed
というエラーが発生し、ダウンロードができないという問題に遭遇しました。この問題を Gemini と対話することで解決することができましたので、その過程と解決策を共有します。frontendはReact、backendはFlaskです。
解決に至った要因
解決の要因は大きく分けて以下の2点です。
- iOSにおける
blob:
URL の制限 - サーバー側での
Content-Disposition
ヘッダーの設定
1. iOSにおける blob:
URL の制限
iOSの Safari やその他の iOS ブラウザでは、JavaScript で生成した blob:
URL を直接ダウンロードリンクとして使用することができません。これは、iOS のセキュリティ上の制限によるものです。
blob:
URL は、ブラウザのメモリ上に一時的に作成される URL であり、ファイルの内容を直接参照します。しかし、iOS では、この blob:
URL を新しいタブで開いたり、<a download>
属性でダウンロードさせようとすると、セキュリティ上の理由から制限されます。
そのため、fetch
API で取得した blob データから URL.createObjectURL()
で blob:
URL を生成し、<a download>
でダウンロードを試みても、TypeError: Load failed
が発生してダウンロードに失敗します。
この問題を回避するために、今回は <form>
タグを使用する方法を取りました。
<form>
タグを使用した理由
<form>
タグを使用した主な理由は以下の2点です。
-
iOS におけるファイルアップロードの制約:
iOS の Safari や iOS ブラウザでは、JavaScript でFormData
オブジェクトを生成し、fetch
API を使ってファイルをアップロードする際に、いくつかの制約があります。特に、fetch
API で取得した blob データを直接ダウンロードさせることが難しいという問題がありました。 -
従来の
<form>
タグによるファイルアップロードの利点:
従来の<form>
タグを使用すると、ブラウザが自動的にmultipart/form-data
形式でリクエストを送信し、ファイルをサーバーにアップロードできます。この方法では、JavaScript で複雑な処理を行う必要がなく、ブラウザの標準機能を利用して簡単にファイルをアップロードできます。また、サーバー側では、従来のファイルアップロード処理と同様に、ファイルを受け取ることができます。
まとめ: iOS におけるファイルアップロードの制約を回避するために、従来の <form>
タグを使用し、ブラウザの標準機能を利用してファイルをアップロードする方法を選択しました。
2. サーバー側での Content-Disposition
ヘッダーの設定
iOS で blob:
URL を直接ダウンロードできない制限を回避するもう一つの重要な要素として、サーバー側での Content-Disposition
ヘッダーの設定があります。
Content-Disposition
ヘッダーは、HTTP レスポンスヘッダーの一つで、受信したコンテンツをブラウザがどのように処理すべきかを指示するために使用されます。主に、ブラウザにコンテンツをインラインで表示するか(例えば、HTML ページや画像)、添付ファイルとしてダウンロードさせるかを指定します。
この問題を回避するために、サーバー側で Content-Disposition
ヘッダーを設定し、ブラウザにダウンロードを強制する必要があります。
response.headers["Content-Disposition"] = "attachment; filename=processed_images.zip"
ここで、
-
attachment
: この部分で、ブラウザに対して「このレスポンスはファイルとしてダウンロードさせるべきである」と指示します。通常、ブラウザは受信したデータに基づいてどのように処理するかを決定しますが、attachment
を指定することで、常にダウンロードを試みるようになります。 -
filename=processed_images.zip
: この部分で、ダウンロードされるファイルのデフォルトのファイル名を指定します。この例では、processed_images.zip
というファイル名でダウンロードされます。ユーザーはダウンロード時にファイル名を変更することもできますが、特に指定がない限り、この名前が提案されます。
このように Content-Disposition: attachment; filename=...
というヘッダーをサーバーからのレスポンスに含めることで、iOS でも blob:
URL を直接扱うことなく、サーバーから送信されたデータを指定したファイル名でダウンロードさせることが可能になります。
まとめ: iOS で blob:
URL を直接ダウンロードできない制限を、サーバー側で Content-Disposition
ヘッダーを設定し、ブラウザにダウンロードを強制することで回避し、iOS でもファイルのダウンロードを可能にしました。