diff --git a/spring-cloud-gateway-integration-tests/grpc/pom.xml b/spring-cloud-gateway-integration-tests/grpc/pom.xml
new file mode 100644
index 00000000..9cf1131c
--- /dev/null
+++ b/spring-cloud-gateway-integration-tests/grpc/pom.xml
@@ -0,0 +1,104 @@
+
+
+ 4.0.0
+
+ grpc
+ jar
+
+ Spring Cloud Gateway gRPC Integration Test
+ Spring Cloud Gateway gRPC Integration Test
+
+
+
+
+
+ org.springframework.cloud
+ spring-cloud-gateway-integration-tests
+ 3.1.0-SNAPSHOT
+ ..
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-webflux
+
+
+ org.springframework.cloud
+ spring-cloud-starter-gateway
+
+
+
+ io.grpc
+ grpc-netty-shaded
+ 1.41.0
+
+
+ io.grpc
+ grpc-protobuf
+ 1.41.0
+
+
+ io.grpc
+ grpc-stub
+ 1.41.0
+
+
+ io.netty
+ netty-tcnative-boringssl-static
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ io.projectreactor
+ reactor-test
+ test
+
+
+ org.assertj
+ assertj-core
+ test
+
+
+
+
+
+ kr.motd.maven
+ os-maven-plugin
+ 1.6.2
+
+
+
+
+ maven-deploy-plugin
+
+ true
+
+
+
+ org.xolstice.maven.plugins
+ protobuf-maven-plugin
+ 0.6.1
+
+ com.google.protobuf:protoc:3.17.3:exe:${os.detected.classifier}
+ grpc-java
+ io.grpc:protoc-gen-grpc-java:1.41.0:exe:${os.detected.classifier}
+
+
+
+
+ compile
+ compile-custom
+
+
+
+
+
+
+
+
diff --git a/spring-cloud-gateway-integration-tests/grpc/src/main/java/org/springframework/cloud/gateway/tests/grpc/GRPCApplication.java b/spring-cloud-gateway-integration-tests/grpc/src/main/java/org/springframework/cloud/gateway/tests/grpc/GRPCApplication.java
new file mode 100644
index 00000000..5436d0cc
--- /dev/null
+++ b/spring-cloud-gateway-integration-tests/grpc/src/main/java/org/springframework/cloud/gateway/tests/grpc/GRPCApplication.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2013-2021 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.io.File;
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+
+import io.grpc.Grpc;
+import io.grpc.Server;
+import io.grpc.ServerCredentials;
+import io.grpc.TlsServerCredentials;
+import io.grpc.stub.StreamObserver;
+
+import org.springframework.boot.ApplicationArguments;
+import org.springframework.boot.ApplicationRunner;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.SpringBootConfiguration;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.cloud.gateway.route.RouteLocator;
+import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
+import org.springframework.context.annotation.Bean;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.stereotype.Component;
+import org.springframework.util.SocketUtils;
+
+/**
+ * @author Alberto C. Ríos
+ */
+@SpringBootConfiguration
+@EnableAutoConfiguration
+public class GRPCApplication {
+
+ private static final int GRPC_SERVER_PORT = SocketUtils.findAvailableTcpPort();
+
+ public static void main(String[] args) {
+ SpringApplication.run(GRPCApplication.class, args);
+ }
+
+ @Bean
+ public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
+ return builder.routes().route("grpc", r -> r.predicate(p -> true).uri("https://localhost:" + GRPC_SERVER_PORT))
+ .build();
+ }
+
+ @Component
+ static class GRPCServer implements ApplicationRunner {
+
+ private Server server;
+
+ @Override
+ public void run(ApplicationArguments args) throws Exception {
+ final GRPCServer server = new GRPCServer();
+ server.start();
+ }
+
+ private void start() throws Exception {
+ /* The port on which the server should run */
+ ServerCredentials creds = createServerCredentials();
+ server = Grpc.newServerBuilderForPort(GRPC_SERVER_PORT, creds).addService(new HelloService()).build()
+ .start();
+
+ System.out.println("Starting server in port " + GRPC_SERVER_PORT);
+
+ Runtime.getRuntime().addShutdownHook(new Thread(() -> {
+ try {
+ GRPCServer.this.stop();
+ }
+ catch (InterruptedException e) {
+ e.printStackTrace(System.err);
+ }
+ }));
+ }
+
+ private ServerCredentials createServerCredentials() throws IOException {
+ File privateKey = new ClassPathResource("private.key").getFile();
+ File certChain = new ClassPathResource("certificate.pem").getFile();
+ return TlsServerCredentials.create(certChain, privateKey);
+ }
+
+ private void stop() throws InterruptedException {
+ if (server != null) {
+ server.shutdown().awaitTermination(30, TimeUnit.SECONDS);
+ }
+ }
+
+ static class HelloService extends HelloServiceGrpc.HelloServiceImplBase {
+
+ @Override
+ public void hello(HelloRequest request, StreamObserver responseObserver) {
+
+ String greeting = "Hello, " + request.getFirstName() + " " + request.getLastName();
+ System.out.println(greeting);
+
+ HelloResponse response = HelloResponse.newBuilder().setGreeting(greeting).build();
+
+ responseObserver.onNext(response);
+ responseObserver.onCompleted();
+ }
+
+ }
+
+ }
+
+}
diff --git a/spring-cloud-gateway-integration-tests/grpc/src/main/proto/hello.proto b/spring-cloud-gateway-integration-tests/grpc/src/main/proto/hello.proto
new file mode 100644
index 00000000..0a7769a7
--- /dev/null
+++ b/spring-cloud-gateway-integration-tests/grpc/src/main/proto/hello.proto
@@ -0,0 +1,16 @@
+syntax = "proto3";
+option java_multiple_files = true;
+option java_package = "org.springframework.cloud.gateway.tests.grpc";
+
+message HelloRequest {
+ string firstName = 1;
+ string lastName = 2;
+}
+
+message HelloResponse {
+ string greeting = 1;
+}
+
+service HelloService {
+ rpc hello(HelloRequest) returns (HelloResponse);
+}
diff --git a/spring-cloud-gateway-integration-tests/grpc/src/main/resources/application.yml b/spring-cloud-gateway-integration-tests/grpc/src/main/resources/application.yml
new file mode 100644
index 00000000..e69de29b
diff --git a/spring-cloud-gateway-integration-tests/grpc/src/main/resources/certificate.pem b/spring-cloud-gateway-integration-tests/grpc/src/main/resources/certificate.pem
new file mode 100644
index 00000000..808fb06c
--- /dev/null
+++ b/spring-cloud-gateway-integration-tests/grpc/src/main/resources/certificate.pem
@@ -0,0 +1,21 @@
+-----BEGIN CERTIFICATE-----
+MIIDdzCCAl+gAwIBAgIEIon96DANBgkqhkiG9w0BAQsFADBsMRAwDgYDVQQGEwdV
+bmtub3duMRAwDgYDVQQIEwdVbmtub3duMRAwDgYDVQQHEwdVbmtub3duMRAwDgYD
+VQQKEwdVbmtub3duMRAwDgYDVQQLEwdVbmtub3duMRAwDgYDVQQDEwdVbmtub3du
+MB4XDTIxMDkyNzE2NDQwNVoXDTMxMDkyNTE2NDQwNVowbDEQMA4GA1UEBhMHVW5r
+bm93bjEQMA4GA1UECBMHVW5rbm93bjEQMA4GA1UEBxMHVW5rbm93bjEQMA4GA1UE
+ChMHVW5rbm93bjEQMA4GA1UECxMHVW5rbm93bjEQMA4GA1UEAxMHVW5rbm93bjCC
+ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAIwevGHY30YdoUdCSB/5q7/F
+c0KHetdjb71G2u6vFdeNvSwMpCV8Z1JznOJ/1zuY+0Z105QCPM7fi1ACi+tqxDR+
+L7yjUHhUEMTiGgCHcYJIZZCYfWS3BQXVxgORXhDv7RduCUaCLnkaFY++iPMTUy0C
+VxIkplIEhAmqcikIgWaa5ZjBkegKgahlPQLKlfD4Rz/kq2P+LLYFsHNNdKfWv6XQ
+u4LMw7ZEAJtfdpaMTzmtQipbTt6Dh87vIa0CIVnCPdlQ3o/5WeaxEA1pnfOLas07
+1VdHih2nC5vHhQcTPQDfa+uwzQvzHrchjuvMUUZaCYJzuT0G6nbGBba54EBT7yUC
+AwEAAaMhMB8wHQYDVR0OBBYEFKOHmfytP5ab2C4iFHlSklu6tCcuMA0GCSqGSIb3
+DQEBCwUAA4IBAQAdgWwdOtRbI796Z22weTBc0/tM8kLc6G0raNb08WyZMPZVki04
+jPh73pPQCgYeI/pq5JqH46KgvehmygTzpWDAFIllW0kgABVw3Nu6duV+blt1JG8T
+lWP7t5A+qDXgPDm3I5diii7O1YlLB3I37XiBdEV/+2WmF1VGQ7uBWAv+uotQeuW2
+JvHOr4ICOiW45TzRYtAbzWukSYKg/A7lwBs7HE9KVomUxNrkD+7+ugRuy/31pyen
+pHsEJQpx5juFRE222B6GXmX0w9xLIOapytl4EoPUx3K8Ecc+yI2q00UUC43x0v28
+c05YqwRZ5vp+jUnRxkxaz85YdArfR7QFYWtO
+-----END CERTIFICATE-----
diff --git a/spring-cloud-gateway-integration-tests/grpc/src/main/resources/keystore.p12 b/spring-cloud-gateway-integration-tests/grpc/src/main/resources/keystore.p12
new file mode 100644
index 00000000..b22b3349
Binary files /dev/null and b/spring-cloud-gateway-integration-tests/grpc/src/main/resources/keystore.p12 differ
diff --git a/spring-cloud-gateway-integration-tests/grpc/src/main/resources/private.key b/spring-cloud-gateway-integration-tests/grpc/src/main/resources/private.key
new file mode 100644
index 00000000..1db2b3cb
--- /dev/null
+++ b/spring-cloud-gateway-integration-tests/grpc/src/main/resources/private.key
@@ -0,0 +1,32 @@
+Bag Attributes
+ friendlyName: mykey
+ localKeyID: 54 69 6D 65 20 31 36 33 32 38 32 34 38 37 31 33 33 35
+Key Attributes:
+-----BEGIN PRIVATE KEY-----
+MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCMHrxh2N9GHaFH
+Qkgf+au/xXNCh3rXY2+9RtrurxXXjb0sDKQlfGdSc5zif9c7mPtGddOUAjzO34tQ
+AovrasQ0fi+8o1B4VBDE4hoAh3GCSGWQmH1ktwUF1cYDkV4Q7+0XbglGgi55GhWP
+vojzE1MtAlcSJKZSBIQJqnIpCIFmmuWYwZHoCoGoZT0CypXw+Ec/5Ktj/iy2BbBz
+TXSn1r+l0LuCzMO2RACbX3aWjE85rUIqW07eg4fO7yGtAiFZwj3ZUN6P+VnmsRAN
+aZ3zi2rNO9VXR4odpwubx4UHEz0A32vrsM0L8x63IY7rzFFGWgmCc7k9Bup2xgW2
+ueBAU+8lAgMBAAECggEAGu2xQJDAYCZDn4FCgTqnYkSdIRUOa6SFjfe3DZYCeZmY
+2IVZaobdCICFjxYIlECTUfhFADXp38wgZvEGWOj86iWyIOu2BFoLmvrlCmL9Uo99
+TWuw9ZEi2vs5gegHDvQ9OXqBN9a+/bEgoa55fVWib4z6lNcMS8joYz8pj28+ByzE
+LW0/3T3p6beM2fUcCJWn3d2M3wUgSuXmcdjVXJSkhEwKTVcc4vTTcOeF6xb2VZ/g
+Pozv/39G1qZ+QtM58yBiqnJ1Z2gtAk71l/1ztQa3uY22gzw8Kj5dmqcHgdiN5DWI
+bNE+k5Q93FzUmZNYPzmY6YVkdEzaNMtmi96sdtEu5QKBgQD36YdGXUyoRXeNDRuc
+yMXr6j/9/ewii+byHhFoUvjLuXWIQ06V+nOtYqohg/zgGIiC5LQ8EB0uFZDMhbDf
+kSwtoXbpUDLYD2OPgIyLaPqHiYQ9BampbUz5vHlfYLr0vB+Xp5r22Eb6nDQRRtb5
+EXHoYgAokgpYdeTIcdKRcB/2DwKBgQCQsPePeu7PagM9vYEo4zfbFxfY3Qkr4lOQ
+BCZ/tgsS0b0jAAxpfUH0/3O5oXizmB/5K7vgKqGnTuBWJJ/hdCWq27FkKxJ+8ejU
+8V9TFd5VQ89VeB5OekZwPks8vftwzW4L82ZRW5hvyQB0jPR+lwqjrNqds+xxH7i+
+c+RFx15biwKBgQCgkGGK0zao7YUGl+zAWNDHgQo9KM5deZr0SUEg/kwhNlbHEEC/
+plxxeauS1XdcdMdFb3bER/N+O31y2Uu7IL0qOJ9ZcRXdFep3sNxWFoHcctZw51AB
+accnIEjD21R62bTkditJoL4n5i9a2TS2T/QkfAR6QkvtCz5IDGBCzgoFRQKBgC51
+VBfy3gklPgMt/PHW+1FSuep9FnvLwQ8F9iKdnjKdu8AoPNQGTw5Ok6bwDOSFnQaR
+n1Kb/anN7sRaICfw9kNFJVFHbznpjNwK4JO5+tif3EvSNND3+/QAXIIVck3G+GXH
+8nt/EJQcExRZSgv3jYf+cXeflPTBvb0RUyOAn3B/AoGAM9aUi2dg3XjlDkZahQlY
+P5QNIr9BY25Ordga3GLwfR6rE+jiWTeTmreXBSJ7nvcaEQUNyFMX9n3v/QI84etk
+lMJkZ4o2TgzRnCsFJoBV36Ihsa6B5uXVWAvMRLKvwKpptKXfO0TnhUg7oKtN4t3a
+/FzAOS2Eu1PFP27z74gAMKY=
+-----END PRIVATE KEY-----
diff --git a/spring-cloud-gateway-integration-tests/grpc/src/test/java/org/springframework/cloud/gateway/tests/grpc/GRPCApplicationTests.java b/spring-cloud-gateway-integration-tests/grpc/src/test/java/org/springframework/cloud/gateway/tests/grpc/GRPCApplicationTests.java
new file mode 100644
index 00000000..f4e9ae34
--- /dev/null
+++ b/spring-cloud-gateway-integration-tests/grpc/src/test/java/org/springframework/cloud/gateway/tests/grpc/GRPCApplicationTests.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2013-2021 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.cert.X509Certificate;
+
+import javax.net.ssl.SSLException;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509TrustManager;
+
+import io.grpc.ManagedChannel;
+import io.grpc.netty.shaded.io.grpc.netty.GrpcSslContexts;
+import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.system.OutputCaptureExtension;
+import org.springframework.boot.web.server.LocalServerPort;
+import org.springframework.test.annotation.DirtiesContext;
+
+import static io.grpc.netty.shaded.io.grpc.netty.NegotiationType.TLS;
+import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
+
+/**
+ * @author Alberto C. Ríos
+ */
+@ExtendWith(OutputCaptureExtension.class)
+@SpringBootTest(classes = GRPCApplication.class, webEnvironment = WebEnvironment.RANDOM_PORT)
+@DirtiesContext
+public class GRPCApplicationTests {
+
+ @LocalServerPort
+ private int port;
+
+ @Test
+ public void gRPCUnaryCalShouldReturnResponse() throws SSLException {
+ ManagedChannel channel = createSecuredChannel(port);
+
+ final HelloResponse response = HelloServiceGrpc.newBlockingStub(channel)
+ .hello(HelloRequest.newBuilder().setFirstName("Sir").setLastName("FromClient").build());
+
+ Assertions.assertThat(response.getGreeting()).isEqualTo("Hello, Sir FromClient");
+ }
+
+ private ManagedChannel createSecuredChannel(int port) throws SSLException {
+ TrustManager[] trustAllCerts = createTrustAllTrustManager();
+
+ return NettyChannelBuilder.forAddress("localhost", port).useTransportSecurity()
+ .sslContext(GrpcSslContexts.forClient().trustManager(trustAllCerts[0]).build()).negotiationType(TLS)
+ .build();
+ }
+
+ private TrustManager[] createTrustAllTrustManager() {
+ return new TrustManager[] { new X509TrustManager() {
+ public X509Certificate[] getAcceptedIssuers() {
+ return new X509Certificate[0];
+ }
+
+ public void checkClientTrusted(X509Certificate[] certs, String authType) {
+ }
+
+ public void checkServerTrusted(X509Certificate[] certs, String authType) {
+ }
+ } };
+ }
+
+}
diff --git a/spring-cloud-gateway-integration-tests/grpc/src/test/resources/application.yml b/spring-cloud-gateway-integration-tests/grpc/src/test/resources/application.yml
new file mode 100644
index 00000000..ba0449a3
--- /dev/null
+++ b/spring-cloud-gateway-integration-tests/grpc/src/test/resources/application.yml
@@ -0,0 +1,27 @@
+server:
+ http2:
+ enabled: true
+ ssl:
+ key-store-type: PKCS12
+ key-store: classpath:keystore.p12
+ key-store-password: password
+ key-password: password
+ enabled: true
+spring:
+ cloud:
+ gateway:
+ httpserver:
+ wiretap: true
+ httpclient:
+ wiretap: true
+ ssl:
+ key-store-type: PKCS12
+ key-store: classpath:keystore.p12
+ key-store-password: password
+ key-password: password
+ use-insecure-trust-manager: true
+
+logging:
+ level:
+ reactor.netty: DEBUG
+ org.springframework.cloud.gateway.filter: TRACE
diff --git a/spring-cloud-gateway-integration-tests/pom.xml b/spring-cloud-gateway-integration-tests/pom.xml
index f0197462..76b5c350 100644
--- a/spring-cloud-gateway-integration-tests/pom.xml
+++ b/spring-cloud-gateway-integration-tests/pom.xml
@@ -21,6 +21,7 @@
+ grpc
http2
mvc-failure-analyzer
diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java
index 60c9634b..2df6f8e5 100644
--- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java
+++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java
@@ -109,6 +109,8 @@ import org.springframework.cloud.gateway.filter.factory.rewrite.MessageBodyEncod
import org.springframework.cloud.gateway.filter.factory.rewrite.ModifyRequestBodyGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.rewrite.ModifyResponseBodyGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.headers.ForwardedHeadersFilter;
+import org.springframework.cloud.gateway.filter.headers.GRPCRequestHeadersFilter;
+import org.springframework.cloud.gateway.filter.headers.GRPCResponseHeadersFilter;
import org.springframework.cloud.gateway.filter.headers.HttpHeadersFilter;
import org.springframework.cloud.gateway.filter.headers.RemoveHopByHopHeadersFilter;
import org.springframework.cloud.gateway.filter.headers.XForwardedHeadersFilter;
@@ -176,6 +178,7 @@ import static org.springframework.cloud.gateway.config.HttpClientProperties.Pool
* @author Spencer Gibb
* @author Ziemowit Stolarczyk
* @author Mete Alpaslan Katırcıoğlu
+ * @author Alberto C. Ríos
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(name = "spring.cloud.gateway.enabled", matchIfMissing = true)
@@ -290,6 +293,18 @@ public class GatewayAutoConfiguration {
return new XForwardedHeadersFilter();
}
+ @Bean
+ @ConditionalOnProperty(name = "server.http2.enabled", matchIfMissing = true)
+ public GRPCRequestHeadersFilter gRPCRequestHeadersFilter() {
+ return new GRPCRequestHeadersFilter();
+ }
+
+ @Bean
+ @ConditionalOnProperty(name = "server.http2.enabled", matchIfMissing = true)
+ public GRPCResponseHeadersFilter gRPCResponseHeadersFilter() {
+ return new GRPCResponseHeadersFilter();
+ }
+
// GlobalFilter beans
@Bean
diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayProperties.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayProperties.java
index 594f6d6e..46b07d61 100644
--- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayProperties.java
+++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayProperties.java
@@ -60,7 +60,8 @@ public class GatewayProperties {
private List defaultFilters = new ArrayList<>();
private List streamingMediaTypes = Arrays.asList(MediaType.TEXT_EVENT_STREAM,
- MediaType.APPLICATION_STREAM_JSON);
+ MediaType.APPLICATION_STREAM_JSON, new MediaType("application", "grpc"),
+ new MediaType("application", "grpc+protobuf"), new MediaType("application", "grpc+json"));
/**
* Option to fail on route definition errors, defaults to true. Otherwise, a warning
diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/headers/GRPCRequestHeadersFilter.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/headers/GRPCRequestHeadersFilter.java
new file mode 100644
index 00000000..3a98a88f
--- /dev/null
+++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/headers/GRPCRequestHeadersFilter.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2013-2020 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.filter.headers;
+
+import java.util.List;
+import java.util.Map;
+
+import org.springframework.core.Ordered;
+import org.springframework.http.HttpHeaders;
+import org.springframework.util.StringUtils;
+import org.springframework.web.server.ServerWebExchange;
+
+/**
+ * @author Alberto C. Ríos
+ */
+public class GRPCRequestHeadersFilter implements HttpHeadersFilter, Ordered {
+
+ @Override
+ public HttpHeaders filter(HttpHeaders headers, ServerWebExchange exchange) {
+ HttpHeaders updated = new HttpHeaders();
+
+ for (Map.Entry> entry : headers.entrySet()) {
+ updated.addAll(entry.getKey(), entry.getValue());
+ }
+
+ // https://datatracker.ietf.org/doc/html/rfc7540#section-8.1.2.2
+ if (isGRPC(headers.getFirst(HttpHeaders.CONTENT_TYPE))) {
+ updated.add("te", "trailers");
+ }
+ return updated;
+ }
+
+ private boolean isGRPC(String contentTypeValue) {
+ return StringUtils.startsWithIgnoreCase(contentTypeValue, "application/grpc");
+ }
+
+ @Override
+ public boolean supports(Type type) {
+ return Type.REQUEST.equals(type);
+ }
+
+ @Override
+ public int getOrder() {
+ return 0;
+ }
+
+}
diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/headers/GRPCResponseHeadersFilter.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/headers/GRPCResponseHeadersFilter.java
new file mode 100644
index 00000000..f8373677
--- /dev/null
+++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/headers/GRPCResponseHeadersFilter.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2013-2020 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.filter.headers;
+
+import reactor.netty.http.server.HttpServerResponse;
+
+import org.springframework.core.Ordered;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.server.reactive.AbstractServerHttpResponse;
+import org.springframework.http.server.reactive.ServerHttpResponse;
+import org.springframework.util.StringUtils;
+import org.springframework.web.server.ServerWebExchange;
+
+/**
+ * @author Alberto C. Ríos
+ */
+public class GRPCResponseHeadersFilter implements HttpHeadersFilter, Ordered {
+
+ @Override
+ public HttpHeaders filter(HttpHeaders headers, ServerWebExchange exchange) {
+ ServerHttpResponse response = exchange.getResponse();
+ HttpHeaders responseHeaders = response.getHeaders();
+ if (isGRPC(exchange)) {
+ String trailerHeaderValue = "grpc-status";
+ String originalTrailerHeaderValue = responseHeaders.getFirst(HttpHeaders.TRAILER);
+ if (originalTrailerHeaderValue != null) {
+ trailerHeaderValue += "," + originalTrailerHeaderValue;
+ }
+ responseHeaders.set(HttpHeaders.TRAILER, trailerHeaderValue);
+ ((HttpServerResponse) ((AbstractServerHttpResponse) response).getNativeResponse())
+ .trailerHeaders(h -> h.set("grpc-status", "0"));
+ }
+ return headers;
+ }
+
+ private boolean isGRPC(ServerWebExchange exchange) {
+ String contentTypeValue = exchange.getRequest().getHeaders().getFirst(HttpHeaders.CONTENT_TYPE);
+ return StringUtils.startsWithIgnoreCase(contentTypeValue, "application/grpc");
+ }
+
+ @Override
+ public boolean supports(Type type) {
+ return Type.RESPONSE.equals(type);
+ }
+
+ @Override
+ public int getOrder() {
+ return 0;
+ }
+
+}
diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/config/GatewayAutoConfigurationTests.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/config/GatewayAutoConfigurationTests.java
index cbff76b2..cbaac27e 100644
--- a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/config/GatewayAutoConfigurationTests.java
+++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/config/GatewayAutoConfigurationTests.java
@@ -40,6 +40,8 @@ import org.springframework.boot.test.context.runner.ReactiveWebApplicationContex
import org.springframework.cloud.gateway.actuate.GatewayControllerEndpoint;
import org.springframework.cloud.gateway.actuate.GatewayLegacyControllerEndpoint;
import org.springframework.cloud.gateway.filter.factory.TokenRelayGatewayFilterFactory;
+import org.springframework.cloud.gateway.filter.headers.GRPCRequestHeadersFilter;
+import org.springframework.cloud.gateway.filter.headers.GRPCResponseHeadersFilter;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.GatewayFilterSpec;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
@@ -231,6 +233,30 @@ public class GatewayAutoConfigurationTests {
assertThat(spec2.protocols()).isNull();
}
+ @Test
+ public void gRPCFiltersConfiguredWhenHTTP2Enabled() {
+ new ReactiveWebApplicationContextRunner()
+ .withConfiguration(AutoConfigurations.of(WebFluxAutoConfiguration.class, MetricsAutoConfiguration.class,
+ SimpleMetricsExportAutoConfiguration.class, GatewayAutoConfiguration.class,
+ HttpClientCustomizedConfig.class, ServerPropertiesConfig.class))
+ .withPropertyValues("server.http2.enabled=true").run(context -> {
+ assertThat(context).hasSingleBean(GRPCRequestHeadersFilter.class);
+ assertThat(context).hasSingleBean(GRPCResponseHeadersFilter.class);
+ });
+ }
+
+ @Test
+ public void gRPCFiltersNotConfiguredWhenHTTP2Disabled() {
+ new ReactiveWebApplicationContextRunner()
+ .withConfiguration(AutoConfigurations.of(WebFluxAutoConfiguration.class, MetricsAutoConfiguration.class,
+ SimpleMetricsExportAutoConfiguration.class, GatewayAutoConfiguration.class,
+ HttpClientCustomizedConfig.class, ServerPropertiesConfig.class))
+ .withPropertyValues("server.http2.enabled=false").run(context -> {
+ assertThat(context).doesNotHaveBean(GRPCRequestHeadersFilter.class);
+ assertThat(context).doesNotHaveBean(GRPCResponseHeadersFilter.class);
+ });
+ }
+
@Configuration
@EnableConfigurationProperties(ServerProperties.class)
protected static class ServerPropertiesConfig {
diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/headers/GRPCRequestHeadersFilterTest.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/headers/GRPCRequestHeadersFilterTest.java
new file mode 100644
index 00000000..cf9121fb
--- /dev/null
+++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/headers/GRPCRequestHeadersFilterTest.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2013-2020 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.filter.headers;
+
+import org.junit.Test;
+
+import org.springframework.http.HttpHeaders;
+import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
+import org.springframework.mock.web.server.MockServerWebExchange;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Alberto C. Ríos
+ */
+public class GRPCRequestHeadersFilterTest {
+
+ @Test
+ public void shouldIncludeTrailersHeaderIfGRPC() {
+ MockServerHttpRequest request = MockServerHttpRequest.get("http://localhost:8080/get")
+ .header(HttpHeaders.CONTENT_TYPE, "application/grpc").build();
+
+ GRPCRequestHeadersFilter filter = new GRPCRequestHeadersFilter();
+
+ HttpHeaders headers = filter.filter(request.getHeaders(), MockServerWebExchange.from(request));
+
+ assertThat(headers).containsKeys("te");
+
+ assertThat(headers.getFirst("te")).isEqualTo("trailers");
+ }
+
+ @Test
+ public void shouldNotIncludeTrailersHeaderIfNotGRPC() {
+ MockServerHttpRequest request = MockServerHttpRequest.get("http://localhost:8080/get")
+ .header(HttpHeaders.CONTENT_TYPE, "application/json").build();
+
+ GRPCRequestHeadersFilter filter = new GRPCRequestHeadersFilter();
+
+ HttpHeaders headers = filter.filter(request.getHeaders(), MockServerWebExchange.from(request));
+
+ assertThat(headers).doesNotContainKeys("te");
+ }
+
+}