diff --git a/docs/src/main/asciidoc/_project-features-flows.adoc b/docs/src/main/asciidoc/_project-features-flows.adoc
index 390c71e04a..b803ae1948 100644
--- a/docs/src/main/asciidoc/_project-features-flows.adoc
+++ b/docs/src/main/asciidoc/_project-features-flows.adoc
@@ -127,6 +127,108 @@ include::{samples_url}/producer_webflux/src/test/java/com/example/BeerRestBase.j
----
====
+[[features-custom-mode]]
+=== Custom Mode
+
+IMPORTANT: This mode is experimental and can change in the future!
+
+The Spring Cloud Contract allows you to provide your own, custom implementation of the
+`org.springframework.cloud.contract.verifier.http.HttpVerifier`. That way you can use any client you want to send and receive a request. The default implementation in Spring Cloud Contract is `OkHttpHttpVerifier` and it leverages OkHttp3 http client.
+
+====
+[source,groovy,indent=0]
+----
+testMode = 'CUSTOM'
+----
+====
+
+The following example shows a generated test:
+
+====
+[source,java,indent=0]
+----
+package com.example.beer;
+
+import com.example.BeerRestBase;
+import javax.inject.Inject;
+import org.springframework.cloud.contract.verifier.http.HttpVerifier;
+import org.springframework.cloud.contract.verifier.http.Request;
+import org.springframework.cloud.contract.verifier.http.Response;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import static org.springframework.cloud.contract.verifier.assertion.SpringCloudContractAssertions.assertThat;
+import static org.springframework.cloud.contract.verifier.util.ContractVerifierUtil.*;
+import static org.springframework.cloud.contract.verifier.http.Request.given;
+
+@SuppressWarnings("rawtypes")
+public class RestTest extends BeerRestBase {
+ @Inject HttpVerifier httpVerifier;
+
+ @Test
+ public void validate_shouldGrantABeerIfOldEnough() throws Exception {
+ // given:
+ Request request = given()
+ .post("/beer.BeerService/check")
+ .scheme("HTTP")
+ .protocol("h2_prior_knowledge")
+ .header("Content-Type", "application/grpc")
+ .header("te", "trailers")
+ .body(fileToBytes(this, "shouldGrantABeerIfOldEnough_request_PersonToCheck_old_enough.bin"))
+ .build();
+
+
+ // when:
+ Response response = httpVerifier.exchange(request);
+
+
+ // then:
+ assertThat(response.statusCode()).isEqualTo(200);
+ assertThat(response.header("Content-Type")).matches("application/grpc.*");
+ assertThat(response.header("grpc-encoding")).isEqualTo("identity");
+ assertThat(response.header("grpc-accept-encoding")).isEqualTo("gzip");
+
+ // and:
+ assertThat(response.getBody().asByteArray()).isEqualTo(fileToBytes(this, "shouldGrantABeerIfOldEnough_response_Response_old_enough.bin"));
+ }
+
+}
+----
+====
+
+Corresponding base class could look like this:
+
+====
+[source,java,indent=0]
+----
+@SpringBootTest(classes = BeerRestBase.Config.class,
+ webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+public abstract class BeerRestBase {
+
+ @Configuration
+ @EnableAutoConfiguration
+ static class Config {
+
+ @Bean
+ ProducerController producerController(PersonCheckingService personCheckingService) {
+ return new ProducerController(personCheckingService);
+ }
+
+ @Bean
+ PersonCheckingService testPersonCheckingService() {
+ return argument -> argument.getAge() >= 20;
+ }
+
+ @Bean
+ HttpVerifier httpOkVerifier(@LocalServerPort int port) {
+ return new OkHttpHttpVerifier("localhost:" + port);
+ }
+
+ }
+}
+----
+====
+
[[features-context-paths]]
=== Working with Context Paths
@@ -564,7 +666,7 @@ contracts {
----
====
-The base class would set up the applicatoin running on a random port.
+The base class would set up the application running on a random port.
====
[source,java,indent=0,subs="verbatim,attributes"]
@@ -622,5 +724,283 @@ public class BeerControllerGraphQLTest {
}
}
+----
+====
+
+[[features-grpc]]
+=== GRPC
+
+https://grpc.io/[GRPC] is an RPC framework built on top of HTTP/2 for which Spring Cloud Contract has basic support.
+
+IMPORTANT: Spring Cloud Contract has an experimental support for basic use cases of GRPC. Unfortunately, due to GRPC's tweaking of the HTTP/2 Header frames, it's impossible to assert the `grpc-status` header.
+
+Let's look at the following contract.
+
+====
+[source,groovy,indent=0,subs="verbatim,attributes"]
+.Groovy contract
+----
+package contracts.beer.rest
+
+
+import org.springframework.cloud.contract.spec.Contract
+import org.springframework.cloud.contract.verifier.http.ContractVerifierHttpMetadata
+
+Contract.make {
+ description("""
+Represents a successful scenario of getting a beer
+
+```
+given:
+ client is old enough
+when:
+ he applies for a beer
+then:
+ we'll grant him the beer
+```
+
+""")
+ request {
+ method 'POST'
+ url '/beer.BeerService/check'
+ body(fileAsBytes("PersonToCheck_old_enough.bin"))
+ headers {
+ contentType("application/grpc")
+ header("te", "trailers")
+ }
+ }
+ response {
+ status 200
+ body(fileAsBytes("Response_old_enough.bin"))
+ headers {
+ contentType("application/grpc")
+ header("grpc-encoding", "identity")
+ header("grpc-accept-encoding", "gzip")
+ }
+ }
+ metadata([
+ "verifierHttp": [
+ "protocol": ContractVerifierHttpMetadata.Protocol.H2_PRIOR_KNOWLEDGE.toString()
+ ]
+ ])
+}
+----
+====
+
+[[features-grpc-producer]]
+==== Producer Side Setup
+
+In order to leverage the HTTP/2 support you must set the `CUSTOM` test mode as follow.
+
+====
+[source,xml,indent=0,subs="verbatim,attributes",role="primary"]
+.Maven
+----
+
+ org.springframework.cloud
+ spring-cloud-contract-maven-plugin
+ ${spring-cloud-contract.version}
+ true
+
+ CUSTOM
+ com.example
+
+
+----
+
+[source,groovy,indent=0,subs="verbatim,attributes",role="secondary"]
+.Gradle
+----
+contracts {
+ packageWithBaseClasses = 'com.example'
+ testMode = "CUSTOM"
+}
+----
+====
+
+The base class would set up the application running on a random port. It will also set the `HttpVerifier` implementation to one that can use the HTTP/2 protocol. Spring Cloud Contract comes with the `OkHttpHttpVerifier` implementation.
+
+====
+[source,java,indent=0,subs="verbatim,attributes"]
+.Base Class
+----
+@SpringBootTest(classes = BeerRestBase.Config.class,
+ webEnvironment = SpringBootTest.WebEnvironment.NONE,
+ properties = {
+ "grpc.server.port=0"
+ })
+public abstract class BeerRestBase {
+
+ @Autowired
+ GrpcServerProperties properties;
+
+ @Configuration
+ @EnableAutoConfiguration
+ static class Config {
+
+ @Bean
+ ProducerController producerController(PersonCheckingService personCheckingService) {
+ return new ProducerController(personCheckingService);
+ }
+
+ @Bean
+ PersonCheckingService testPersonCheckingService() {
+ return argument -> argument.getAge() >= 20;
+ }
+
+ @Bean
+ HttpVerifier httpOkVerifier(GrpcServerProperties properties) {
+ return new OkHttpHttpVerifier("localhost:" + properties.getPort());
+ }
+
+ }
+}
+----
+====
+
+[[features-grpc-consumer]]
+==== Consumer Side Setup
+
+Example of GRPC consumer side test. Due to the unusual behaviour of the GRPC server side, the stub is unable to return the `grpc-status` header in the proper moment. This is why we need to manually set the return status.
+
+====
+[source,java,indent=0,subs="verbatim,attributes"]
+.Consumer Side Test
+----
+@SpringBootTest(webEnvironment = WebEnvironment.NONE, classes = GrpcTests.TestConfiguration.class, properties = {
+ "grpc.client.beerService.address=static://localhost:5432", "grpc.client.beerService.negotiationType=TLS"
+})
+public class GrpcTests {
+
+ @GrpcClient(value = "beerService", interceptorNames = "fixedStatusSendingClientInterceptor")
+ BeerServiceGrpc.BeerServiceBlockingStub beerServiceBlockingStub;
+
+ int port;
+
+ @RegisterExtension
+ static StubRunnerExtension rule = new StubRunnerExtension()
+ .downloadStub("com.example", "beer-api-producer-grpc")
+ // With WireMock PlainText mode you can just set an HTTP port
+// .withPort(5432)
+ .stubsMode(StubRunnerProperties.StubsMode.LOCAL)
+ .withHttpServerStubConfigurer(MyWireMockConfigurer.class);
+
+ @BeforeEach
+ public void setupPort() {
+ this.port = rule.findStubUrl("beer-api-producer-grpc").getPort();
+ }
+
+ @Test
+ public void should_give_me_a_beer_when_im_old_enough() throws Exception {
+ Response response = beerServiceBlockingStub.check(PersonToCheck.newBuilder().setAge(23).build());
+
+ BDDAssertions.then(response.getStatus()).isEqualTo(Response.BeerCheckStatus.OK);
+ }
+
+ @Test
+ public void should_reject_a_beer_when_im_too_young() throws Exception {
+ Response response = beerServiceBlockingStub.check(PersonToCheck.newBuilder().setAge(17).build());
+ response = response == null ? Response.newBuilder().build() : response;
+
+ BDDAssertions.then(response.getStatus()).isEqualTo(Response.BeerCheckStatus.NOT_OK);
+ }
+
+ // Not necessary with WireMock PlainText mode
+ static class MyWireMockConfigurer extends WireMockHttpServerStubConfigurer {
+ @Override
+ public WireMockConfiguration configure(WireMockConfiguration httpStubConfiguration, HttpServerStubConfiguration httpServerStubConfiguration) {
+ return httpStubConfiguration
+ .httpsPort(5432);
+ }
+ }
+
+ @Configuration
+ @ImportAutoConfiguration(GrpcClientAutoConfiguration.class)
+ static class TestConfiguration {
+
+ // Not necessary with WireMock PlainText mode
+ @Bean
+ public GrpcChannelConfigurer keepAliveClientConfigurer() {
+ return (channelBuilder, name) -> {
+ if (channelBuilder instanceof NettyChannelBuilder) {
+ try {
+ ((NettyChannelBuilder) channelBuilder)
+ .sslContext(GrpcSslContexts.forClient()
+ .trustManager(InsecureTrustManagerFactory.INSTANCE)
+ .build());
+ }
+ catch (SSLException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+ };
+ }
+
+ /**
+ * GRPC client interceptor that sets the returned status always to OK.
+ * You might want to change the return status depending on the received stub payload.
+ *
+ * Hopefully in the future this will be unnecessary and will be removed.
+ */
+ @Bean
+ ClientInterceptor fixedStatusSendingClientInterceptor() {
+ return new ClientInterceptor() {
+ @Override
+ public ClientCall interceptCall(MethodDescriptor method, CallOptions callOptions, Channel next) {
+ ClientCall call = next.newCall(method, callOptions);
+ return new ClientCall() {
+ @Override
+ public void start(Listener responseListener, Metadata headers) {
+ Listener listener = new Listener() {
+ @Override
+ public void onHeaders(Metadata headers) {
+ responseListener.onHeaders(headers);
+ }
+
+ @Override
+ public void onMessage(RespT message) {
+ responseListener.onMessage(message);
+ }
+
+ @Override
+ public void onClose(Status status, Metadata trailers) {
+ // TODO: This must be fixed somehow either in Jetty (WireMock) or somewhere else
+ responseListener.onClose(Status.OK, trailers);
+ }
+
+ @Override
+ public void onReady() {
+ responseListener.onReady();
+ }
+ };
+ call.start(listener, headers);
+ }
+
+ @Override
+ public void request(int numMessages) {
+ call.request(numMessages);
+ }
+
+ @Override
+ public void cancel(@Nullable String message, @Nullable Throwable cause) {
+ call.cancel(message, cause);
+ }
+
+ @Override
+ public void halfClose() {
+ call.halfClose();
+ }
+
+ @Override
+ public void sendMessage(ReqT message) {
+ call.sendMessage(message);
+ }
+ };
+ }
+ };
+ }
+ }
+}
+
----
====
\ No newline at end of file