Skip to content

Commit a66054c

Browse files
Add support for backtraces.
1 parent 66c6547 commit a66054c

File tree

12 files changed

+224
-192
lines changed

12 files changed

+224
-192
lines changed

context/getting-started.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,3 +129,4 @@ call.deadline.exceeded? # => false
129129
# Access peer information
130130
call.peer # => Protocol::HTTP::Address
131131
```
132+

context/index.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ files:
1010
title: Getting Started
1111
description: This guide explains how to use `protocol-grpc` for building abstract
1212
gRPC interfaces.
13+

design.md

Lines changed: 38 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -293,58 +293,22 @@ module Protocol
293293
message ? URI.decode_www_form_component(message) : nil
294294
end
295295

296-
# Build headers with gRPC status and message
297-
# @parameter status [Integer] gRPC status code
298-
# @parameter message [String, nil] Optional status message
299-
# @parameter policy [Hash] Header policy to use
300-
# @returns [Protocol::HTTP::Headers]
301-
def self.build_status_headers(status: Status::OK, message: nil, policy: HEADER_POLICY)
302-
headers = Protocol::HTTP::Headers.new([], nil, policy: policy)
303-
headers["grpc-status"] = status.to_s
304-
headers["grpc-message"] = URI.encode_www_form_component(message) if message
305-
headers
306-
end
307-
308-
# Mark that trailers will follow (call after sending initial headers)
309-
# @parameter headers [Protocol::HTTP::Headers]
310-
# @returns [Protocol::HTTP::Headers]
311-
def self.prepare_trailers!(headers)
312-
headers.trailer!
313-
headers
314-
end
296+
# Add gRPC status, message, and optional backtrace to headers.
297+
# Whether these become headers or trailers is controlled by the protocol layer.
298+
# @parameter headers [Protocol::HTTP::Headers]
299+
# @parameter status [Integer] gRPC status code
300+
# @parameter message [String | Nil] Optional status message
301+
# @parameter error [Exception | Nil] Optional error object (used to extract backtrace)
302+
def self.add_status!(headers, status: Status::OK, message: nil, error: nil)
303+
headers["grpc-status"] = Header::Status.new(status)
304+
headers["grpc-message"] = Header::Message.new(Header::Message.encode(message)) if message
315305

316-
# Add status as trailers to existing headers
317-
# @parameter headers [Protocol::HTTP::Headers]
318-
# @parameter status [Integer] gRPC status code
319-
# @parameter message [String, nil] Optional status message
320-
def self.add_status_trailer!(headers, status: Status::OK, message: nil)
321-
headers.trailer! unless headers.trailer?
322-
headers["grpc-status"] = status.to_s
323-
headers["grpc-message"] = URI.encode_www_form_component(message) if message
324-
end
325-
326-
# Add status as initial headers (for trailers-only responses)
327-
# @parameter headers [Protocol::HTTP::Headers]
328-
# @parameter status [Integer] gRPC status code
329-
# @parameter message [String, nil] Optional status message
330-
def self.add_status_header!(headers, status: Status::OK, message: nil)
331-
headers["grpc-status"] = status.to_s
332-
headers["grpc-message"] = URI.encode_www_form_component(message) if message
333-
end
334-
335-
# Build a trailers-only error response (no body, status in headers)
336-
# @parameter status [Integer] gRPC status code
337-
# @parameter message [String, nil] Optional status message
338-
# @parameter policy [Hash] Header policy to use
339-
# @returns [Protocol::HTTP::Response]
340-
def self.build_trailers_only_response(status:, message: nil, policy: HEADER_POLICY)
341-
headers = Protocol::HTTP::Headers.new([], nil, policy: policy)
342-
headers["content-type"] = "application/grpc+proto"
343-
add_status_header!(headers, status: status, message: message)
344-
345-
Protocol::HTTP::Response[200, headers, nil]
306+
# Add backtrace from error if available
307+
if error && error.backtrace && !error.backtrace.empty?
308+
headers["backtrace"] = error.backtrace
346309
end
347310
end
311+
end
348312
end
349313
end
350314
```
@@ -754,7 +718,7 @@ module Protocol
754718
# Find handler
755719
handler = @services[service_name]
756720
unless handler
757-
return trailers_only_error(Status::UNIMPLEMENTED, "Service not found: #{service_name}")
721+
return make_response(Status::UNIMPLEMENTED, "Service not found: #{service_name}")
758722
end
759723

760724
# Determine handler method and message classes
@@ -773,17 +737,17 @@ module Protocol
773737
end
774738

