背景
ライブラリ | バージョン |
---|---|
Rails | 7.1.1 |
google-cloud-tasks | 3.0.0 |
google-cloud-ruby/google-cloud-tasks at main · googleapis/google-cloud-ruby
↑ライブラリーを使って、Google Cloud Taskを作成しようとする時に以下のエラーが発生した。
def create_task(payload)
parent = @queue_path
task = {
http_request: {
http_method: 'POST',
url: @function_url,
headers: {
'Content-Type' => 'application/json'
},
body: payload,
oidc_token: {
service_account_email: @service_account_email,
audience: @function_url
}
}
}
@client.create_task(parent: parent, task: task)
rescue Google::Cloud::Error => e
Rails.logger.error("Failed to create cloud task: #{e.message}")
raise
end
エラー
[241740fc-a33b-48d7-b58f-3f3a682f376f] Error in extract action:
/usr/local/bundle/gems/gapic-common-0.25.0/lib/gapic/protobuf.rb:43:in `initialize':
U+FF1A from UTF-8 to ASCII-8BIT (Encoding::UndefinedConversionError)
from /usr/local/bundle/gems/gapic-common-0.25.0/lib/gapic/protobuf.rb:43:in `new'
from /usr/local/bundle/gems/gapic-common-0.25.0/lib/gapic/protobuf.rb:43:in `coerce'
from /usr/local/bundle/gems/gapic-common-0.25.0/lib/gapic/protobuf.rb:100:in `coerce_submessage'
from /usr/local/bundle/gems/gapic-common-0.25.0/lib/gapic/protobuf.rb:62:in `block in coerce_submessages'
from /usr/local/bundle/gems/gapic-common-0.25.0/lib/gapic/protobuf.rb:58:in `each'
from /usr/local/bundle/gems/gapic-common-0.25.0/lib/gapic/protobuf.rb:58:in `coerce_submessages'
from /usr/local/bundle/gems/gapic-common-0.25.0/lib/gapic/protobuf.rb:42:in `coerce'
from /usr/local/bundle/gems/gapic-common-0.25.0/lib/gapic/protobuf.rb:100:in `coerce_submessage'
from /usr/local/bundle/gems/gapic-common-0.25.0/lib/gapic/protobuf.rb:62:in `block in coerce_submessages'
from /usr/local/bundle/gems/gapic-common-0.25.0/lib/gapic/protobuf.rb:58:in `each'
from /usr/local/bundle/gems/gapic-common-0.25.0/lib/gapic/protobuf.rb:58:in `coerce_submessages'
from /usr/local/bundle/gems/gapic-common-0.25.0/lib/gapic/protobuf.rb:42:in `coerce'
from /usr/local/bundle/gems/google-cloud-tasks-v2-1.2.0/lib/google/cloud/tasks/v2/cloud_tasks/client.rb:1699:in `create_task'
from /app/app/services/gcp/cloud_tasks_service.rb:51:in `create_task'
調査
Rubyの特徴(欠点)として、専用のbinary data typeが存在しません。Rubyの基本データタイプは以下です:
データタイプ | 説明 |
---|---|
Integer | 整数 |
Float | 浮動小数点数 |
String | 文字列 |
Array | 配列 |
Hash | ハッシュ |
Symbol | シンボル |
TrueClass, FalseClass | 真偽値 |
NilClass | nil |
バイナリデータも文字列も同じStringクラスで表現されます。区別するために、encodingがASCII-8BIT
(別名BINARY
)のStringはバイナリデータとして扱われます。そのため、文字列をバイナリとして扱いたい場合は、encodingをASCII-8BIT
に変更する必要があります。
encodeとforce_encodingの違い
メソッド | 動作 | 例 | 特徴 |
---|---|---|---|
encode |
文字列を別のエンコーディングに変換し、バイト表現が変更される | "こんにちは".encode("EUC-JP") |
・変換できない文字があるとエラー ・新しいStringオブジェクトを返す |
force_encoding |
バイト表現はそのままで、エンコーディングラベルだけを変更 | "こんにちは".force_encoding("ASCII-8BIT") |
・実際の変換は行われない ・不適切な指定で文字化けの可能性 ・元のオブジェクトが変更される |
[1] pry(main)> h = {name: 'こんにちは!'}
=> {:name=>"こんにちは!"}
[2] pry(main)> JSON.dump(h).encoding
=> #<Encoding:UTF-8>
[3] pry(main)> JSON.dump(h).encode('UTF-8').class
=> String
# エンコーディングラベルを ASCII-8BIT に変更することで、バイナリデータとして出力されることを確認できます
[4] pry(main)> JSON.dump(h).force_encoding('ASCII-8BIT')
=> "{\"name\":\"\xE3\x81\x93\xE3\x82\x93\xE3\x81\xAB\xE3\x81\xA1\xE3\x81\xAF\xEF\xBC\x81\"}"
[5] pry(main)> JSON.dump(h).force_encoding('ASCII-8BIT').class
=> String
# エンコーディングラベルはASCII-8BITですが、中身は`UTF-8`のままです
[6] pry(main)> JSON.dump(h).force_encoding('ASCII-8BIT').encoding
=> #<Encoding:ASCII-8BIT>
Cloud TaskのHTTPリクエストのbodyはバイナリとして扱われるため、今回は変換ではなくラベル変更のforce_encoding
が適切な選択でした。
原因と解決策
原因
Cloud TaskのHTTP requestのbodyパラメータは、Google Protocol Buffersによってシリアライズされる際にバイナリデータ(ASCII-8BITエンコード)として扱われます。しかし、RubyのJSON.dump
やto_json
メソッドの結果はUTF-8エンコードのままです。
今回のケースでは、JSONデータ内に日本語文字(U+FF1A、全角コロン「:」)が含まれており、Protocol Buffersライブラリが暗黙的にUTF-8からASCII-8BITへの変換(つまり、encodeが呼び出されることになります)を試みた際にEncoding::UndefinedConversionError
が発生しました。これは、マルチバイト文字がASCII-8BITの範囲を超えるためです。
解決策
JSONシリアライズした後で、Protocol Buffersによる変換前に明示的にASCII-8BITエンコーディングに変更することで問題を解決しました:
# ここでJSONをシリアライズして、ASCII-8BITエンコーディングに明示的に変更
body = JSON.dump(payload).force_encoding('ASCII-8BIT')