From eaf96d532b83ece297fc31f4c934e2b97504894f Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Thu, 13 Aug 2026 20:25:44 +1200 Subject: [PATCH 1/4] Achieve complete test coverage Assisted-By: devx/48bd9802-2aca-41ba-856d-0b5bd523f9cf --- test/protocol/grpc/body/readable.rb | 63 +++++++++++++++++++++++++++ test/protocol/grpc/body/writable.rb | 30 +++++++++++++ test/protocol/grpc/error.rb | 39 +++++++++++++++++ test/protocol/grpc/header/encoding.rb | 39 +++++++++++++++++ test/protocol/grpc/header/message.rb | 7 +++ test/protocol/grpc/header/status.rb | 7 +++ test/protocol/grpc/header/timeout.rb | 20 +++++++++ test/protocol/grpc/metadata.rb | 52 ++++++++++++++++++++++ test/protocol/grpc/methods.rb | 10 +++++ test/protocol/grpc/middleware.rb | 9 ++++ 10 files changed, 276 insertions(+) create mode 100644 test/protocol/grpc/header/encoding.rb diff --git a/test/protocol/grpc/body/readable.rb b/test/protocol/grpc/body/readable.rb index d3379c5..f53e0ed 100644 --- a/test/protocol/grpc/body/readable.rb +++ b/test/protocol/grpc/body/readable.rb @@ -7,6 +7,8 @@ require "protocol/http/body/buffered" require_relative "../../../../fixtures/protocol/grpc/test_message" +require "zlib" + describe Protocol::GRPC::Body::Readable do let(:message_class) {Protocol::GRPC::Fixtures::TestMessage} let(:source_body) {Protocol::HTTP::Body::Buffered.new} @@ -19,6 +21,28 @@ def write_message(message, compressed: false) source_body.write(prefix + data) end + def write_data(data, compressed: false) + compression_flag = compressed ? 1 : 0 + prefix = [compression_flag].pack("C") + [data.bytesize].pack("N") + source_body.write(prefix + data) + end + + with ".wrap" do + it "wraps a message body" do + message = Struct.new(:body).new(source_body) + wrapped_body = subject.wrap(message, message_class: message_class) + + expect(wrapped_body).to be_a(subject) + expect(message.body).to be_equal(wrapped_body) + end + + it "returns nil when the message has no body" do + message = Struct.new(:body).new(nil) + + expect(subject.wrap(message)).to be_nil + end + end + it "has body attribute" do expect(body.body).to be == source_body end @@ -70,6 +94,20 @@ def write_message(message, compressed: false) read_message = body.read expect(read_message).to be == message end + + it "returns nil when the underlying body ends during a message" do + source_body = Object.new + def source_body.empty? + false + end + + def source_body.read + nil + end + + body = subject.new(source_body) + expect(body.read).to be_nil + end end with "#each" do @@ -127,6 +165,31 @@ def write_message(message, compressed: false) read_message = body.read expect(read_message).to be == message end + + it "decompresses deflate messages" do + body = subject.new(source_body, message_class: message_class, encoding: "deflate") + message = message_class.new(value: "Hello") + write_data(Zlib::Deflate.deflate(message.to_proto), compressed: true) + + expect(body.read).to be == message + end + + it "leaves messages unchanged for unknown encodings" do + body = subject.new(source_body, message_class: message_class, encoding: "custom") + message = message_class.new(value: "Hello") + write_message(message, compressed: true) + + expect(body.read).to be == message + end + + it "raises a gRPC error for invalid compressed data" do + body = subject.new(source_body, encoding: "gzip") + write_data("invalid", compressed: true) + + expect{body.read}.to raise_exception(Protocol::GRPC::Error) do |error| + expect(error.status_code).to be == Protocol::GRPC::Status::INTERNAL + end + end end with "empty stream" do diff --git a/test/protocol/grpc/body/writable.rb b/test/protocol/grpc/body/writable.rb index 425424b..247a828 100644 --- a/test/protocol/grpc/body/writable.rb +++ b/test/protocol/grpc/body/writable.rb @@ -212,6 +212,36 @@ def message.encode compressed = prefix[0].unpack1("C") expect(compressed).to be == 0 end + + it "compresses messages using deflate" do + body = subject.new(encoding: "deflate") + message = message_class.new(value: "Hello") + body.write(message) + body.close_write + + framed_data = body.join + expect(framed_data.getbyte(0)).to be == 1 + expect(Zlib::Inflate.inflate(framed_data.byteslice(5..))).to be == message.to_proto + end + + it "leaves messages unchanged for unknown encodings" do + body = subject.new(encoding: "custom") + message = message_class.new(value: "Hello") + body.write(message) + body.close_write + + framed_data = body.join + expect(framed_data.getbyte(0)).to be == 1 + expect(framed_data.byteslice(5..)).to be == message.to_proto + end + + it "raises a gRPC error when compression fails" do + body = subject.new(encoding: "deflate", level: 100) + + expect{body.write("Hello")}.to raise_exception(Protocol::GRPC::Error) do |error| + expect(error.status_code).to be == Protocol::GRPC::Status::INTERNAL + end + end end with "message framing" do diff --git a/test/protocol/grpc/error.rb b/test/protocol/grpc/error.rb index 16ed827..e3d03ab 100644 --- a/test/protocol/grpc/error.rb +++ b/test/protocol/grpc/error.rb @@ -86,4 +86,43 @@ expect(error.status_code).to be == Protocol::GRPC::Status::UNAUTHENTICATED end end + + with ".error_class_for_status" do + it "maps status codes to specialized error classes" do + expected_classes = { + Protocol::GRPC::Status::CANCELLED => Protocol::GRPC::Cancelled, + Protocol::GRPC::Status::INVALID_ARGUMENT => Protocol::GRPC::InvalidArgument, + Protocol::GRPC::Status::DEADLINE_EXCEEDED => Protocol::GRPC::DeadlineExceeded, + Protocol::GRPC::Status::NOT_FOUND => Protocol::GRPC::NotFound, + Protocol::GRPC::Status::INTERNAL => Protocol::GRPC::Internal, + Protocol::GRPC::Status::UNAVAILABLE => Protocol::GRPC::Unavailable, + Protocol::GRPC::Status::UNAUTHENTICATED => Protocol::GRPC::Unauthenticated, + } + + expected_classes.each do |status_code, error_class| + expect(subject.error_class_for_status(status_code)).to be == error_class + end + end + + it "uses the base error class for other status codes" do + expect(subject.error_class_for_status(Protocol::GRPC::Status::UNKNOWN)).to be == subject + end + end + + with ".for" do + it "creates a specialized error" do + error = subject.for(Protocol::GRPC::Status::NOT_FOUND, "Missing", metadata: {"key" => "value"}) + + expect(error).to be_a(Protocol::GRPC::NotFound) + expect(error.message).to be == "Missing" + expect(error.metadata).to be == {"key" => "value"} + end + + it "creates a base error for an unmapped status" do + error = subject.for(Protocol::GRPC::Status::UNKNOWN) + + expect(error.class).to be == subject + expect(error.status_code).to be == Protocol::GRPC::Status::UNKNOWN + end + end end diff --git a/test/protocol/grpc/header/encoding.rb b/test/protocol/grpc/header/encoding.rb new file mode 100644 index 0000000..6dbebc0 --- /dev/null +++ b/test/protocol/grpc/header/encoding.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2026, by Samuel Williams. + +require "protocol/grpc/header/encoding" + +describe Protocol::GRPC::Header::Encoding do + with ".coerce" do + it "coerces values to strings" do + expect(subject.coerce(:gzip)).to be == "gzip" + end + end + + with "#identity?" do + it "recognizes identity and empty encodings" do + expect(subject.new("identity")).to be(:identity?) + expect(subject.new("")).to be(:identity?) + end + + it "rejects compressed encodings" do + expect(subject.new("gzip")).not.to be(:identity?) + end + end + + with "#<<" do + it "replaces the encoding and returns itself" do + encoding = subject.new("identity") + expect(encoding << :gzip).to be_equal(encoding) + expect(encoding).to be == "gzip" + end + end + + with ".trailer?" do + it "returns false" do + expect(subject.trailer?).to be == false + end + end +end diff --git a/test/protocol/grpc/header/message.rb b/test/protocol/grpc/header/message.rb index f65d452..1d9c2b9 100644 --- a/test/protocol/grpc/header/message.rb +++ b/test/protocol/grpc/header/message.rb @@ -6,6 +6,13 @@ require "protocol/grpc/header/message" describe Protocol::GRPC::Header::Message do + with ".coerce" do + it "returns an existing message" do + message = subject.new("Error%20message") + expect(subject.coerce(message)).to be_equal(message) + end + end + with ".parse" do it "parses string value" do message = subject.parse("Error%20occurred") diff --git a/test/protocol/grpc/header/status.rb b/test/protocol/grpc/header/status.rb index f2271e6..5d8feec 100644 --- a/test/protocol/grpc/header/status.rb +++ b/test/protocol/grpc/header/status.rb @@ -6,6 +6,13 @@ require "protocol/grpc/header/status" describe Protocol::GRPC::Header::Status do + with ".coerce" do + it "returns an existing status" do + status = subject.new(5) + expect(subject.coerce(status)).to be_equal(status) + end + end + with ".parse" do it "parses string value to integer" do status = subject.parse("0") diff --git a/test/protocol/grpc/header/timeout.rb b/test/protocol/grpc/header/timeout.rb index 9afbfe6..f23b13c 100644 --- a/test/protocol/grpc/header/timeout.rb +++ b/test/protocol/grpc/header/timeout.rb @@ -6,6 +6,12 @@ require "protocol/grpc/header/timeout" describe Protocol::GRPC::Header::Timeout do + with ".coerce" do + it "coerces a string timeout" do + expect(subject.coerce("5S")).to be == "5S" + end + end + with ".format" do it "formats seconds" do expect(subject.format(5)).to be == "5S" @@ -72,4 +78,18 @@ end end end + + with "#<<" do + it "replaces the timeout and returns itself" do + timeout = subject.new("5S") + expect(timeout << "10S").to be_equal(timeout) + expect(timeout).to be == "10S" + end + end + + with ".trailer?" do + it "returns false" do + expect(subject.trailer?).to be == false + end + end end diff --git a/test/protocol/grpc/metadata.rb b/test/protocol/grpc/metadata.rb index 7658e59..8aa7a7e 100644 --- a/test/protocol/grpc/metadata.rb +++ b/test/protocol/grpc/metadata.rb @@ -8,6 +8,20 @@ require "protocol/http" describe Protocol::GRPC::Metadata do + def raw_headers(values) + Class.new do + attr_accessor :policy + + define_method(:initialize) do |values| + @values = values + end + + define_method(:[]) do |key| + @values[key] + end + end.new(values) + end + with ".extract_status" do it "extracts status from headers" do headers = Protocol::HTTP::Headers.new([%w[grpc-status 0]], nil, policy: Protocol::GRPC::HEADER_POLICY) @@ -23,6 +37,27 @@ headers = Protocol::HTTP::Headers.new([%w[grpc-status 14]], nil, policy: Protocol::GRPC::HEADER_POLICY) expect(subject.extract_status(headers)).to be == Protocol::GRPC::Status::UNAVAILABLE end + + it "extracts a raw scalar status" do + headers = raw_headers("grpc-status" => "14") + expect(subject.extract_status(headers)).to be == Protocol::GRPC::Status::UNAVAILABLE + end + + it "extracts a raw array status" do + headers = raw_headers("grpc-status" => [nil, "13"]) + expect(subject.extract_status(headers)).to be == Protocol::GRPC::Status::INTERNAL + end + + it "extracts a deeply nested raw array status" do + status = Class.new(Array) do + def flatten + [[["5"]]] + end + end.new + headers = raw_headers("grpc-status" => status) + + expect(subject.extract_status(headers)).to be == Protocol::GRPC::Status::NOT_FOUND + end end with ".extract_message" do @@ -35,6 +70,16 @@ headers = Protocol::HTTP::Headers.new expect(subject.extract_message(headers)).to be_nil end + + it "extracts a raw scalar message" do + headers = raw_headers("grpc-message" => "Error%20message") + expect(subject.extract_message(headers)).to be == "Error message" + end + + it "extracts a raw array message" do + headers = raw_headers("grpc-message" => ["Error%20message"]) + expect(subject.extract_message(headers)).to be == "Error message" + end end with ".assign_status!" do @@ -86,6 +131,13 @@ message_value = message_value.first if message_value.is_a?(Array) expect(message_value.to_s).to be == "Test" end + + it "uses the error message when no message is given" do + headers = Protocol::HTTP::Headers.new([], nil, policy: Protocol::GRPC::HEADER_POLICY) + subject.assign_status!(headers, status: Protocol::GRPC::Status::INTERNAL, error: StandardError.new("Failure")) + + expect(subject.extract_message(headers)).to be == "Failure" + end end end diff --git a/test/protocol/grpc/methods.rb b/test/protocol/grpc/methods.rb index 11dcb30..344e00b 100644 --- a/test/protocol/grpc/methods.rb +++ b/test/protocol/grpc/methods.rb @@ -100,6 +100,16 @@ expect(metadata["custom-bin"]).to be == ["\x01\x02\x03\x04".dup.force_encoding(Encoding::BINARY)] end + + it "decodes scalar binary metadata" do + headers = Object.new + def headers.to_h + {"custom-bin" => "AQIDBA=="} + end + + metadata = subject.extract_metadata(headers) + expect(metadata["custom-bin"]).to be == "\x01\x02\x03\x04".dup.force_encoding(Encoding::BINARY) + end end with ".format_timeout" do diff --git a/test/protocol/grpc/middleware.rb b/test/protocol/grpc/middleware.rb index ee29ba1..97d22dd 100644 --- a/test/protocol/grpc/middleware.rb +++ b/test/protocol/grpc/middleware.rb @@ -13,6 +13,15 @@ let(:services) {{}} let(:middleware) {TestMiddleware.new(services: services)} + with "abstract middleware" do + it "raises a not implemented error" do + headers = Protocol::HTTP::Headers.new([["content-type", "application/grpc"]]) + request = Protocol::HTTP::Request.new("https", "localhost", "POST", "/service/Method", nil, headers, nil) + + expect{subject.new.call(request)}.to raise_exception(NotImplementedError, message: be == "Subclasses must implement #dispatch") + end + end + with "#call" do let(:headers) do Protocol::HTTP::Headers.new([ From 9af1dc112a734882b35d148f8fa7047a4f342ff1 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Thu, 13 Aug 2026 20:42:10 +1200 Subject: [PATCH 2/4] Reject unsupported compression encodings Assisted-By: devx/48bd9802-2aca-41ba-856d-0b5bd523f9cf --- lib/protocol/grpc/body/readable.rb | 7 ++++++- lib/protocol/grpc/body/writable.rb | 5 ++++- test/protocol/grpc/body/readable.rb | 7 +++++-- test/protocol/grpc/body/writable.rb | 11 +++++------ 4 files changed, 20 insertions(+), 10 deletions(-) diff --git a/lib/protocol/grpc/body/readable.rb b/lib/protocol/grpc/body/readable.rb index 10ce169..0dcd3fd 100644 --- a/lib/protocol/grpc/body/readable.rb +++ b/lib/protocol/grpc/body/readable.rb @@ -7,6 +7,9 @@ require "protocol/http/body/wrapper" require "zlib" +require_relative "../error" +require_relative "../status" + module Protocol module GRPC # @namespace @@ -122,8 +125,10 @@ def decompress(data) inflater.close result else - data + raise Error.new(Status::UNIMPLEMENTED, "Unsupported compression encoding: #{@encoding.inspect}") end + rescue Error + raise rescue StandardError => error raise Error.new(Status::INTERNAL, "Failed to decompress message: #{error.message}") end diff --git a/lib/protocol/grpc/body/writable.rb b/lib/protocol/grpc/body/writable.rb index 503137e..2a02d47 100644 --- a/lib/protocol/grpc/body/writable.rb +++ b/lib/protocol/grpc/body/writable.rb @@ -8,6 +8,9 @@ require "zlib" require "stringio" +require_relative "../error" +require_relative "../status" + module Protocol module GRPC # @namespace @@ -97,7 +100,7 @@ def compress(data) # This matches HTTP's "deflate" content-encoding Zlib::Deflate.deflate(data, @level) else - data # No compression or identity + raise ArgumentError, "Unsupported compression encoding: #{@encoding.inspect}" end rescue StandardError => error raise Error.new(Status::INTERNAL, "Failed to compress message: #{error.message}") diff --git a/test/protocol/grpc/body/readable.rb b/test/protocol/grpc/body/readable.rb index f53e0ed..c293c49 100644 --- a/test/protocol/grpc/body/readable.rb +++ b/test/protocol/grpc/body/readable.rb @@ -174,12 +174,15 @@ def source_body.read expect(body.read).to be == message end - it "leaves messages unchanged for unknown encodings" do + it "rejects unsupported encodings" do body = subject.new(source_body, message_class: message_class, encoding: "custom") message = message_class.new(value: "Hello") write_message(message, compressed: true) - expect(body.read).to be == message + expect{body.read}.to raise_exception(Protocol::GRPC::Error) do |error| + expect(error.status_code).to be == Protocol::GRPC::Status::UNIMPLEMENTED + expect(error.message).to be =~ /Unsupported compression encoding: "custom"/ + end end it "raises a gRPC error for invalid compressed data" do diff --git a/test/protocol/grpc/body/writable.rb b/test/protocol/grpc/body/writable.rb index 247a828..7be9ad2 100644 --- a/test/protocol/grpc/body/writable.rb +++ b/test/protocol/grpc/body/writable.rb @@ -224,15 +224,14 @@ def message.encode expect(Zlib::Inflate.inflate(framed_data.byteslice(5..))).to be == message.to_proto end - it "leaves messages unchanged for unknown encodings" do + it "rejects unsupported encodings" do body = subject.new(encoding: "custom") message = message_class.new(value: "Hello") - body.write(message) - body.close_write - framed_data = body.join - expect(framed_data.getbyte(0)).to be == 1 - expect(framed_data.byteslice(5..)).to be == message.to_proto + expect{body.write(message)}.to raise_exception(Protocol::GRPC::Error) do |error| + expect(error.status_code).to be == Protocol::GRPC::Status::INTERNAL + expect(error.message).to be =~ /Unsupported compression encoding: "custom"/ + end end it "raises a gRPC error when compression fails" do From f68180c0d941533e27ba7d5a8b162806abed0560 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Thu, 13 Aug 2026 20:42:40 +1200 Subject: [PATCH 3/4] Reject truncated gRPC frames Assisted-By: devx/48bd9802-2aca-41ba-856d-0b5bd523f9cf --- lib/protocol/grpc/body/readable.rb | 22 +++++++++------ test/protocol/grpc/body/readable.rb | 44 ++++++++++++++++++++++++++++- 2 files changed, 57 insertions(+), 9 deletions(-) diff --git a/lib/protocol/grpc/body/readable.rb b/lib/protocol/grpc/body/readable.rb index 0dcd3fd..6cf297f 100644 --- a/lib/protocol/grpc/body/readable.rb +++ b/lib/protocol/grpc/body/readable.rb @@ -50,8 +50,6 @@ def initialize(body, message_class: nil, encoding: nil) # Overrides Wrapper#read to transform raw HTTP body chunks into decoded gRPC messages. # @returns [Object | String | Nil] Decoded message, raw binary, or `Nil` if stream ended def read - return nil if @body.nil? || @body.empty? - # Read 5-byte prefix: 1 byte compression flag + 4 bytes length prefix = read_exactly(5) return nil unless prefix @@ -61,7 +59,9 @@ def read # Read the message body: data = read_exactly(length) - return nil unless data + unless data + raise Error.new(Status::INTERNAL, "Truncated gRPC frame: expected #{length} bytes, received 0") + end # Decompress if needed: data = decompress(data) if compressed @@ -79,19 +79,25 @@ def read private # Read exactly n bytes from the underlying body. - # @parameter n [Integer] The number of bytes to read - # @returns [String | Nil] The data read, or `Nil` if the stream ended + # @parameter n [Integer] The number of bytes to read. + # @returns [String | Nil] The data read, or `Nil` if the stream ended before reading any bytes. + # @raises [Error] If the stream ends after reading a partial value. def read_exactly(n) # Fill buffer until we have enough data: while @buffer.bytesize < n - return nil if @body.nil? || @body.empty? + if @body.nil? || @body.empty? + return nil if @buffer.empty? + + raise Error.new(Status::INTERNAL, "Truncated gRPC frame: expected #{n} bytes, received #{@buffer.bytesize}") + end # Read chunk from underlying body: chunk = @body.read if chunk.nil? - # End of stream: - return nil + return nil if @buffer.empty? + + raise Error.new(Status::INTERNAL, "Truncated gRPC frame: expected #{n} bytes, received #{@buffer.bytesize}") end # Append to buffer: diff --git a/test/protocol/grpc/body/readable.rb b/test/protocol/grpc/body/readable.rb index c293c49..1685d77 100644 --- a/test/protocol/grpc/body/readable.rb +++ b/test/protocol/grpc/body/readable.rb @@ -95,7 +95,7 @@ def write_data(data, compressed: false) expect(read_message).to be == message end - it "returns nil when the underlying body ends during a message" do + it "returns nil when the underlying body reports clean EOF" do source_body = Object.new def source_body.empty? false @@ -108,6 +108,48 @@ def source_body.read body = subject.new(source_body) expect(body.read).to be_nil end + + it "raises an error for a truncated prefix" do + source_body.write("\x00\x00".b) + + expect{body.read}.to raise_exception(Protocol::GRPC::Error) do |error| + expect(error.status_code).to be == Protocol::GRPC::Status::INTERNAL + expect(error.message).to be =~ /expected 5 bytes, received 2/ + end + end + + it "raises an error when a partial prefix is followed by nil" do + chunks = ["\x00".b, nil] + source_body = Object.new + source_body.define_singleton_method(:empty?){false} + source_body.define_singleton_method(:read){chunks.shift} + body = subject.new(source_body) + + expect{body.read}.to raise_exception(Protocol::GRPC::Error) do |error| + expect(error.status_code).to be == Protocol::GRPC::Status::INTERNAL + expect(error.message).to be =~ /expected 5 bytes, received 1/ + end + end + + it "raises an error for a truncated payload" do + write_data("ab") + framed_data = source_body.read + source_body.write(framed_data.byteslice(0...5) + "a") + + expect{body.read}.to raise_exception(Protocol::GRPC::Error) do |error| + expect(error.status_code).to be == Protocol::GRPC::Status::INTERNAL + expect(error.message).to be =~ /expected 2 bytes, received 1/ + end + end + + it "raises an error when the payload is missing" do + source_body.write("\x00".b + [2].pack("N")) + + expect{body.read}.to raise_exception(Protocol::GRPC::Error) do |error| + expect(error.status_code).to be == Protocol::GRPC::Status::INTERNAL + expect(error.message).to be =~ /expected 2 bytes, received 0/ + end + end end with "#each" do From f574d168287c7a5b3549aa4e090cca2cbb0bc038 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Thu, 13 Aug 2026 20:59:51 +1200 Subject: [PATCH 4/4] Rely on gRPC header policy Assisted-By: devx/48bd9802-2aca-41ba-856d-0b5bd523f9cf --- lib/protocol/grpc/metadata.rb | 33 ++--------------------- test/protocol/grpc/metadata.rb | 49 ++-------------------------------- 2 files changed, 4 insertions(+), 78 deletions(-) diff --git a/lib/protocol/grpc/metadata.rb b/lib/protocol/grpc/metadata.rb index bd023fc..44552f7 100644 --- a/lib/protocol/grpc/metadata.rb +++ b/lib/protocol/grpc/metadata.rb @@ -3,7 +3,6 @@ # Released under the MIT License. # Copyright, 2025-2026, by Samuel Williams. -require "uri" require_relative "header" require_relative "status" @@ -12,7 +11,6 @@ module GRPC # @namespace module Metadata # Extract gRPC status from headers. - # Convenience method that handles both Header::Status instances and raw values. # Returns Status::UNKNOWN if status is not present. # # Note: In Protocol::HTTP::Headers, trailers are merged into the headers @@ -29,31 +27,10 @@ def self.extract_status(headers) status = headers["grpc-status"] return Status::UNKNOWN unless status - if status.is_a?(Header::Status) - status.to_i - else - # Fallback for when header policy isn't used - # Handle Array case (may occur with external clients) - status_value = if status.is_a?(Array) - # Flatten and take first non-nil value, recursively handle nested arrays - flattened = status.flatten.compact.first - # If still an array, take first element - flattened.is_a?(Array) ? flattened.first : flattened - else - status - end - - # Convert to string then integer to handle various types - # Handle case where status_value might still be an array somehow - if status_value.is_a?(Array) - status_value = status_value.first - end - status_value.to_s.to_i - end + return status.to_i end # Extract gRPC status message from headers. - # Convenience method that handles both Header::Message instances and raw values. # Returns `Nil` if message is not present. # # @parameter headers [Protocol::HTTP::Headers] @@ -66,13 +43,7 @@ def self.extract_message(headers) message = headers["grpc-message"] return nil unless message - if message.is_a?(Header::Message) - message.decode - else - # Fallback for when header policy isn't used - message_value = message.is_a?(Array) ? message.first : message.to_s - URI.decode_www_form_component(message_value) - end + return message.decode end # Assign gRPC status, message, and optional backtrace to headers. diff --git a/test/protocol/grpc/metadata.rb b/test/protocol/grpc/metadata.rb index 8aa7a7e..03e02e3 100644 --- a/test/protocol/grpc/metadata.rb +++ b/test/protocol/grpc/metadata.rb @@ -8,20 +8,6 @@ require "protocol/http" describe Protocol::GRPC::Metadata do - def raw_headers(values) - Class.new do - attr_accessor :policy - - define_method(:initialize) do |values| - @values = values - end - - define_method(:[]) do |key| - @values[key] - end - end.new(values) - end - with ".extract_status" do it "extracts status from headers" do headers = Protocol::HTTP::Headers.new([%w[grpc-status 0]], nil, policy: Protocol::GRPC::HEADER_POLICY) @@ -34,35 +20,14 @@ def raw_headers(values) end it "extracts non-zero status" do - headers = Protocol::HTTP::Headers.new([%w[grpc-status 14]], nil, policy: Protocol::GRPC::HEADER_POLICY) + headers = Protocol::HTTP::Headers.new([%w[grpc-status 14]]) expect(subject.extract_status(headers)).to be == Protocol::GRPC::Status::UNAVAILABLE end - - it "extracts a raw scalar status" do - headers = raw_headers("grpc-status" => "14") - expect(subject.extract_status(headers)).to be == Protocol::GRPC::Status::UNAVAILABLE - end - - it "extracts a raw array status" do - headers = raw_headers("grpc-status" => [nil, "13"]) - expect(subject.extract_status(headers)).to be == Protocol::GRPC::Status::INTERNAL - end - - it "extracts a deeply nested raw array status" do - status = Class.new(Array) do - def flatten - [[["5"]]] - end - end.new - headers = raw_headers("grpc-status" => status) - - expect(subject.extract_status(headers)).to be == Protocol::GRPC::Status::NOT_FOUND - end end with ".extract_message" do it "extracts message from headers" do - headers = Protocol::HTTP::Headers.new([["grpc-message", "Error%20message"]], nil, policy: Protocol::GRPC::HEADER_POLICY) + headers = Protocol::HTTP::Headers.new([["grpc-message", "Error%20message"]]) expect(subject.extract_message(headers)).to be == "Error message" end @@ -70,16 +35,6 @@ def flatten headers = Protocol::HTTP::Headers.new expect(subject.extract_message(headers)).to be_nil end - - it "extracts a raw scalar message" do - headers = raw_headers("grpc-message" => "Error%20message") - expect(subject.extract_message(headers)).to be == "Error message" - end - - it "extracts a raw array message" do - headers = raw_headers("grpc-message" => ["Error%20message"]) - expect(subject.extract_message(headers)).to be == "Error message" - end end with ".assign_status!" do