ことの発端
とあるテスト中の顧客から、「ファイルのインポートができない」と問い合わせがありました。
インポート処理は顧客のサーバに配置されたgzipファイルをダウンロードしてからインポートするというもの。
エラーメッセージを見ると、gzip以外の形式で圧縮されているファイル
とのことで、
これは、内部でgzipファイルを展開する際にZlib::GzipFile::Error
が出た場合に出るメッセージです。
しかし、実際に配置されているファイルをcurlでダウンロードすると普通にgzipファイルでした。fileコマンドの結果もちゃんとgzipです。
gzip展開するコードにも特に変な箇所は見当たりません。ただ単にextractを実行しているだけのコードです。
def extract(content)
FileIo::Archive::Gzip.extract(str: content)
なんでgzip展開でエラーが出ているのかわからない......
おかしな点に気づく
顧客ファイルをブラウザ上で閲覧すると(テスト時だったので、公開しても特に問題ないファイルを使用していました)、平文で中身が見えてしまいます。
gzip圧縮されたファイルならばバイナリ列が表示されるはずではないのでしょうか?
まるで、ブラウザで表示するとき、勝手にgzip圧縮を展開しているかのような......
まさか本当に?
調べてみると、以下のようなページがヒットしました。
要約すると、
- アップロードする際に、レスポンスヘッダに
Content-Encoding: gzip
を付与するように設定されている - ダウンロード側がHttp Compression有効
という二つの条件が噛み合うと、「拡張子が.gzのままなのに展開済みのファイルがダウンロードされる」のです🥶!!
HTTP Compressionについては、Net::HTTPを使用している場合はバージョン2.0以降は必ず有効になっているそうです。また、Chromeとかのメジャーなブラウザは有効みたいです。
実際、弊社のシステムではHTTPダウンロードをFaradayで実装しており、
デフォルトであるNet::HTTPアダプターを使用していました。
解決編
さて、この問題を解決する方法を考えなくてはなりません。
いくつか案を挙げてみました。
1.HTTP Compressionを無効化してダウンロードする
Accept-Encodingが設定されていない場合にNet::HTTPが自動でgzipファイルをAcceptするよう設定してしまうのが原因なので、
Accept-Encodingをidentityで上書きしてしまえばよいかと考えました。
この設定でダウンロードしたところ、見事gzipファイルとして保存されました。
問題点
この方法だと、元々のaccept-encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3
を上書きするので、仮にZlibのdeflateを受け付けている場合にはおそらく不具合になります。
弊システムでは、gzipを推奨しているものの、圧縮方式を縛っていませんでした。他の顧客に影響が出てしまいますね......
2.gzip展開に失敗したら「実は展開済みかどうか」を確認する
考えてみれば、展開後のファイルをダウンロードしたとて、ファイルの中身がインポート可能なのであれば問題がありません。
なので、いったん展開処理にかけてみて、エラーが出た場合に「拡張子.gz
だけど実は生のインポート可能ファイルですか?」という確認を入れることにします。
これならば、今までの処理を変えないで後ろに処理を付け足す形になるので、これまで上手くいっていた顧客には影響がありません。
ということで、今回は2.の方法を採用しました。
ちなみに
展開前にファイル形式を判定すればよいのでは?とも思ったのですが、
それでは結局「gzip圧縮したファイルを渡しているのにgzipファイルではないとエラーになる!」という問い合わせ自体は解決できません。
ついでに、他のうまくいっている顧客もすべて通るルートになってしまうため、影響が出てしまう可能性があります。
というわけで没になりました。
まとめ
-
Content-Encoding: gzip
が付与されているとgzip圧縮が展開された状態でダウンロードされる場合がある - 特例の顧客に対応するための変更は、他の顧客が通るルートとは別に処理を付け足す形での修正を検討しよう
まとめるとたった2行になってしまいましたが、僕自身が答えに辿り着くまでは結構時間がかかったので、
どこかの誰かの助けになればいいなと思って書かせていただきました。
それではまた!