Merge pull request #22876 from apolcyn/recv_status_error_message_ruby

Ruby: surface recv_status_op.error_string and into BadStatus exceptions
pull/22473/head
apolcyn 5 years ago committed by GitHub
commit 5a5105b89c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 10
      src/ruby/ext/grpc/rb_call.c
  2. 145
      src/ruby/lib/grpc/errors.rb
  3. 5
      src/ruby/lib/grpc/generic/active_call.rb
  4. 2
      src/ruby/lib/grpc/structs.rb
  5. 134
      src/ruby/spec/debug_message_spec.rb

@ -620,6 +620,7 @@ typedef struct run_batch_stack {
int recv_cancelled;
grpc_status_code recv_status;
grpc_slice recv_status_details;
const char* recv_status_debug_error_string;
unsigned write_flag;
grpc_slice send_status_details;
} run_batch_stack;
@ -729,6 +730,8 @@ static void grpc_run_batch_stack_fill_ops(run_batch_stack* st, VALUE ops_hash) {
&st->recv_status;
st->ops[st->op_num].data.recv_status_on_client.status_details =
&st->recv_status_details;
st->ops[st->op_num].data.recv_status_on_client.error_string =
&st->recv_status_debug_error_string;
break;
case GRPC_OP_RECV_CLOSE_ON_SERVER:
st->ops[st->op_num].data.recv_close_on_server.cancelled =
@ -780,7 +783,12 @@ static VALUE grpc_run_batch_stack_build_result(run_batch_stack* st) {
(GRPC_SLICE_START_PTR(st->recv_status_details) == NULL
? Qnil
: grpc_rb_slice_to_ruby_string(st->recv_status_details)),
grpc_rb_md_ary_to_h(&st->recv_trailing_metadata), NULL));
grpc_rb_md_ary_to_h(&st->recv_trailing_metadata),
st->recv_status_debug_error_string == NULL
? Qnil
: rb_str_new_cstr(st->recv_status_debug_error_string),
NULL));
gpr_free((void*)st->recv_status_debug_error_string);
break;
case GRPC_OP_RECV_CLOSE_ON_SERVER:
rb_struct_aset(result, sym_send_close, Qtrue);