775739
unless handler.respond_to?(handler_method)
776-
return trailers_only_error(Status::UNIMPLEMENTED, "Method not found: #{method_name}")
740+
return make_response(Status::UNIMPLEMENTED, "Method not found: #{method_name}")
777741
end
778742

779743
# Handle the RPC
780-
begin
781-
handle_rpc(request, handler, handler_method, request_class, response_class)
782-
rescue Error => error
783-
trailers_only_error(e.status_code, error.message)
784-
rescue => error
785-
trailers_only_error(Status::INTERNAL, error.message)
786-
end
744+
begin
745+
handle_rpc(request, handler, handler_method, request_class, response_class)
746+
rescue Error => error
747+
make_response(error.status_code, error.message, error: error)
748+
rescue => error
749+
make_response(Status::INTERNAL, error.message, error: error)
750+
end
787751
end
788752

789753
protected
@@ -811,20 +775,22 @@ module Protocol
811775
handler.send(method, input, output, call)
812776
output.close_write unless output.closed?
813777

814-
# Mark trailers and add status
815-
response_headers.trailer!
816-
Metadata.add_status_trailer!(response_headers, status: Status::OK)
817-
818-
Protocol::HTTP::Response[200, response_headers, output]
819-
end
778+
# Mark trailers and add status
779+
response_headers.trailer!
780+
Metadata.add_status!(response_headers, status: Status::OK)
820781

821-
def trailers_only_error(status_code, message)
822-
Metadata.build_trailers_only_response(
823-
status: status_code,
824-
message: message,
825-
policy: HEADER_POLICY
826-
)
827-
end
782+
Protocol::HTTP::Response[200, response_headers, output]
783+
end
784+
785+
protected
786+
787+
def make_response(status_code, message, error: nil)
788+
headers = Protocol::HTTP::Headers.new([], nil, policy: HEADER_POLICY)
789+
headers["content-type"] = "application/grpc+proto"
790+
Metadata.add_status!(headers, status: status_code, message: message, error: error)
791+
792+
Protocol::HTTP::Response[200, headers, nil]
793+
end
828794
end
829795
end
830796
end
@@ -953,7 +919,7 @@ def handle_grpc_request(http_request)
953919
# Add status as trailer - these will be sent after the response body
954920
# Note: The user just adds them to headers; the @tail marker ensures
955921
# they're recognized as trailers internally
956-
Protocol::GRPC::Metadata.add_status_trailer!(headers, status: Protocol::GRPC::Status::OK)
922+
Protocol::GRPC::Metadata.add_status!(headers, status: Protocol::GRPC::Status::OK)
957923

958924
Protocol::HTTP::Response[200, headers, output]
959925
end

fixtures/protocol/grpc/test_middleware.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ def dispatch(request)
6666

6767
# Mark trailers and add status
6868
response_headers.trailer!
69-
Protocol::GRPC::Metadata.add_status_trailer!(response_headers, status: Protocol::GRPC::Status::OK)
69+
Protocol::GRPC::Metadata.add_status!(response_headers, status: Protocol::GRPC::Status::OK)
7070

7171
Protocol::HTTP::Response[200, response_headers, final_output]
7272
end

