Skip to content

Commit b0750a8

Browse files
authored
Adds support for gRPC reflection service version v1alpha for tools (like Postman) that don't yet support the latest v1 version. (helidon-io#10122)
1 parent 44e0733 commit b0750a8

File tree

6 files changed

+560
-11
lines changed

6 files changed

+560
-11
lines changed

webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcReflectionFeature.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,8 @@ public void setup(ServerFeatureContext featureContext) {
120120
if (sb.routingBuilders().hasRouting(GrpcRouting.Builder.class)) {
121121
sb.routingBuilders()
122122
.routingBuilder(GrpcRouting.Builder.class)
123-
.service(new GrpcReflectionService(socket));
123+
.service(new GrpcReflectionService(socket))
124+
.service(new GrpcReflectionServiceV1Alpha(socket)); // older version for some tools
124125
} else {
125126
LOGGER.log(Level.WARNING, "Unable to register gRPC reflection service, "
126127
+ "no gRPC routes found for socket " + socket);

webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcReflectionService.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,12 @@
3737
import io.grpc.stub.StreamObserver;
3838

3939
/**
40-
* Grpc reflection service.
40+
* Grpc reflection service version v1. Note the code in this class is almost identical
41+
* to {@link io.helidon.webserver.grpc.GrpcReflectionServiceV1Alpha} except for the
42+
* code-generated protobuf types. The v1alpha will be phased out once more tools
43+
* add support for v1.
44+
*
45+
* @see io.helidon.webserver.grpc.GrpcReflectionServiceV1Alpha
4146
*/
4247
class GrpcReflectionService implements GrpcService {
4348

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
/*
2+
* Copyright (c) 2025 Oracle and/or its affiliates.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.helidon.webserver.grpc;
18+
19+
import java.io.ByteArrayOutputStream;
20+
import java.io.IOException;
21+
import java.io.UncheckedIOException;
22+
import java.util.List;
23+
import java.util.Map;
24+
import java.util.concurrent.ConcurrentHashMap;
25+
26+
import com.google.protobuf.ByteString;
27+
import com.google.protobuf.DescriptorProtos;
28+
import com.google.protobuf.Descriptors;
29+
import io.grpc.Status;
30+
import io.grpc.reflection.v1alpha.ErrorResponse;
31+
import io.grpc.reflection.v1alpha.FileDescriptorResponse;
32+
import io.grpc.reflection.v1alpha.ListServiceResponse;
33+
import io.grpc.reflection.v1alpha.ServerReflectionProto;
34+
import io.grpc.reflection.v1alpha.ServerReflectionRequest;
35+
import io.grpc.reflection.v1alpha.ServerReflectionResponse;
36+
import io.grpc.reflection.v1alpha.ServiceResponse;
37+
import io.grpc.stub.StreamObserver;
38+
39+
/**
40+
* Grpc reflection service version v1alpha. Some tools such as Postman still do not
41+
* support version v1. Once more tools support the new version we can remove support
42+
* for version v1alpha. Note the code in this class is almost identical to
43+
* {@link io.helidon.webserver.grpc.GrpcReflectionService} except for the code-generated
44+
* protobuf types.
45+
*
46+
* @see io.helidon.webserver.grpc.GrpcReflectionService
47+
*/
48+
class GrpcReflectionServiceV1Alpha implements GrpcService {
49+
50+
/**
51+
* Caches FileDescriptorProto representations as byte strings to avoid serialization
52+
* on every reflection request.
53+
*/
54+
private static final Map<String, ByteString> FILE_DESCRIPTOR_CACHE = new ConcurrentHashMap<>();
55+
56+
private final String socket;
57+
58+
GrpcReflectionServiceV1Alpha(String socket) {
59+
this.socket = socket;
60+
}
61+
62+
@Override
63+
public Descriptors.FileDescriptor proto() {
64+
return ServerReflectionProto.getDescriptor();
65+
}
66+
67+
@Override
68+
public String serviceName() {
69+
List<Descriptors.ServiceDescriptor> services = proto().getServices();
70+
return services.getFirst().getFullName(); // only one service
71+
}
72+
73+
@Override
74+
public void update(Routing router) {
75+
router.bidi("ServerReflectionInfo", this::serverReflectionInfo);
76+
}
77+
78+
private StreamObserver<ServerReflectionRequest> serverReflectionInfo(StreamObserver<ServerReflectionResponse> res) {
79+
return new StreamObserver<>() {
80+
@Override
81+
public void onNext(ServerReflectionRequest req) {
82+
res.onNext(processRequest(req));
83+
}
84+
85+
@Override
86+
public void onError(Throwable t) {
87+
res.onError(t);
88+
}
89+
90+
@Override
91+
public void onCompleted() {
92+
res.onCompleted();
93+
}
94+
};
95+
}
96+
97+
private ServerReflectionResponse processRequest(ServerReflectionRequest req) {
98+
return switch (req.getMessageRequestCase().getNumber()) {
99+
case ServerReflectionRequest.LIST_SERVICES_FIELD_NUMBER -> listServices();
100+
case ServerReflectionRequest.FILE_BY_FILENAME_FIELD_NUMBER -> findFile(req);
101+
case ServerReflectionRequest.FILE_CONTAINING_SYMBOL_FIELD_NUMBER -> findSymbol(req);
102+
case ServerReflectionRequest.FILE_CONTAINING_EXTENSION_FIELD_NUMBER -> findExtensionField(req);
103+
default -> notImplemented();
104+
};
105+
}
106+
107+
private ServerReflectionResponse listServices() {
108+
List<GrpcRouting> grpcRoutings = GrpcReflectionFeature.socketGrpcRoutings().get(socket);
109+
110+
ListServiceResponse.Builder builder = ListServiceResponse.newBuilder();
111+
for (GrpcRouting grpcRouting : grpcRoutings) {
112+
for (GrpcRoute grpcRoute : grpcRouting.routes()) {
113+
builder.addService(ServiceResponse.newBuilder().setName(grpcRoute.serviceName()).build());
114+
}
115+
}
116+
return ServerReflectionResponse.newBuilder().setListServicesResponse(builder).build();
117+
}
118+
119+
private ServerReflectionResponse findFile(ServerReflectionRequest req) {
120+
String fileName = req.getFileByFilename();
121+
String cachedFileNameKey = "/" + fileName; // not a legal identifier
122+
ByteString byteString = FILE_DESCRIPTOR_CACHE.get(cachedFileNameKey);
123+
if (byteString != null) {
124+
return fileDescResponse(byteString);
125+
}
126+
127+
List<GrpcRouting> grpcRoutings = GrpcReflectionFeature.socketGrpcRoutings().get(socket);
128+
for (GrpcRouting grpcRouting : grpcRoutings) {
129+
for (GrpcRoute grpcRoute : grpcRouting.routes()) {
130+
Descriptors.FileDescriptor fileDesc = grpcRoute.proto();
131+
if (fileDesc.getFile().getFullName().equals(fileName)) {
132+
return symbolFound(fileDesc, cachedFileNameKey);
133+
}
134+
}
135+
}
136+
return notFound("Unable to find file name " + fileName);
137+
}
138+
139+
private ServerReflectionResponse findSymbol(ServerReflectionRequest req) {
140+
String symbol = req.getFileContainingSymbol();
141+
ByteString byteString = FILE_DESCRIPTOR_CACHE.get(symbol);
142+
if (byteString != null) {
143+
return fileDescResponse(byteString);
144+
}
145+
146+
List<GrpcRouting> grpcRoutings = GrpcReflectionFeature.socketGrpcRoutings().get(socket);
147+
for (GrpcRouting grpcRouting : grpcRoutings) {
148+
for (GrpcRoute grpcRoute : grpcRouting.routes()) {
149+
Descriptors.FileDescriptor fileDesc = grpcRoute.proto();
150+
151+
// scan through services and methods
152+
List<Descriptors.ServiceDescriptor> services = fileDesc.getServices();
153+
for (Descriptors.ServiceDescriptor service : services) {
154+
if (service.getFullName().equals(symbol)) {
155+
return symbolFound(fileDesc, symbol);
156+
}
157+
List<Descriptors.MethodDescriptor> methods = service.getMethods();
158+
for (Descriptors.MethodDescriptor method : methods) {
159+
if (method.getFullName().equals(symbol)) {
160+
return symbolFound(fileDesc, symbol);
161+
}
162+
}
163+
}
164+
165+
// scan through message types
166+
List<Descriptors.Descriptor> types = fileDesc.getMessageTypes();
167+
for (Descriptors.Descriptor type : types) {
168+
if (type.getFullName().equals(symbol)) {
169+
return symbolFound(fileDesc, symbol);
170+
}
171+
}
172+
}
173+
}
174+
return notFound("Unable to find proto file for " + symbol);
175+
}
176+
177+
private ServerReflectionResponse findExtensionField(ServerReflectionRequest req) {
178+
String type = req.getFileContainingExtension().getContainingType();
179+
int number = req.getFileContainingExtension().getExtensionNumber();
180+
String cachedFileNameKey = number + type; // not a legal identifier
181+
ByteString byteString = FILE_DESCRIPTOR_CACHE.get(cachedFileNameKey);
182+
if (byteString != null) {
183+
return fileDescResponse(byteString);
184+
}
185+
186+
List<GrpcRouting> grpcRoutings = GrpcReflectionFeature.socketGrpcRoutings().get(socket);
187+
for (GrpcRouting grpcRouting : grpcRoutings) {
188+
for (GrpcRoute grpcRoute : grpcRouting.routes()) {
189+
Descriptors.FileDescriptor fileDesc = grpcRoute.proto();
190+
List<Descriptors.FieldDescriptor> extensions = fileDesc.getExtensions();
191+
for (Descriptors.FieldDescriptor extension : extensions) {
192+
if (extension.getContainingType().getFullName().equals(type)
193+
&& extension.toProto().getNumber() == number) {
194+
return symbolFound(fileDesc, cachedFileNameKey);
195+
}
196+
}
197+
}
198+
}
199+
return notFound("Unable to find proto file for " + type);
200+
}
201+
202+
private ServerReflectionResponse symbolFound(Descriptors.FileDescriptor fileDesc, String symbol) {
203+
ByteString byteString;
204+
DescriptorProtos.FileDescriptorProto proto = fileDesc.toProto();
205+
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
206+
proto.writeTo(baos);
207+
byteString = ByteString.copyFrom(baos.toByteArray());
208+
} catch (IOException e) {
209+
throw new UncheckedIOException(e);
210+
}
211+
ByteString cachedValue = FILE_DESCRIPTOR_CACHE.putIfAbsent(symbol, byteString);
212+
return fileDescResponse(cachedValue != null ? cachedValue : byteString);
213+
}
214+
215+
private ServerReflectionResponse fileDescResponse(ByteString byteString) {
216+
FileDescriptorResponse.Builder builder = FileDescriptorResponse.newBuilder();
217+
builder.addFileDescriptorProto(byteString);
218+
return ServerReflectionResponse.newBuilder().setFileDescriptorResponse(builder.build()).build();
219+
}
220+
221+
private ServerReflectionResponse notImplemented() {
222+
return ServerReflectionResponse.newBuilder().setErrorResponse(
223+
ErrorResponse.newBuilder().setErrorCode(Status.UNIMPLEMENTED.getCode().value())
224+
.setErrorMessage("Reflection request not implemented").build()).build();
225+
}
226+
227+
private ServerReflectionResponse notFound(String message) {
228+
return ServerReflectionResponse.newBuilder().setErrorResponse(
229+
ErrorResponse.newBuilder().setErrorCode(Status.NOT_FOUND.getCode().value())
230+
.setErrorMessage(message).build()).build();
231+
}
232+
}
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
// Copyright 2016 gRPC authors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// Service exported by server reflection
16+
17+
syntax = "proto3";
18+
19+
package grpc.reflection.v1alpha;
20+
21+
option go_package = "google.golang.org/grpc/reflection/grpc_reflection_v1alpha";
22+
option java_multiple_files = true;
23+
option java_package = "io.grpc.reflection.v1alpha";
24+
option java_outer_classname = "ServerReflectionProto";
25+
26+
service ServerReflection {
27+
// The reflection service is structured as a bidirectional stream, ensuring
28+
// all related requests go to a single server.
29+
rpc ServerReflectionInfo(stream ServerReflectionRequest)
30+
returns (stream ServerReflectionResponse);
31+
}
32+
33+
// The message sent by the client when calling ServerReflectionInfo method.
34+
message ServerReflectionRequest {
35+
string host = 1;
36+
// To use reflection service, the client should set one of the following
37+
// fields in message_request. The server distinguishes requests by their
38+
// defined field and then handles them using corresponding methods.
39+
oneof message_request {
40+
// Find a proto file by the file name.
41+
string file_by_filename = 3;
42+
43+
// Find the proto file that declares the given fully-qualified symbol name.
44+
// This field should be a fully-qualified symbol name
45+
// (e.g. <package>.<service>[.<method>] or <package>.<type>).
46+
string file_containing_symbol = 4;
47+
48+
// Find the proto file which defines an extension extending the given
49+
// message type with the given field number.
50+
ExtensionRequest file_containing_extension = 5;
51+
52+
// Finds the tag numbers used by all known extensions of the given message
53+
// type, and appends them to ExtensionNumberResponse in an undefined order.
54+
// Its corresponding method is best-effort: it's not guaranteed that the
55+
// reflection service will implement this method, and it's not guaranteed
56+
// that this method will provide all extensions. Returns
57+
// StatusCode::UNIMPLEMENTED if it's not implemented.
58+
// This field should be a fully-qualified type name. The format is
59+
// <package>.<type>
60+
string all_extension_numbers_of_type = 6;
61+
62+
// List the full names of registered services. The content will not be
63+
// checked.
64+
string list_services = 7;
65+
}
66+
}
67+
68+
// The type name and extension number sent by the client when requesting
69+
// file_containing_extension.
70+
message ExtensionRequest {
71+
// Fully-qualified type name. The format should be <package>.<type>
72+
string containing_type = 1;
73+
int32 extension_number = 2;
74+
}
75+
76+
// The message sent by the server to answer ServerReflectionInfo method.
77+
message ServerReflectionResponse {
78+
string valid_host = 1;
79+
ServerReflectionRequest original_request = 2;
80+
// The server set one of the following fields accroding to the message_request
81+
// in the request.
82+
oneof message_response {
83+
// This message is used to answer file_by_filename, file_containing_symbol,
84+
// file_containing_extension requests with transitive dependencies. As
85+
// the repeated label is not allowed in oneof fields, we use a
86+
// FileDescriptorResponse message to encapsulate the repeated fields.
87+
// The reflection service is allowed to avoid sending FileDescriptorProtos
88+
// that were previously sent in response to earlier requests in the stream.
89+
FileDescriptorResponse file_descriptor_response = 4;
90+
91+
// This message is used to answer all_extension_numbers_of_type requst.
92+
ExtensionNumberResponse all_extension_numbers_response = 5;
93+
94+
// This message is used to answer list_services request.
95+
ListServiceResponse list_services_response = 6;
96+
97+
// This message is used when an error occurs.
98+
ErrorResponse error_response = 7;
99+
}
100+
}
101+
102+
// Serialized FileDescriptorProto messages sent by the server answering
103+
// a file_by_filename, file_containing_symbol, or file_containing_extension
104+
// request.
105+
message FileDescriptorResponse {
106+
// Serialized FileDescriptorProto messages. We avoid taking a dependency on
107+
// descriptor.proto, which uses proto2 only features, by making them opaque
108+
// bytes instead.
109+
repeated bytes file_descriptor_proto = 1;
110+
}
111+
112+
// A list of extension numbers sent by the server answering
113+
// all_extension_numbers_of_type request.
114+
message ExtensionNumberResponse {
115+
// Full name of the base type, including the package name. The format
116+
// is <package>.<type>
117+
string base_type_name = 1;
118+
repeated int32 extension_number = 2;
119+
}
120+
121+
// A list of ServiceResponse sent by the server answering list_services request.
122+
message ListServiceResponse {
123+
// The information of each service may be expanded in the future, so we use
124+
// ServiceResponse message to encapsulate it.
125+
repeated ServiceResponse service = 1;
126+
}
127+
128+
// The information of a single service used by ListServiceResponse to answer
129+
// list_services request.
130+
message ServiceResponse {
131+
// Full name of a registered service, including its package name. The format
132+
// is <package>.<service>
133+
string name = 1;
134+
}
135+
136+
// The error code and error message sent by the server when an error occurs.
137+
message ErrorResponse {
138+
// This field uses the error codes defined in grpc::StatusCode.
139+
int32 error_code = 1;
140+
string error_message = 2;
141+
}

0 commit comments

Comments
 (0)