@ -30,18 +30,26 @@ module GRPC
# https://github.com/grpc/grpc/blob/master/include/grpc/impl/codegen/status.h
# for detailed descriptions of each status code.
class BadStatus < StandardError
attr_reader :code, :details, :metadata
attr_reader :code, :details, :metadata, :debug_error_string
include GRPC::Core::StatusCodes
# @param code [Numeric] the status code
# @param details [String] the details of the exception
# @param metadata [Hash] the error's metadata
def initialize(code, details = 'unknown cause', metadata = {})
super("#{code}:#{details}")
def initialize(code,
details = 'unknown cause',
metadata = {},
debug_error_string = nil)
exception_message = "#{code}:#{details}"
if debug_error_string
exception_message += ". debug_error_string:#{debug_error_string}"
end
super(exception_message)
@code = code
@details = details
@metadata = metadata
@debug_error_string = debug_error_string
end
# Converts the exception to a {Struct::Status} for use in the networking
@ -49,7 +57,7 @@ module GRPC
#
# @return [Struct::Status] with the same code and details
def to_status
Struct::Status.new(code, details, metadata)
Struct::Status.new(code, details, metadata, debug_error_string)
end
# Converts the exception to a deserialized {Google::Rpc::Status} object.
@ -66,8 +74,10 @@ module GRPC
nil
end
def self.new_status_exception(code, details = 'unknown cause',
metadata = {})
def self.new_status_exception(code,
details = 'unknown cause',
metadata = {},
debug_error_string = nil)
codes = {}
codes[OK] = Ok
codes[CANCELLED] = Cancelled
@ -88,129 +98,180 @@ module GRPC
codes[DATA_LOSS] = DataLoss
if codes[code].nil?
BadStatus.new(code, details, metadata)
BadStatus.new(code, details, metadata, debug_error_string)
else
codes[code].new(details, metadata)
codes[code].new(details, metadata, debug_error_string)
end
end
end
# GRPC status code corresponding to status OK
class Ok < BadStatus
def initialize(details = 'unknown cause', metadata = {})
super(Core::StatusCodes::OK, details, metadata)
def initialize(details = 'unknown cause',
metadata = {},
debug_error_string = nil)
super(Core::StatusCodes::OK,
details, metadata, debug_error_string)
end
end
# GRPC status code corresponding to status CANCELLED
class Cancelled < BadStatus
def initialize(details = 'unknown cause', metadata = {})
super(Core::StatusCodes::CANCELLED, details, metadata)
def initialize(details = 'unknown cause',
metadata = {},
debug_error_string = nil)
super(Core::StatusCodes::CANCELLED,
details, metadata, debug_error_string)
end
end
# GRPC status code corresponding to status UNKNOWN
class Unknown < BadStatus
def initialize(details = 'unknown cause', metadata = {})
super(Core::StatusCodes::UNKNOWN, details, metadata)
def initialize(details = 'unknown cause',
metadata = {},
debug_error_string = nil)
super(Core::StatusCodes::UNKNOWN,
details, metadata, debug_error_string)
end
end
# GRPC status code corresponding to status INVALID_ARGUMENT
class InvalidArgument < BadStatus
def initialize(details = 'unknown cause', metadata = {})
super(Core::StatusCodes::INVALID_ARGUMENT, details, metadata)
def initialize(details = 'unknown cause',
metadata = {},
debug_error_string = nil)
super(Core::StatusCodes::INVALID_ARGUMENT,
details, metadata, debug_error_string)
end
end
# GRPC status code corresponding to status DEADLINE_EXCEEDED
class DeadlineExceeded < BadStatus
def initialize(details = 'unknown cause', metadata = {})
super(Core::StatusCodes::DEADLINE_EXCEEDED, details, metadata)
def initialize(details = 'unknown cause',
metadata = {},
debug_error_string = nil)
super(Core::StatusCodes::DEADLINE_EXCEEDED,
details, metadata, debug_error_string)
end
end
# GRPC status code corresponding to status NOT_FOUND
class NotFound < BadStatus
def initialize(details = 'unknown cause', metadata = {})
super(Core::StatusCodes::NOT_FOUND, details, metadata)
def initialize(details = 'unknown cause',
metadata = {},
debug_error_string = nil)
super(Core::StatusCodes::NOT_FOUND,
details, metadata, debug_error_string)
end
end
# GRPC status code corresponding to status ALREADY_EXISTS
class AlreadyExists < BadStatus
def initialize(details = 'unknown cause', metadata = {})
super(Core::StatusCodes::ALREADY_EXISTS, details, metadata)
def initialize(details = 'unknown cause',
metadata = {},
debug_error_string = nil)
super(Core::StatusCodes::ALREADY_EXISTS,
details, metadata, debug_error_string)
end
end
# GRPC status code corresponding to status PERMISSION_DENIED
class PermissionDenied < BadStatus
def initialize(details = 'unknown cause', metadata = {})
super(Core::StatusCodes::PERMISSION_DENIED, details, metadata)
def initialize(details = 'unknown cause',
metadata = {},
debug_error_string = nil)
super(Core::StatusCodes::PERMISSION_DENIED,
details, metadata, debug_error_string)
end
end
# GRPC status code corresponding to status UNAUTHENTICATED
class Unauthenticated < BadStatus
def initialize(details = 'unknown cause', metadata = {})
super(Core::StatusCodes::UNAUTHENTICATED, details, metadata)
def initialize(details = 'unknown cause',
metadata = {},
debug_error_string = nil)
super(Core::StatusCodes::UNAUTHENTICATED,
details, metadata, debug_error_string)
end
end
# GRPC status code corresponding to status RESOURCE_EXHAUSTED
class ResourceExhausted < BadStatus
def initialize(details = 'unknown cause', metadata = {})
super(Core::StatusCodes::RESOURCE_EXHAUSTED, details, metadata)
def initialize(details = 'unknown cause',
metadata = {},
debug_error_string = nil)
super(Core::StatusCodes::RESOURCE_EXHAUSTED,
details, metadata, debug_error_string)
end
end
# GRPC status code corresponding to status FAILED_PRECONDITION
class FailedPrecondition < BadStatus
def initialize(details = 'unknown cause', metadata = {})
super(Core::StatusCodes::FAILED_PRECONDITION, details, metadata)
def initialize(details = 'unknown cause',
metadata = {},
debug_error_string = nil)
super(Core::StatusCodes::FAILED_PRECONDITION,
details, metadata, debug_error_string)
end
end
# GRPC status code corresponding to status ABORTED
class Aborted < BadStatus
def initialize(details = 'unknown cause', metadata = {})
super(Core::StatusCodes::ABORTED, details, metadata)
def initialize(details = 'unknown cause',
metadata = {},
debug_error_string = nil)
super(Core::StatusCodes::ABORTED,
details, metadata, debug_error_string)
end
end
# GRPC status code corresponding to status OUT_OF_RANGE
class OutOfRange < BadStatus
def initialize(details = 'unknown cause', metadata = {})
super(Core::StatusCodes::OUT_OF_RANGE, details, metadata)
def initialize(details = 'unknown cause',
metadata = {},
debug_error_string = nil)
super(Core::StatusCodes::OUT_OF_RANGE,
details, metadata, debug_error_string)
end
end
# GRPC status code corresponding to status UNIMPLEMENTED
class Unimplemented < BadStatus
def initialize(details = 'unknown cause', metadata = {})
super(Core::StatusCodes::UNIMPLEMENTED, details, metadata)
def initialize(details = 'unknown cause',
metadata = {},
debug_error_string = nil)
super(Core::StatusCodes::UNIMPLEMENTED,
details, metadata, debug_error_string)
end
end
# GRPC status code corresponding to status INTERNAL
class Internal < BadStatus
def initialize(details = 'unknown cause', metadata = {})
super(Core::StatusCodes::INTERNAL, details, metadata)
def initialize(details = 'unknown cause',
metadata = {},
debug_error_string = nil)
super(Core::StatusCodes::INTERNAL,
details, metadata, debug_error_string)
end
end
# GRPC status code corresponding to status UNAVAILABLE
class Unavailable < BadStatus
def initialize(details = 'unknown cause', metadata = {})
super(Core::StatusCodes::UNAVAILABLE, details, metadata)
def initialize(details = 'unknown cause',
metadata = {},
debug_error_string = nil)
super(Core::StatusCodes::UNAVAILABLE,
details, metadata, debug_error_string)
end
end
# GRPC status code corresponding to status DATA_LOSS
class DataLoss < BadStatus
def initialize(details = 'unknown cause', metadata = {})
super(Core::StatusCodes::DATA_LOSS, details, metadata)
def initialize(details = 'unknown cause',
metadata = {},
debug_error_string = nil)
super(Core::StatusCodes::DATA_LOSS,
details, metadata, debug_error_string)
end
end
end

