spring-cloud-gateway icon indicating copy to clipboard operation
spring-cloud-gateway copied to clipboard

spring-cloud-gateway grpc supports H2C

Open jianmo1997 opened this issue 3 years ago • 7 comments

Our business scenarios need to support non-SSL mode transmission,That means we need to support H2C

Could you give us some suggestions and tell us how to modify so that spring-cloud-gateway can support H2C?

Many thanks

jianmo1997 avatar Dec 27 '21 11:12 jianmo1997

I was able to configure gateway to do both HTTP1.1 & H2C:

@Configuration
public class H2CConfiguration {

    // https://projectreactor.io/docs/netty/release/reference/index.html#_http2
    @Bean
    public NettyWebServerFactoryCustomizer h2cServerCustomizer(Environment environment, ServerProperties serverProperties) {
        return new NettyWebServerFactoryCustomizer(environment, serverProperties) {
            @Override
            public void customize(NettyReactiveWebServerFactory factory) {
                factory.addServerCustomizers(httpServer -> httpServer.protocol(HttpProtocol.HTTP11, HttpProtocol.H2C));
                super.customize(factory);
            }
        };
    }
}
@Component
public class H2CAwareNettyRoutingFilter extends NettyRoutingFilter {

    public H2CAwareNettyRoutingFilter(HttpClient httpClient, ObjectProvider<List<HttpHeadersFilter>> headersFiltersProvider, HttpClientProperties properties) {
        super(httpClient, headersFiltersProvider, properties);
    }

    @Override
    protected HttpClient getHttpClient(Route route, ServerWebExchange exchange) {
        HttpClient httpClient = super.getHttpClient(route, exchange);

        boolean h2cRoute = ofNullable(route.getMetadata().get("h2c"))
                .map(o -> (Boolean)o)
                .orElse(false);
        if (h2cRoute) {
            // https://projectreactor.io/docs/netty/release/reference/index.html#_http2_2
            return httpClient.protocol(HttpProtocol.H2C);
        } else {
            return httpClient;
        }
    }

    @Override
    public int getOrder() {
        return super.getOrder() - 1;
    }
}
spring:
  application:
    name: gateway
  cloud:
    gateway:
      routes:
        - predicates:
            - Path=/h2c
          uri: http://127.0.0.1:8088
          metadata:
            h2c: true
        - predicates:
            - Path=/http
          uri: http://127.0.0.1:8089

See here for full project.

I also tried to set both HTTP1.1 & H2C in client

@Bean
    public HttpClientCustomizer h2ClientCustomizer() {
        return httpClient -> httpClient.protocol(HttpProtocol.HTTP11, HttpProtocol.H2C);
    }

but it isn't working, seems it requires to dig deeper in reactor-netty

nkonev avatar Dec 29 '21 04:12 nkonev

we also encountered the same problem (we both set h2c and h1 in spring cloud gateway , and use grpc server) and got answers from relevant communities: grpc-java reactor-netty

so we first make it clear that there are four protocl:

  1. h2 with tls/ssl;
  2. h2 without tls/ssl;
  3. Negotiable H2,no tls/ssl
  4. http1.1

for protocol No.3 it is a negotiation protocol based on http1.1 and just carries keywords such as upgrade in some positions such as the header.

And it's clear from https://projectreactor.io/docs/netty/release/reference/index.html#_protocol_selection_2, for reactor-netty

  • only set h2c protocol, it's will use h2 without tls/ssl (above 2);
  • both set h2c and http1, it's will use h2 upgrade form http1(above 3);

in some doc and our test case about protocol No3 :

  • if server support protocol No2 and protocol No3, client request with protocol No3 ,server side will reply status code 101, then client and server will use H2 without tls/ssl.
  • if server only support H1, server will ignore the keywords , then client and server will still use htttp1.1
  • if server support H2, not support H2C (like grpc), servr side will close the channel.

we reslove this problem like @nkonev's code, but we define two independent clients instead of modifying the protocol when using:one client set h1 protocol ,and other one set h2c protocol. decide which client to use according to the header (H2 has a unique header) or business-related routing, etc.