guides/getting-started/readme.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,3 +129,4 @@ call.deadline.exceeded? # => false
129129
# Access peer information
130130
call.peer # => Protocol::HTTP::Address
131131
```
132+

lib/protocol/grpc/metadata.rb

Lines changed: 10 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -75,57 +75,23 @@ def self.extract_message(headers)
7575
end
7676
end
7777

78-
# Build headers with gRPC status and message
79-
# @parameter status [Integer] gRPC status code
80-
# @parameter message [String | Nil] Optional status message
81-
# @parameter policy [Hash] Header policy to use
82-
# @returns [Protocol::HTTP::Headers]
83-
def self.build_status_headers(status: Status::OK, message: nil, policy: HEADER_POLICY)
84-
headers = Protocol::HTTP::Headers.new([], nil, policy: policy)
85-
headers["grpc-status"] = Header::Status.new(status)
86-
headers["grpc-message"] = Header::Message.new(Header::Message.encode(message)) if message
87-
headers
88-
end
89-
90-
# Mark that trailers will follow (call after sending initial headers)
91-
# @parameter headers [Protocol::HTTP::Headers]
92-
# @returns [Protocol::HTTP::Headers]
93-
def self.prepare_trailers!(headers)
94-
headers.trailer!
95-
headers
96-
end
97-
98-
# Add status as trailers to existing headers
99-
# @parameter headers [Protocol::HTTP::Headers]
100-
# @parameter status [Integer] gRPC status code
101-
# @parameter message [String | Nil] Optional status message
102-
def self.add_status_trailer!(headers, status: Status::OK, message: nil)
103-
headers.trailer! unless headers.trailer?
104-
headers["grpc-status"] = Header::Status.new(status)
105-
headers["grpc-message"] = Header::Message.new(Header::Message.encode(message)) if message
106-
end
107-
108-
# Add status as initial headers (for trailers-only responses)
78+
# Add gRPC status, message, and optional backtrace to headers.
79+
# Whether these become headers or trailers is controlled by the protocol layer.
10980
# @parameter headers [Protocol::HTTP::Headers]
11081
# @parameter status [Integer] gRPC status code
11182
# @parameter message [String | Nil] Optional status message
112-
def self.add_status_header!(headers, status: Status::OK, message: nil)
83+
# @parameter error [Exception | Nil] Optional error object (used to extract backtrace)
84+
def self.add_status!(headers, status: Status::OK, message: nil, error: nil)
11385
headers["grpc-status"] = Header::Status.new(status)
11486
headers["grpc-message"] = Header::Message.new(Header::Message.encode(message)) if message
115-
end
116-
117-
# Build a trailers-only error response (no body, status in headers)
118-
# @parameter status [Integer] gRPC status code
119-
# @parameter message [String | Nil] Optional status message
120-
# @parameter policy [Hash] Header policy to use
121-
# @returns [Protocol::HTTP::Response]
122-
def self.build_trailers_only_response(status:, message: nil, policy: HEADER_POLICY)
123-
headers = Protocol::HTTP::Headers.new([], nil, policy: policy)
124-
headers["content-type"] = "application/grpc+proto"
125-
add_status_header!(headers, status: status, message: message)
12687

127-
Protocol::HTTP::Response[200, headers, nil]
88+
# Add backtrace from error if available
89+
if error && error.backtrace && !error.backtrace.empty?
90+
# Assign backtrace array directly - Split header will handle it
91+
headers["backtrace"] = error.backtrace
92+
end
12893
end
94+
12995
end
13096
end
13197
end

lib/protocol/grpc/middleware.rb

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,9 @@ def call(request)
3030
begin
3131
dispatch(request)
3232
rescue Error => error
33-
trailers_only_error(error.status_code, error.message)
33+
make_response(error.status_code, error.message, error: error)
3434
rescue StandardError => error
35-
trailers_only_error(Status::INTERNAL, error.message)
35+
make_response(Status::INTERNAL, error.message, error: error)
3636
end
3737
end
3838

@@ -45,7 +45,7 @@ def dispatch(request)
4545
raise NotImplementedError, "Subclasses must implement #dispatch"
4646
end
4747

48-
protected
48+
protected
4949

5050
# Check if the request is a gRPC request.
5151
# @parameter request [Protocol::HTTP::Request]
@@ -55,16 +55,17 @@ def grpc_request?(request)
5555
content_type&.start_with?("application/grpc")
5656
end
5757

58-
# Build a trailers-only error response.
58+
# Make a gRPC error response with status and optional message.
5959
# @parameter status_code [Integer] gRPC status code
6060
# @parameter message [String] Error message
61+
# @parameter error [Exception] Optional error object (used to extract backtrace)
6162
# @returns [Protocol::HTTP::Response]
62-
def trailers_only_error(status_code, message)
63-
Metadata.build_trailers_only_response(
64-
status: status_code,
65-
message: message,
66-
policy: HEADER_POLICY
67-
)
63+
def make_response(status_code, message, error: nil)
64+
headers = Protocol::HTTP::Headers.new([], nil, policy: HEADER_POLICY)
65+
headers["content-type"] = "application/grpc+proto"
66+
Metadata.add_status!(headers, status: status_code, message: message, error: error)
67+
68+
Protocol::HTTP::Response[200, headers, nil]
6869
end
6970
end
7071
end

lib/protocol/grpc/version.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ module GRPC
1010
VERSION = "0.4.0"
1111
end
1212
end
13+

protocol-grpc.gemspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,5 @@ Gem::Specification.new do |spec|
2727
spec.add_dependency "async", "~> 2"
2828
spec.add_dependency "base64"
2929
spec.add_dependency "google-protobuf", "~> 4.0"
30-
spec.add_dependency "protocol-http", "~> 0.28"
30+
spec.add_dependency "protocol-http", "~> 0.56"
3131
end

test/protocol/grpc/error.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,4 @@
8787
end
8888
end
8989
end
90+

0 commit comments

Comments
 (0)