@ -23,13 +23,12 @@ class Struct
# is non-nil and not OK.
def check_status
return nil if status.nil?
fail GRPC::Cancelled if status.code == GRPC::Core::StatusCodes::CANCELLED
if status.code != GRPC::Core::StatusCodes::OK
GRPC.logger.debug("Failing with status #{status}")
# raise BadStatus, propagating the metadata if present.
md = status.metadata
fail GRPC::BadStatus.new_status_exception(
status.code, status.details, md)
status.code, status.details, status.metadata,
status.debug_error_string)
end
status
end

@ -12,4 +12,4 @@
# See the License for the specific language governing permissions and
# limitations under the License.
Struct.new('Status', :code, :details, :metadata)
Struct.new('Status', :code, :details, :metadata, :debug_error_string)

@ -0,0 +1,134 @@
# Copyright 2015 gRPC authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
require 'spec_helper'
TEST_DEBUG_MESSAGE = 'raised by test server'.freeze
# a test service that checks the cert of its peer
class DebugMessageTestService
include GRPC::GenericService
rpc :an_rpc_raises_abort, EchoMsg, EchoMsg
rpc :an_rpc_raises_standarderror, EchoMsg, EchoMsg
def an_rpc_raises_abort(_req, _call)
fail GRPC::Aborted.new(
'aborted',
{},
TEST_DEBUG_MESSAGE)
end
def an_rpc_raises_standarderror(_req, _call)
fail(StandardError, TEST_DEBUG_MESSAGE)
end
end
DebugMessageTestServiceStub = DebugMessageTestService.rpc_stub_class
describe 'surfacing and transmitting of debug messages' do
RpcServer = GRPC::RpcServer
before(:all) do
server_opts = {
poll_period: 1
}
@srv = new_rpc_server_for_testing(**server_opts)
@port = @srv.add_http2_port('0.0.0.0:0', :this_port_is_insecure)
@srv.handle(DebugMessageTestService)
@srv_thd = Thread.new { @srv.run }
@srv.wait_till_running
end
after(:all) do
expect(@srv.stopped?).to be(false)
@srv.stop
@srv_thd.join
end
it 'debug error message is not present BadStatus exceptions that dont set it' do
exception_message = ''
begin
fail GRPC::Unavailable('unavailable', {})
rescue StandardError => e
p "Got exception: #{e.message}"
exception_message = e.message
end
expect(exception_message.empty?).to be(false)
expect(exception_message.include?('debug_error_string')).to be(false)
end
it 'debug error message is present in locally generated errors' do
# Create a secure channel. This is just one way to force a
# connection handshake error, which shoud result in C-core
# generating a status and error message and surfacing them up.
test_root = File.join(File.dirname(__FILE__), 'testdata')
files = ['ca.pem', 'client.key', 'client.pem']
creds = files.map { |f| File.open(File.join(test_root, f)).read }
creds = GRPC::Core::ChannelCredentials.new(creds[0], creds[1], creds[2])
stub = DebugMessageTestServiceStub.new(
"localhost:#{@port}", creds)
begin
stub.an_rpc_raises_abort(EchoMsg.new)
rescue StandardError => e
p "Got exception: #{e.message}"
exception_message = e.message
# check that the RPC did actually result in a BadStatus exception
expect(e.is_a?(GRPC::BadStatus)).to be(true)
end
# just check that the debug_error_string is non-empty (we know that
# it's a JSON object, so the first character is '{').
expect(exception_message.include?('. debug_error_string:{')).to be(true)
end
it 'debug message is not transmitted from server to client' do
# in order to not accidentally leak internal details about a
# server to untrusted clients, avoid including the debug_error_string
# field of a BadStatusException raised at a server in the
# RPC status that it sends to clients.
stub = DebugMessageTestServiceStub.new(
"localhost:#{@port}", :this_channel_is_insecure)
exception_message = ''
begin
stub.an_rpc_raises_abort(EchoMsg.new)
rescue StandardError => e
p "Got exception: #{e.message}"
exception_message = e.message
# check that the status was aborted is an indirect way to
# tell that the RPC did actually get handled by the server
expect(e.is_a?(GRPC::Aborted)).to be(true)
end
# just assert that the contents of the server-side BadStatus
# debug_error_string field were *not* propagated to the client.
expect(exception_message.include?('. debug_error_string:{')).to be(true)
expect(exception_message.include?(TEST_DEBUG_MESSAGE)).to be(false)
end
it 'standard_error messages are transmitted from server to client' do
# this test exists mostly in order to understand the test case
# above, by comparison.
stub = DebugMessageTestServiceStub.new(
"localhost:#{@port}", :this_channel_is_insecure)
exception_message = ''
begin
stub.an_rpc_raises_standarderror(EchoMsg.new)
rescue StandardError => e
p "Got exception: #{e.message}"
exception_message = e.message
expect(e.is_a?(GRPC::BadStatus)).to be(true)
end
# assert that the contents of the StandardError exception message
# are propagated to the client.
expect(exception_message.include?(TEST_DEBUG_MESSAGE)).to be(true)
end
end
Loading…
Cancel
Save