Adding grpc-status header value (adapted for 3.1.x) (#2938)
This commit is contained in:
@@ -23,6 +23,8 @@ import java.util.concurrent.TimeUnit;
|
||||
import io.grpc.Grpc;
|
||||
import io.grpc.Server;
|
||||
import io.grpc.ServerCredentials;
|
||||
import io.grpc.Status;
|
||||
import io.grpc.StatusRuntimeException;
|
||||
import io.grpc.TlsServerCredentials;
|
||||
import io.grpc.stub.StreamObserver;
|
||||
import org.slf4j.Logger;
|
||||
@@ -70,9 +72,6 @@ public class GRPCApplication {
|
||||
private void start() throws IOException {
|
||||
Integer serverPort = environment.getProperty("local.server.port", Integer.class);
|
||||
int grpcPort = serverPort + 1;
|
||||
/*
|
||||
* The port on which the server should run. We run
|
||||
*/
|
||||
ServerCredentials creds = createServerCredentials();
|
||||
server = Grpc.newServerBuilderForPort(grpcPort, creds).addService(new HelloService()).build().start();
|
||||
|
||||
@@ -105,6 +104,13 @@ public class GRPCApplication {
|
||||
|
||||
@Override
|
||||
public void hello(HelloRequest request, StreamObserver<HelloResponse> responseObserver) {
|
||||
if ("failWithRuntimeException!".equals(request.getFirstName())) {
|
||||
StatusRuntimeException exception = Status.FAILED_PRECONDITION.withDescription("Invalid firstName")
|
||||
.asRuntimeException();
|
||||
responseObserver.onError(exception);
|
||||
responseObserver.onCompleted();
|
||||
return;
|
||||
}
|
||||
|
||||
String greeting = String.format("Hello, %s %s", request.getFirstName(), request.getLastName());
|
||||
log.info("Sending response: " + greeting);
|
||||
|
||||
@@ -23,14 +23,17 @@ import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
|
||||
import io.grpc.ManagedChannel;
|
||||
import io.grpc.StatusRuntimeException;
|
||||
import io.grpc.netty.GrpcSslContexts;
|
||||
import io.grpc.netty.NettyChannelBuilder;
|
||||
import org.assertj.core.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.web.server.LocalServerPort;
|
||||
|
||||
import static io.grpc.Status.FAILED_PRECONDITION;
|
||||
import static io.grpc.netty.NegotiationType.TLS;
|
||||
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
|
||||
|
||||
@@ -42,11 +45,18 @@ import static org.springframework.boot.test.context.SpringBootTest.WebEnvironmen
|
||||
public class GRPCApplicationTests {
|
||||
|
||||
@LocalServerPort
|
||||
private int port;
|
||||
private int gatewayPort;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
int grpcServerPort = gatewayPort + 1;
|
||||
final RouteConfigurer configurer = new RouteConfigurer(gatewayPort);
|
||||
configurer.addRoute(grpcServerPort, "/**", null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void gRPCUnaryCalShouldReturnResponse() throws SSLException {
|
||||
ManagedChannel channel = createSecuredChannel(port + 1);
|
||||
public void gRPCUnaryCallShouldReturnResponse() throws SSLException {
|
||||
ManagedChannel channel = createSecuredChannel(gatewayPort);
|
||||
|
||||
final HelloResponse response = HelloServiceGrpc.newBlockingStub(channel)
|
||||
.hello(HelloRequest.newBuilder().setFirstName("Sir").setLastName("FromClient").build());
|
||||
@@ -62,6 +72,20 @@ public class GRPCApplicationTests {
|
||||
.build();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void gRPCUnaryCallShouldHandleRuntimeException() throws SSLException {
|
||||
ManagedChannel channel = createSecuredChannel(gatewayPort);
|
||||
|
||||
try {
|
||||
HelloServiceGrpc.newBlockingStub(channel)
|
||||
.hello(HelloRequest.newBuilder().setFirstName("failWithRuntimeException!").build());
|
||||
}
|
||||
catch (StatusRuntimeException e) {
|
||||
Assertions.assertThat(FAILED_PRECONDITION.getCode()).isEqualTo(e.getStatus().getCode());
|
||||
Assertions.assertThat("Invalid firstName").isEqualTo(e.getStatus().getDescription());
|
||||
}
|
||||
}
|
||||
|
||||
private TrustManager[] createTrustAllTrustManager() {
|
||||
return new TrustManager[] { new X509TrustManager() {
|
||||
public X509Certificate[] getAcceptedIssuers() {
|
||||
|
||||
@@ -19,11 +19,6 @@ package org.springframework.cloud.gateway.tests.grpc;
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
|
||||
@@ -45,12 +40,7 @@ import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.web.server.LocalServerPort;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
|
||||
@@ -63,7 +53,7 @@ import static org.springframework.boot.test.context.SpringBootTest.WebEnvironmen
|
||||
public class JsonToGrpcApplicationTests {
|
||||
|
||||
@LocalServerPort
|
||||
private int port;
|
||||
private int gatewayPort;
|
||||
|
||||
private RestTemplate restTemplate;
|
||||
|
||||
@@ -77,11 +67,12 @@ public class JsonToGrpcApplicationTests {
|
||||
// Since GRPC server and GW run in same instance and don't know server port until
|
||||
// test starts,
|
||||
// we need to configure route dynamically using the actuator endpoint.
|
||||
final RouteConfigurer configurer = new RouteConfigurer(port);
|
||||
configurer.addRoute(port + 1, "/json/hello",
|
||||
final RouteConfigurer configurer = new RouteConfigurer(gatewayPort);
|
||||
int grpcServerPort = gatewayPort + 1;
|
||||
configurer.addRoute(grpcServerPort, "/json/hello",
|
||||
"JsonToGrpc=file:src/main/proto/hello.pb,file:src/main/proto/hello.proto,HelloService,hello");
|
||||
|
||||
String response = restTemplate.postForEntity("https://localhost:" + this.port + "/json/hello",
|
||||
String response = restTemplate.postForEntity("https://localhost:" + this.gatewayPort + "/json/hello",
|
||||
"{\"firstName\":\"Duff\", \"lastName\":\"McKagan\"}", String.class).getBody();
|
||||
|
||||
Assertions.assertThat(response).isNotNull();
|
||||
@@ -111,46 +102,4 @@ public class JsonToGrpcApplicationTests {
|
||||
return new RestTemplate(requestFactory);
|
||||
}
|
||||
|
||||
class RouteConfigurer {
|
||||
|
||||
private final WebTestClient actuatorWebClient;
|
||||
|
||||
private final int actuatorPort;
|
||||
|
||||
RouteConfigurer(int actuatorPort) {
|
||||
this.actuatorPort = actuatorPort;
|
||||
this.actuatorWebClient = WebTestClient.bindToServer().baseUrl("http://localhost:" + actuatorPort).build();
|
||||
|
||||
}
|
||||
|
||||
public void addRoute(int uriPort, String path, String filter) {
|
||||
final String routeId = "test-route-" + UUID.randomUUID();
|
||||
|
||||
Map<String, Object> route = new HashMap<>();
|
||||
route.put("id", routeId);
|
||||
route.put("uri", "http://localhost:" + uriPort);
|
||||
route.put("predicates", Collections.singletonList("Path=" + path));
|
||||
route.put("filters", Arrays.asList(filter));
|
||||
|
||||
ResponseEntity<String> exchange = restTemplate.exchange(url("/actuator/gateway/routes/" + routeId),
|
||||
HttpMethod.POST, new HttpEntity<>(route), String.class);
|
||||
|
||||
assert exchange.getStatusCode() == HttpStatus.CREATED;
|
||||
|
||||
refreshRoutes();
|
||||
}
|
||||
|
||||
private void refreshRoutes() {
|
||||
ResponseEntity<String> exchange = restTemplate.exchange(url("/actuator/gateway/refresh"), HttpMethod.POST,
|
||||
new HttpEntity<>(""), String.class);
|
||||
|
||||
assert exchange.getStatusCode() == HttpStatus.OK;
|
||||
}
|
||||
|
||||
private String url(String context) {
|
||||
return String.format("https://localhost:%s%s", this.actuatorPort, context);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* Copyright 2013-2023 the original author or 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
|
||||
*
|
||||
* https://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.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.gateway.tests.grpc;
|
||||
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
|
||||
import org.apache.http.config.Registry;
|
||||
import org.apache.http.config.RegistryBuilder;
|
||||
import org.apache.http.conn.HttpClientConnectionManager;
|
||||
import org.apache.http.conn.socket.ConnectionSocketFactory;
|
||||
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
|
||||
import org.apache.http.conn.ssl.NoopHostnameVerifier;
|
||||
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
|
||||
import org.apache.http.conn.ssl.TrustStrategy;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.apache.http.impl.client.HttpClients;
|
||||
import org.apache.http.impl.conn.BasicHttpClientConnectionManager;
|
||||
import org.apache.http.ssl.SSLContexts;
|
||||
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
public class RouteConfigurer {
|
||||
|
||||
private final int actuatorPort;
|
||||
|
||||
private final RestTemplate restTemplate;
|
||||
|
||||
RouteConfigurer(int actuatorPort) {
|
||||
this.actuatorPort = actuatorPort;
|
||||
this.restTemplate = createUnsecureClient();
|
||||
}
|
||||
|
||||
public void addRoute(int grpcServerPort, String path, String filter) {
|
||||
final String routeId = "test-route-" + UUID.randomUUID();
|
||||
|
||||
Map<String, Object> route = new HashMap<>();
|
||||
route.put("id", routeId);
|
||||
route.put("uri", "https://localhost:" + grpcServerPort);
|
||||
route.put("predicates", Collections.singletonList("Path=" + path));
|
||||
if (filter != null) {
|
||||
route.put("filters", Arrays.asList(filter));
|
||||
}
|
||||
|
||||
ResponseEntity<String> exchange = restTemplate.exchange(url("/actuator/gateway/routes/" + routeId),
|
||||
HttpMethod.POST, new HttpEntity<>(route), String.class);
|
||||
|
||||
assert exchange.getStatusCode() == HttpStatus.CREATED;
|
||||
|
||||
refreshRoutes();
|
||||
}
|
||||
|
||||
private void refreshRoutes() {
|
||||
ResponseEntity<String> exchange = restTemplate.exchange(url("/actuator/gateway/refresh"), HttpMethod.POST,
|
||||
new HttpEntity<>(""), String.class);
|
||||
|
||||
assert exchange.getStatusCode() == HttpStatus.OK;
|
||||
}
|
||||
|
||||
private String url(String context) {
|
||||
return String.format("https://localhost:%s%s", this.actuatorPort, context);
|
||||
}
|
||||
|
||||
private RestTemplate createUnsecureClient() {
|
||||
TrustStrategy acceptingTrustStrategy = (cert, authType) -> true;
|
||||
SSLContext sslContext;
|
||||
try {
|
||||
sslContext = SSLContexts.custom().loadTrustMaterial(null, acceptingTrustStrategy).build();
|
||||
}
|
||||
catch (NoSuchAlgorithmException | KeyStoreException | KeyManagementException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
SSLConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory(sslContext,
|
||||
NoopHostnameVerifier.INSTANCE);
|
||||
|
||||
Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory>create()
|
||||
.register("https", sslSocketFactory).register("http", new PlainConnectionSocketFactory()).build();
|
||||
|
||||
HttpClientConnectionManager connectionManager = new BasicHttpClientConnectionManager(socketFactoryRegistry);
|
||||
CloseableHttpClient httpClient = HttpClients.custom().setConnectionManager(connectionManager).build();
|
||||
|
||||
HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient);
|
||||
|
||||
return new RestTemplate(requestFactory);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -31,12 +31,16 @@ import org.springframework.web.server.ServerWebExchange;
|
||||
*/
|
||||
public class GRPCResponseHeadersFilter implements HttpHeadersFilter, Ordered {
|
||||
|
||||
private static final String GRPC_STATUS_HEADER = "grpc-status";
|
||||
|
||||
private static final String GRPC_MESSAGE_HEADER = "grpc-message";
|
||||
|
||||
@Override
|
||||
public HttpHeaders filter(HttpHeaders headers, ServerWebExchange exchange) {
|
||||
ServerHttpResponse response = exchange.getResponse();
|
||||
HttpHeaders responseHeaders = response.getHeaders();
|
||||
if (isGRPC(exchange)) {
|
||||
String trailerHeaderValue = "grpc-status";
|
||||
String trailerHeaderValue = GRPC_STATUS_HEADER + "," + GRPC_MESSAGE_HEADER;
|
||||
String originalTrailerHeaderValue = responseHeaders.getFirst(HttpHeaders.TRAILER);
|
||||
if (originalTrailerHeaderValue != null) {
|
||||
trailerHeaderValue += "," + originalTrailerHeaderValue;
|
||||
@@ -47,8 +51,12 @@ public class GRPCResponseHeadersFilter implements HttpHeadersFilter, Ordered {
|
||||
response = ((ServerHttpResponseDecorator) response).getDelegate();
|
||||
}
|
||||
if (response instanceof AbstractServerHttpResponse) {
|
||||
((HttpServerResponse) ((AbstractServerHttpResponse) response).getNativeResponse())
|
||||
.trailerHeaders(h -> h.set("grpc-status", "0"));
|
||||
String grpcStatus = getGrpcStatus(headers);
|
||||
String grpcMessage = getGrpcMessage(headers);
|
||||
((HttpServerResponse) ((AbstractServerHttpResponse) response).getNativeResponse()).trailerHeaders(h -> {
|
||||
h.set(GRPC_STATUS_HEADER, grpcStatus);
|
||||
h.set(GRPC_MESSAGE_HEADER, grpcMessage);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -60,6 +68,16 @@ public class GRPCResponseHeadersFilter implements HttpHeadersFilter, Ordered {
|
||||
return StringUtils.startsWithIgnoreCase(contentTypeValue, "application/grpc");
|
||||
}
|
||||
|
||||
private String getGrpcStatus(HttpHeaders headers) {
|
||||
final String grpcStatusValue = headers.getFirst(GRPC_STATUS_HEADER);
|
||||
return StringUtils.hasText(grpcStatusValue) ? grpcStatusValue : "0";
|
||||
}
|
||||
|
||||
private String getGrpcMessage(HttpHeaders headers) {
|
||||
final String grpcStatusValue = headers.getFirst(GRPC_MESSAGE_HEADER);
|
||||
return StringUtils.hasText(grpcStatusValue) ? grpcStatusValue : "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(Type type) {
|
||||
return Type.RESPONSE.equals(type);
|
||||
|
||||
Reference in New Issue
Block a user