by the way, I suspectused like this, it may cause the code return httpClient; use of H2 protocol if httpclient base on pool if (h2cRoute) { // https://projectreactor.io/docs/netty/release/reference/index.html#_http2_2 return httpClient.protocol(HttpProtocol.H2C); } else { return httpClient; }

sodaRyCN avatar Dec 29 '21 10:12 sodaRyCN

@sodaRyCN I'm facing the same issue, can you provide some code sample for that ?

jasonhao518 avatar Nov 15 '22 23:11 jasonhao518

Analogous configurations for what is supported on the server side could be added for client site. Right now, only H2 is supported because ssl is assumed https://github.com/spring-cloud/spring-cloud-gateway/blob/be2abff70ea68b2bc7a1bd75bc6e0af74f45e9b2/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/HttpClientFactory.java#L86.

abelsromero avatar Nov 16 '22 13:11 abelsromero

@jasonhao518 u cloud refer to the following code, if you have a better implementation, please share. Also strongly recommend reading my previous review carefully.

@Configuration(proxyBeanMethods = false)
//Indicates that the scg server supports H2 protocol without tls
@ConditionalOnExpression("${server.http2.enabled:false} && !${server.ssl.enabled:true}")
@ConditionalOnClass({DispatcherHandler.class})
@AutoConfigureAfter({GatewayNoLoadBalancerClientAutoConfiguration.class)
public class GrpcAutoConfiguration {
        @Bean("grpcHttpClientCustomizer")
    public GrpcHttpClientCustomizer grpcHttpClientCustomizer(
            KeeperClientEventLoop clientEventLoopForGrpc) {
        return httpClient -> httpClient.protocol(HttpProtocol.H2C);
    }

    @Bean("grpcHttpClient")
    public GrpcHttpClient grpcHttpClient(HttpClientProperties properties, GrpcHttpClientCustomizer grpcHttpClientCustomizer) {
        return GrpcHttpClient.GrpcHttpClientBuilder.config(properties, grpcHttpClientCustomizer).create();
    }
        
        @Bean
    @ConditionalOnMissingClass
    public GrpcRoutingFilter grpcRoutingFilter(GrpcHttpClient grpcHttpClient,
                                               ObjectProvider<List<HttpHeadersFilter>> headersFilters,
                                               HttpClientProperties properties) {
        return new GrpcRoutingFilter(grpcHttpClient, headersFilters, properties);
    }

    @Bean("grpcRouteBuilder")
    public RouteLocatorBuilder.Builder grpcRouteBuilder(RouteLocatorBuilder builder, GrpcRoutingFilter grpcRoutingFilter) {
                return builder.routes()
              .route("grpc", r -> r.header("Content-Type", "^(application/grpc)( *;.*)*?$")
                    .filters(f -> f.filters(grpcRoutingFilter))
                    .uri("xxxx"));
    }

    @Bean("grpcRouteLocator")
    public RouteLocator routeLocator(RouteLocatorBuilder.Builder grpcRouteBuilder) {
        return grpcRouteBuilder.build();
    }
}

public class GrpcRoutingFilter implements GatewayFilter, Ordered {
        public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
                ....
        }
}


@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({DispatcherHandler.class})
@AutoConfigureAfter({GatewayNoLoadBalancerClientAutoConfiguration.class)
@EnableWebFluxSecurity
public class HttpAutoConfiguration {
        @Bean
    public HttpClientCustomizer httpClientCustomizer(KeeperClientEventLoop clientEventLoop) {
        return httpClient -> httpClient.protocol(HttpProtocol.HTTP11);
    }
        
        @Bean("baseRouteBuilder")
    public RouteLocatorBuilder.Builder baseRouteBuilder(RouteLocatorBuilder builder, NettyRoutingFilter nettyRoutingFilter) {
        return builder.routes()
              .route("http", r -> r.path("/xx/xx").filters(f -> f.filters(nettyRoutingFilter))
                    .uri("xxx"));

    }
        
        @Bean("baseRouteLocator")
    public RouteLocator routeLocator(RouteLocatorBuilder.Builder baseRouteBuilder) {
        return baseRouteBuilder.build();
    }
}

public class NettyRoutingFilter implements GatewayFilter, Ordered {
        public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
                ....
        }
}

sodaRyCN avatar Nov 22 '22 02:11 sodaRyCN

There is a simple solution, although it is not very reasonable.

  1. Copy the source code of org.springframework.cloud.gateway.config.HttpClientFactory to your project, making sure it is in the same package.
  2. Refer to the modifications below, which only support H2C.
  3. Since the target/classes of the application take precedence over the jar, modifying the version of HttpClientFactory is more prioritized.
	@Override
	protected HttpClient createInstance() {
		// configure pool resources
		ConnectionProvider connectionProvider = buildConnectionProvider(properties);

		HttpClient httpClient = HttpClient.create(connectionProvider)
				// TODO: move customizations to HttpClientCustomizers
				.httpResponseDecoder(this::httpResponseDecoder);

		if (serverProperties.getHttp2().isEnabled()) {
			httpClient = httpClient.protocol(HttpProtocol.HTTP11, HttpProtocol.H2);
		}

		if (properties.getConnectTimeout() != null) {
			httpClient = httpClient.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, properties.getConnectTimeout());
		}

		httpClient = configureProxy(httpClient);

		httpClient = configureSsl(httpClient);

change into:

	@Override
	protected HttpClient createInstance() {
		// configure pool resources
		ConnectionProvider connectionProvider = buildConnectionProvider(properties);

		HttpClient httpClient = HttpClient.create(connectionProvider)
				// TODO: move customizations to HttpClientCustomizers
				.httpResponseDecoder(this::httpResponseDecoder);

		if (serverProperties.getHttp2().isEnabled()) {
			httpClient = httpClient.protocol(HttpProtocol.H2C);
		}

		if (properties.getConnectTimeout() != null) {
			httpClient = httpClient.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, properties.getConnectTimeout());
		}

		httpClient = configureProxy(httpClient);

//		httpClient = configureSsl(httpClient);

hengyunabc avatar Sep 14 '23 04:09 hengyunabc

I was able to make it work as @nkonev suggested, is there still no OOTB solution for this?

liorderei avatar Jul 07 '24 16:07 liorderei

@jianmo1997 Hello, I have also encountered the same problem. Have you solved it so far?

huicunjun avatar Dec 04 '24 01:12 huicunjun