0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ruby binary encoding

Last updated at Posted at 2025-04-10

背景

ライブラリ バージョン
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.dumpto_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')

参照

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?