結論!
blob作り直すだけでいけたわ!!!
はじめに
Pythonのgoogle-cloud-storage
ライブラリを使って、Google Cloud Storage (GCS)上のファイルを更新する処理を書いていたところ、奇妙なエラーに遭遇しました。
**「新規作成は成功するのに、更新(再アップロード or 削除→再アップロード(ほぼ新規作成))は100%失敗する」**という現象です。
エラー内容は、多くの人が一度は見たことがあるであろうCRC32C mismatch
。一般的にはネットワークの不安定さが原因とされていますが、今回はどうも様子が違いました。AIアシスタントとの長いデバッグの末にたどり着いた(geminiはずっと嘘ついてました)、意外な原因と解決策を共有します。
現象:更新時だけ発生するCRC32C
エラー
やりたかったことは、GCS上にある日記ファイル(diary.txt
)に新しい内容を追記することです。初めはfirebaseの同じ名前のファイルをアップロードすると上書きされる機能をあてにして、そのままアップロードしていましたが、失敗。次は「既存ファイルを削除し、新しい内容で再作成する」というロジックで実装していました。
しかし、2回目以降の書き込み(更新処理)で、必ず以下のエラーが発生しました。
google.api_core.exceptions.BadRequest: 400 POST https://... {
"error": {
"code": 400,
"message": "Provided CRC32C "by6lCg==" doesn't match calculated CRC32C "AJqWgg==".",
...
}
}
不思議なことに、ファイルが存在しない状態での新規作成は一度も失敗しませんでした。この完璧な再現性が、単なるネットワークエラーではないと確信するきっかけとなりました。
問題のコード(再現コード)
当初、問題が起きていたコードのロジックは以下のようなものでした。blob
オブジェクトを一度取得すれば、削除からアップロードまで使い回せるだろう、と考えていました。
import asyncio
from datetime import datetime
from google.cloud import storage, exceptions
class FirebaseManager:
# (初期化処理などは省略)
async def append_log_to_storage(self, log_entry: str):
file_path = f"Users/{self.user_hash}/{datetime.now().strftime('%Y-%m-%d')}/diary.txt"
# 1. blobオブジェクトを取得
blob = self.bucket.blob(file_path)
# (既存内容の読み込み処理)
# ... existing_content = blob.download_as_text() ...
updated_content = existing_content.strip() + "\n" + log_entry
# 2. 既存ファイルを削除
if blob.exists():
blob.delete()
print("削除が完了しました。")
# 3. 同じblobオブジェクトを使って新しい内容をアップロード
# ▼▼▼ この処理がCRC32Cエラーで失敗する ▼▼▼
blob.upload_from_string(updated_content)
print("アップロードが完了しました。")
犯人探しの旅
仮説1:ネットワーク経路の問題(AIの回答)
CRC32Cエラーの最も一般的な原因は、通信経路上でのデータ破損です。AIアシスタントも、当初はこの線で原因を分析していました。不安定なWi-Fiやルーターの問題など、環境要因が強く疑われました。
しかし、「なぜ更新時だけ100%失敗するのか?」という疑問が残ります。
仮説2:blobオブジェクトの内部状態(私の仮説)
ネットワークの問題にしては再現性が高すぎる(100%)ため、私はコード、特にblobオブジェクトの扱いに問題があるのではないかと考えました。
「もしかして、blob.delete()を実行した後のblobオブジェクトは、内部的に何か古い情報を保持していて、それが次のアップロードを妨害しているのではないか?」
この仮説を検証するため、コードを少しだけ変更してみました。
解決策:blobオブジェクトの再生成
仮説を検証するための修正は、delete()とupload_from_string()の間で、もう一度blobオブジェクトを生成し直すという、ただそれだけでした。
import asyncio
from datetime import datetime
from google.cloud import storage, exceptions
class FirebaseManager:
# (初期化処理などは省略)
async def append_log_to_storage(self, log_entry: str):
file_path = f"Users/{self.user_hash}/{datetime.now().strftime('%Y-%m-%d')}/diary.txt"
# 1. blobオブジェクトを取得
blob = self.bucket.blob(file_path)
# (既存内容の読み込み処理)
# ... existing_content = blob.download_as_text() ...
updated_content = existing_content.strip() + "\n" + log_entry
# 2. 既存ファイルを削除
if blob.exists():
blob.delete()
print("削除が完了しました。")
# ▼▼▼▼▼【解決策】blobオブジェクトを再生成する ▼▼▼▼▼
print("blobを再設定します")
blob = self.bucket.blob(file_path)
# ▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲
# 3. 新しいblobオブジェクトを使ってアップロード
blob.upload_from_string(updated_content)
print("アップロードが完了しました。")
結果、あれだけ失敗し続けていた更新処理が、一発で成功するようになりました。
なぜ解決したのか?(考察)
この現象から、google-cloud-storageライブラリのblobオブジェクトは、以下のようなステートフル(状態を持つ)な挙動をするのではないかと推測されます。
最初にblobオブジェクトを取得した際、既存のファイルのメタデータ(世代IDなど)を内部的に保持する。
blob.delete()を実行すると、サーバー上のファイルは削除されるが、手元のblobオブジェクトには古いファイルのメタデータの残骸が残ってしまう。
その同じblobオブジェクトでupload_from_string()を実行すると、新しいデータと一緒に古いファイルのメタデータの残骸も送信してしまう。
サーバー側は、「存在しないはずの古いファイルに対する更新リクエスト」としてこの矛盾した情報を受け取り、リクエストをBadRequest (400)として拒否する。その際のエラーが、たまたまCRC32C mismatchとして現れていた。
blobを再生成することで、古い情報を持たないクリーンなオブジェクトが作成され、サーバーに「新規作成」として正しく認識された、というのが真相かな?といった感じです。
まとめ
GCSでCRC32C mismatchエラーに遭遇したら、まずはネットワークを疑うのが定石。
しかし、特定の操作(今回で言えば「更新」)でのみ100%エラーが再現する場合は、コード側の問題を疑う価値がある。
Pythonのgoogle-cloud-storageライブラリでは、ファイルをdelete()した後に同じパスにupload()する場合、blobオブジェクトを再生成するのが安全かもしれない。
デバッグでは自分の直感を信じることも大事。
この記事が、同じような不可解なエラーに悩む誰かの助けになれば幸いです。