Browse Source

Adding grpc-status header value (adapted for 3.1.x) (#2938)

pull/2943/head
Marta Medio 1 year ago committed by spencergibb
parent
commit
8120303121
No known key found for this signature in database
GPG Key ID: 7788A47380690861
  1. 12
      spring-cloud-gateway-integration-tests/grpc/src/main/java/org/springframework/cloud/gateway/tests/grpc/GRPCApplication.java
  2. 30
      spring-cloud-gateway-integration-tests/grpc/src/test/java/org/springframework/cloud/gateway/tests/grpc/GRPCApplicationTests.java
  3. 61
      spring-cloud-gateway-integration-tests/grpc/src/test/java/org/springframework/cloud/gateway/tests/grpc/JsonToGrpcApplicationTests.java
  4. 114
      spring-cloud-gateway-integration-tests/grpc/src/test/java/org/springframework/cloud/gateway/tests/grpc/RouteConfigurer.java
  5. 24
      spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/headers/GRPCResponseHeadersFilter.java

12
spring-cloud-gateway-integration-tests/grpc/src/main/java/org/springframework/cloud/gateway/tests/grpc/GRPCApplication.java

@ -23,6 +23,8 @@ import java.util.concurrent.TimeUnit; @@ -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 { @@ -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 { @@ -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);

30
spring-cloud-gateway-integration-tests/grpc/src/test/java/org/springframework/cloud/gateway/tests/grpc/GRPCApplicationTests.java

@ -23,14 +23,17 @@ import javax.net.ssl.TrustManager; @@ -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 @@ -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 { @@ -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() {

61
spring-cloud-gateway-integration-tests/grpc/src/test/java/org/springframework/cloud/gateway/tests/grpc/JsonToGrpcApplicationTests.java

@ -19,11 +19,6 @@ package org.springframework.cloud.gateway.tests.grpc; @@ -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; @@ -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 @@ -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 { @@ -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 { @@ -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);
}
}
}

114
spring-cloud-gateway-integration-tests/grpc/src/test/java/org/springframework/cloud/gateway/tests/grpc/RouteConfigurer.java

@ -0,0 +1,114 @@ @@ -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);
}
}

24
spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/headers/GRPCResponseHeadersFilter.java

@ -31,12 +31,16 @@ import org.springframework.web.server.ServerWebExchange; @@ -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 { @@ -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 { @@ -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);

Loading…
Cancel
Save