You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@servicecomb.apache.org by li...@apache.org on 2020/12/04 09:22:58 UTC

[servicecomb-java-chassis] branch master updated: [SCB-2142] add RestClientEncoder (#2101)

This is an automated email from the ASF dual-hosted git repository.

liubao pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/servicecomb-java-chassis.git


The following commit(s) were added to refs/heads/master by this push:
     new 71c0cba  [SCB-2142] add RestClientEncoder (#2101)
71c0cba is described below

commit 71c0cbab8f2c78cd8a5dcc7d9aa870a3cdced476
Author: wujimin <wu...@huawei.com>
AuthorDate: Fri Dec 4 17:22:48 2020 +0800

    [SCB-2142] add RestClientEncoder (#2101)
---
 .../transport/rest/client/RestClientEncoder.java   | 233 +++++++++++++++++++++
 .../rest/client/RestClientExceptionCodes.java      |   1 +
 ...atureController.java => FakeRestTransport.java} |  32 ++-
 .../rest/client/RestClientEncoderTest.java         | 185 ++++++++++++++++
 ...extFactoryTest.java => RestClientTestBase.java} | 110 ++--------
 .../RestClientTransportContextFactoryTest.java     | 135 +++---------
 .../rest/client/RestFeatureController.java         |  43 ++++
 7 files changed, 538 insertions(+), 201 deletions(-)

diff --git a/transports/transport-rest/transport-rest-client/src/main/java/org/apache/servicecomb/transport/rest/client/RestClientEncoder.java b/transports/transport-rest/transport-rest-client/src/main/java/org/apache/servicecomb/transport/rest/client/RestClientEncoder.java
new file mode 100644
index 0000000..fa36b4f
--- /dev/null
+++ b/transports/transport-rest/transport-rest-client/src/main/java/org/apache/servicecomb/transport/rest/client/RestClientEncoder.java
@@ -0,0 +1,233 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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
+ *
+ *     http://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.apache.servicecomb.transport.rest.client;
+
+import static javax.ws.rs.core.HttpHeaders.CONTENT_TYPE;
+import static javax.ws.rs.core.MediaType.MULTIPART_FORM_DATA;
+import static javax.ws.rs.core.Response.Status.BAD_REQUEST;
+import static org.apache.servicecomb.transport.rest.client.RestClientExceptionCodes.FAILED_TO_ENCODE_REST_CLIENT_REQUEST;
+
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.servlet.http.Part;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MediaType;
+
+import org.apache.servicecomb.common.rest.codec.RestCodec;
+import org.apache.servicecomb.common.rest.codec.RestObjectMapperFactory;
+import org.apache.servicecomb.common.rest.codec.query.QueryCodec;
+import org.apache.servicecomb.core.Const;
+import org.apache.servicecomb.core.Invocation;
+import org.apache.servicecomb.core.definition.OperationConfig;
+import org.apache.servicecomb.foundation.common.utils.StringBuilderUtils;
+import org.apache.servicecomb.swagger.invocation.exception.InvocationException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.util.CollectionUtils;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.vertx.core.buffer.Buffer;
+import io.vertx.core.http.HttpClientRequest;
+
+/**
+ * encode all send data except upload
+ */
+public class RestClientEncoder {
+  private static final Logger LOGGER = LoggerFactory.getLogger(RestClientEncoder.class);
+
+  public static final int FORM_BUFFER_SIZE = 1024;
+
+  protected final Invocation invocation;
+
+  protected final RestClientTransportContext transportContext;
+
+  protected final RestClientRequestParameters requestParameters;
+
+  protected final HttpClientRequest httpClientRequest;
+
+  public RestClientEncoder(Invocation invocation) {
+    this.invocation = invocation;
+    this.transportContext = invocation.getTransportContext();
+    this.requestParameters = transportContext.getRequestParameters();
+    this.httpClientRequest = transportContext.getHttpClientRequest();
+  }
+
+  public void encode() {
+    try {
+      doEncode();
+    } catch (Exception e) {
+      throw new InvocationException(BAD_REQUEST, FAILED_TO_ENCODE_REST_CLIENT_REQUEST, e.getMessage(), e);
+    }
+  }
+
+  protected void doEncode() throws Exception {
+    LOGGER.debug("rest client request, method={}, operation={}, endpoint={}, path={}.",
+        httpClientRequest.method(),
+        invocation.getMicroserviceQualifiedName(),
+        invocation.getEndpoint().getEndpoint(),
+        httpClientRequest.uri());
+
+    swaggerArgumentsToRequest();
+
+    writeCookies(requestParameters.getCookieMap());
+    writeScbHeaders();
+    writeForm(requestParameters.getFormMap());
+  }
+
+  protected void swaggerArgumentsToRequest() throws Exception {
+    RestCodec.argsToRest(invocation.getSwaggerArguments(), transportContext.getRestOperationMeta(), requestParameters);
+  }
+
+  protected void writeCookies(@Nullable Map<String, String> cookieMap) {
+    if (CollectionUtils.isEmpty(cookieMap)) {
+      return;
+    }
+
+    StringBuilder builder = new StringBuilder();
+    for (Entry<String, String> entry : cookieMap.entrySet()) {
+      builder.append(entry.getKey())
+          .append('=')
+          .append(entry.getValue())
+          .append("; ");
+    }
+    StringBuilderUtils.deleteLast(builder, 2);
+    httpClientRequest.putHeader(HttpHeaders.COOKIE, builder.toString());
+  }
+
+  protected void writeScbHeaders() throws JsonProcessingException {
+    OperationConfig operationConfig = invocation.getOperationMeta().getConfig();
+    if (invocation.isThirdPartyInvocation() && operationConfig.isClientRequestHeaderFilterEnabled()) {
+      return;
+    }
+
+    httpClientRequest.putHeader(Const.TARGET_MICROSERVICE, invocation.getMicroserviceName());
+    httpClientRequest.putHeader(Const.CSE_CONTEXT,
+        RestObjectMapperFactory.getRestObjectMapper().writeValueAsString(invocation.getContext()));
+  }
+
+  protected void writeForm(@Nullable Map<String, Object> formMap) throws Exception {
+    if (requestParameters.getUploads() == null) {
+      writeUrlEncodedForm(formMap);
+      return;
+    }
+
+    writeChunkedForm(formMap);
+  }
+
+  protected void writeUrlEncodedForm(@Nullable Map<String, Object> formMap) throws Exception {
+    if (formMap == null) {
+      return;
+    }
+
+    httpClientRequest.putHeader(CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED);
+
+    Buffer bodyBuffer = genUrlEncodedFormBuffer(formMap);
+    requestParameters.setBodyBuffer(bodyBuffer);
+  }
+
+  protected Buffer genUrlEncodedFormBuffer(@Nonnull Map<String, Object> formMap) throws Exception {
+    // 2x faster than UriComponentsBuilder
+    ByteBuf byteBuf = Unpooled.buffer(FORM_BUFFER_SIZE);
+    for (Entry<String, Object> entry : formMap.entrySet()) {
+      writeCharSequence(byteBuf, entry.getKey());
+      byteBuf.writeByte('=');
+
+      String value = QueryCodec.convertToString(entry.getValue());
+      String encodedValue = URLEncoder.encode(value, StandardCharsets.UTF_8.name());
+      writeCharSequence(byteBuf, encodedValue);
+
+      byteBuf.markWriterIndex();
+      byteBuf.writeByte('&');
+    }
+
+    byteBuf.resetWriterIndex();
+    return Buffer.buffer(byteBuf);
+  }
+
+  protected void writeChunkedForm(@Nullable Map<String, Object> formMap) throws Exception {
+    String boundary = transportContext.getOrCreateBoundary();
+
+    httpClientRequest.setChunked(true);
+    httpClientRequest.putHeader(CONTENT_TYPE, MULTIPART_FORM_DATA + "; charset=UTF-8; boundary=" + boundary);
+
+    if (formMap == null) {
+      return;
+    }
+
+    Buffer bodyBuffer = genChunkedFormBuffer(formMap, boundary);
+    requestParameters.setBodyBuffer(bodyBuffer);
+  }
+
+  protected Buffer genChunkedFormBuffer(@Nonnull Map<String, Object> formMap, String boundary) throws Exception {
+    ByteBuf byteBuf = Unpooled.buffer(FORM_BUFFER_SIZE);
+    for (Entry<String, Object> entry : formMap.entrySet()) {
+      writeCharSequence(byteBuf, "\r\n--");
+      writeCharSequence(byteBuf, boundary);
+      writeCharSequence(byteBuf, "\r\nContent-Disposition: form-data; name=\"");
+      writeCharSequence(byteBuf, entry.getKey());
+      writeCharSequence(byteBuf, "\"\r\n\r\n");
+
+      String value = QueryCodec.convertToString(entry.getValue());
+      writeCharSequence(byteBuf, value);
+    }
+    return Buffer.buffer(byteBuf);
+  }
+
+  protected static void writeCharSequence(ByteBuf byteBuf, String value) {
+    byteBuf.writeCharSequence(value, StandardCharsets.UTF_8);
+  }
+
+  public static Buffer genFileBoundaryBuffer(Part part, String name, String boundary) {
+    ByteBuf byteBuf = Unpooled.buffer();
+
+    writeCharSequence(byteBuf, "\r\n--");
+    writeCharSequence(byteBuf, boundary);
+    writeCharSequence(byteBuf, "\r\nContent-Disposition: form-data; name=\"");
+    writeCharSequence(byteBuf, name);
+    writeCharSequence(byteBuf, "\" filename=\"");
+    writeCharSequence(byteBuf, String.valueOf(part.getSubmittedFileName()));
+    writeCharSequence(byteBuf, "\"\r\n");
+
+    writeCharSequence(byteBuf, "Content-Type: ");
+    writeCharSequence(byteBuf, part.getContentType());
+    writeCharSequence(byteBuf, "\r\n");
+
+    writeCharSequence(byteBuf, "Content-Transfer-Encoding: binary\r\n");
+
+    writeCharSequence(byteBuf, "\r\n");
+
+    return Buffer.buffer(byteBuf);
+  }
+
+  public static Buffer genBoundaryEndBuffer(String boundary) {
+    ByteBuf byteBuf = Unpooled.buffer();
+
+    writeCharSequence(byteBuf, "\r\n--");
+    writeCharSequence(byteBuf, boundary);
+    writeCharSequence(byteBuf, "--\r\n");
+
+    return Buffer.buffer(byteBuf);
+  }
+}
diff --git a/transports/transport-rest/transport-rest-client/src/main/java/org/apache/servicecomb/transport/rest/client/RestClientExceptionCodes.java b/transports/transport-rest/transport-rest-client/src/main/java/org/apache/servicecomb/transport/rest/client/RestClientExceptionCodes.java
index c30010f..5dcef13 100644
--- a/transports/transport-rest/transport-rest-client/src/main/java/org/apache/servicecomb/transport/rest/client/RestClientExceptionCodes.java
+++ b/transports/transport-rest/transport-rest-client/src/main/java/org/apache/servicecomb/transport/rest/client/RestClientExceptionCodes.java
@@ -18,4 +18,5 @@ package org.apache.servicecomb.transport.rest.client;
 
 public interface RestClientExceptionCodes {
   String FAILED_TO_CREATE_REST_CLIENT_TRANSPORT_CONTEXT = "scb_rest_client.40000000";
+  String FAILED_TO_ENCODE_REST_CLIENT_REQUEST = "scb_rest_client.40000001";
 }
diff --git a/transports/transport-rest/transport-rest-client/src/test/java/org/apache/servicecomb/transport/rest/client/RestFeatureController.java b/transports/transport-rest/transport-rest-client/src/test/java/org/apache/servicecomb/transport/rest/client/FakeRestTransport.java
similarity index 58%
copy from transports/transport-rest/transport-rest-client/src/test/java/org/apache/servicecomb/transport/rest/client/RestFeatureController.java
copy to transports/transport-rest/transport-rest-client/src/test/java/org/apache/servicecomb/transport/rest/client/FakeRestTransport.java
index a11f10f..17e1d86 100644
--- a/transports/transport-rest/transport-rest-client/src/test/java/org/apache/servicecomb/transport/rest/client/RestFeatureController.java
+++ b/transports/transport-rest/transport-rest-client/src/test/java/org/apache/servicecomb/transport/rest/client/FakeRestTransport.java
@@ -16,17 +16,29 @@
  */
 package org.apache.servicecomb.transport.rest.client;
 
-import javax.ws.rs.GET;
-import javax.ws.rs.Path;
-import javax.ws.rs.QueryParam;
+import org.apache.servicecomb.core.Invocation;
+import org.apache.servicecomb.core.transport.AbstractTransport;
+import org.apache.servicecomb.foundation.common.net.URIEndpointObject;
+import org.apache.servicecomb.swagger.invocation.AsyncResponse;
 
-@Path("/")
-public class RestFeatureController {
-  public static final String SCHEMA_ID = "rest-feature";
+class FakeRestTransport extends AbstractTransport {
+  @Override
+  public String getName() {
+    return null;
+  }
+
+  @Override
+  public boolean init() {
+    return false;
+  }
+
+  @Override
+  public void send(Invocation invocation, AsyncResponse asyncResp) {
+
+  }
 
-  @GET
-  @Path("/query")
-  public String query(@QueryParam("query") String query) {
-    return query;
+  @Override
+  public Object parseAddress(String address) {
+    return new URIEndpointObject(address);
   }
 }
diff --git a/transports/transport-rest/transport-rest-client/src/test/java/org/apache/servicecomb/transport/rest/client/RestClientEncoderTest.java b/transports/transport-rest/transport-rest-client/src/test/java/org/apache/servicecomb/transport/rest/client/RestClientEncoderTest.java
new file mode 100644
index 0000000..753220f
--- /dev/null
+++ b/transports/transport-rest/transport-rest-client/src/test/java/org/apache/servicecomb/transport/rest/client/RestClientEncoderTest.java
@@ -0,0 +1,185 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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
+ *
+ *     http://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.apache.servicecomb.transport.rest.client;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.io.File;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.servlet.http.Part;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MediaType;
+
+import org.apache.servicecomb.foundation.common.part.FilePart;
+import org.junit.jupiter.api.Test;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+
+class RestClientEncoderTest extends RestClientTestBase {
+  RestClientEncoder encoder;
+
+  void init(String operationId, Map<String, Object> swaggerArgs) {
+    init(operationId, swaggerArgs, false);
+    encoder = new RestClientEncoder(invocation);
+  }
+
+  @Test
+  void should_encode_header_parameter() {
+    init("header", ImmutableMap.of("header", "value"));
+
+    encoder.encode();
+
+    assertThat(httpClientRequest.headers().get("header"))
+        .isEqualTo("value");
+  }
+
+  @Test
+  void should_encode_servicecomb_headers() {
+    init("header", ImmutableMap.of("header", "value"));
+
+    encoder.encode();
+
+    assertThat(httpClientRequest.headers().toString())
+        .isEqualTo("header: value\n"
+            + "x-cse-target-microservice: defaultMicroservice\n"
+            + "x-cse-context: {\"x-cse-src-microservice\":\"defaultMicroservice\"}\n");
+  }
+
+  @Test
+  void should_not_encode_servicecomb_headers_when_invoke_3rd_service_and_filter_servicecomb_headers() {
+    init("header", ImmutableMap.of("header", "value"));
+    referenceConfig.setThirdPartyService(true);
+    operationMeta.getConfig().setClientRequestHeaderFilterEnabled(true);
+
+    encoder.encode();
+
+    assertThat(httpClientRequest.headers().toString())
+        .isEqualTo("header: value\n");
+  }
+
+  @Test
+  void should_encode_servicecomb_headers_when_invoke_3rd_service_and_not_filter_servicecomb_headers() {
+    init("header", ImmutableMap.of("header", "value"));
+    referenceConfig.setThirdPartyService(true);
+    operationMeta.getConfig().setClientRequestHeaderFilterEnabled(false);
+
+    encoder.encode();
+
+    assertThat(httpClientRequest.headers().toString())
+        .isEqualTo("header: value\n"
+            + "x-cse-target-microservice: defaultMicroservice\n"
+            + "x-cse-context: {\"x-cse-src-microservice\":\"defaultMicroservice\"}\n");
+  }
+
+  @Test
+  void should_encode_cookie_parameter() {
+    init("cookie", ImmutableMap.of("cookie1", "v1", "cookie2", "v2"));
+
+    encoder.encode();
+
+    assertThat(httpClientRequest.headers().get(HttpHeaders.COOKIE))
+        .isEqualTo("cookie1=v1; cookie2=v2");
+  }
+
+  @Test
+  void should_encode_body_parameter() {
+    init("body", ImmutableMap.of("body", "value"));
+
+    encoder.encode();
+
+    assertThat(transportContext.getRequestParameters().getBodyBuffer().toString())
+        .isEqualTo("\"value\"");
+  }
+
+  @Test
+  void should_encode_form_attribute_parameter() {
+    init("form", ImmutableMap.of("form1", "v1", "form2", "v2"));
+
+    encoder.encode();
+
+    assertThat(httpClientRequest.headers().get(HttpHeaders.CONTENT_TYPE))
+        .isEqualTo(MediaType.APPLICATION_FORM_URLENCODED);
+    assertThat(transportContext.getRequestParameters().getBodyBuffer().toString())
+        .isEqualTo("form1=v1&form2=v2");
+  }
+
+  @Test
+  void should_not_encode_null_form_attribute() {
+    Map<String, Object> swaggerArgs = new HashMap<>();
+    swaggerArgs.put("form1", "v1");
+    swaggerArgs.put("form2", null);
+    init("form", swaggerArgs);
+
+    encoder.encode();
+
+    assertThat(httpClientRequest.headers().get(HttpHeaders.CONTENT_TYPE))
+        .isEqualTo(MediaType.APPLICATION_FORM_URLENCODED);
+    assertThat(transportContext.getRequestParameters().getBodyBuffer().toString())
+        .isEqualTo("form1=v1");
+  }
+
+  @Test
+  void should_encode_form_with_upload_parameter() {
+    init("formWithUpload", ImmutableMap.of("form1", "v1", "form2", new File("form2")));
+
+    encoder.encode();
+
+    RestClientRequestParameters requestParameters = transportContext.getRequestParameters();
+    assertThat(requestParameters.getBodyBuffer().toString())
+        .isEqualTo("\r\n"
+            + "--my-boundary\r\n"
+            + "Content-Disposition: form-data; name=\"form1\"\r\n"
+            + "\r\n"
+            + "v1");
+    List<Part> parts = Lists.newArrayList(requestParameters.getUploads().get("form2"));
+    assertThat(parts).hasSize(1);
+    assertThat(((FilePart) parts.get(0))).isInstanceOf(FilePart.class);
+  }
+
+  @Test
+  void should_encode_form_with_upload_list() {
+    init("formWithUploadList", ImmutableMap.of("files", Arrays.asList(new File("f1"), new File("f2"))));
+
+    encoder.encode();
+
+    checkUploadList();
+  }
+
+  @Test
+  void should_encode_form_with_upload_array() {
+    init("formWithUploadList", ImmutableMap.of("files", new File[] {new File("f1"), new File("f2")}));
+
+    encoder.encode();
+
+    checkUploadList();
+  }
+
+  private void checkUploadList() {
+    RestClientRequestParameters requestParameters = transportContext.getRequestParameters();
+    assertThat(requestParameters.getBodyBuffer()).isNull();
+    List<Part> parts = Lists.newArrayList(requestParameters.getUploads().get("files"));
+    assertThat(parts).hasSize(2);
+    assertThat(((FilePart) parts.get(0)).getAbsolutePath()).endsWith("f1");
+    assertThat(((FilePart) parts.get(1)).getAbsolutePath()).endsWith("f2");
+  }
+}
\ No newline at end of file
diff --git a/transports/transport-rest/transport-rest-client/src/test/java/org/apache/servicecomb/transport/rest/client/RestClientTransportContextFactoryTest.java b/transports/transport-rest/transport-rest-client/src/test/java/org/apache/servicecomb/transport/rest/client/RestClientTestBase.java
similarity index 53%
copy from transports/transport-rest/transport-rest-client/src/test/java/org/apache/servicecomb/transport/rest/client/RestClientTransportContextFactoryTest.java
copy to transports/transport-rest/transport-rest-client/src/test/java/org/apache/servicecomb/transport/rest/client/RestClientTestBase.java
index 563bf3b..7aa9535 100644
--- a/transports/transport-rest/transport-rest-client/src/test/java/org/apache/servicecomb/transport/rest/client/RestClientTransportContextFactoryTest.java
+++ b/transports/transport-rest/transport-rest-client/src/test/java/org/apache/servicecomb/transport/rest/client/RestClientTestBase.java
@@ -14,13 +14,10 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
 package org.apache.servicecomb.transport.rest.client;
 
 import static org.apache.servicecomb.transport.rest.client.RestFeatureController.SCHEMA_ID;
-import static org.assertj.core.api.Assertions.assertThat;
 
-import java.util.Arrays;
 import java.util.Map;
 
 import org.apache.servicecomb.common.rest.definition.RestMetaUtils;
@@ -36,50 +33,32 @@ import org.apache.servicecomb.core.definition.InvocationRuntimeType;
 import org.apache.servicecomb.core.definition.OperationMeta;
 import org.apache.servicecomb.core.invocation.InvocationFactory;
 import org.apache.servicecomb.core.provider.consumer.ReferenceConfig;
-import org.apache.servicecomb.core.transport.AbstractTransport;
-import org.apache.servicecomb.foundation.common.net.URIEndpointObject;
 import org.apache.servicecomb.foundation.test.scaffolding.config.ArchaiusUtils;
 import org.apache.servicecomb.foundation.vertx.client.http.HttpClients;
-import org.apache.servicecomb.swagger.invocation.AsyncResponse;
 import org.junit.jupiter.api.AfterAll;
 import org.junit.jupiter.api.BeforeAll;
-import org.junit.jupiter.api.Test;
 
-import com.google.common.collect.ImmutableMap;
+import io.vertx.core.http.HttpClientRequest;
 
-class RestClientTransportContextFactoryTest {
+public class RestClientTestBase {
   static SCBEngine scbEngine;
 
-  static Transport restTransport = new AbstractTransport() {
-    @Override
-    public String getName() {
-      return null;
-    }
+  static Transport restTransport = new FakeRestTransport();
 
-    @Override
-    public boolean init() {
-      return false;
-    }
+  static RestClientTransportContextFactory factory = new RestClientTransportContextFactory()
+      .setBoundaryFactory(() -> "my-boundary");
 
-    @Override
-    public void send(Invocation invocation, AsyncResponse asyncResp) {
+  ReferenceConfig referenceConfig = new ReferenceConfig(Const.RESTFUL, Const.DEFAULT_VERSION_RULE);
 
-    }
+  OperationMeta operationMeta;
 
-    @Override
-    public Object parseAddress(String address) {
-      return new URIEndpointObject(address);
-    }
-  };
+  RestOperationMeta restOperationMeta;
 
-  static RestClientTransportContextFactory factory = new RestClientTransportContextFactory()
-      .setBoundaryFactory(BoundaryFactory.DEFAULT);
-
-  static OperationMeta operationMeta;
+  Invocation invocation;
 
-  static RestOperationMeta restOperationMeta;
+  RestClientTransportContext transportContext;
 
-  static ReferenceConfig referenceConfig = new ReferenceConfig(Const.RESTFUL, Const.DEFAULT_VERSION_RULE);
+  HttpClientRequest httpClientRequest;
 
   @BeforeAll
   static void beforeAll() {
@@ -87,10 +66,6 @@ class RestClientTransportContextFactoryTest {
     scbEngine = SCBBootstrap.createSCBEngineForTest()
         .addProducerMeta(SCHEMA_ID, new RestFeatureController())
         .run();
-    operationMeta = scbEngine.getProducerMicroserviceMeta()
-        .ensureFindSchemaMeta(SCHEMA_ID)
-        .ensureFindOperation("query");
-    restOperationMeta = RestMetaUtils.getRestOperationMeta(operationMeta);
     HttpClients.load();
   }
 
@@ -102,67 +77,24 @@ class RestClientTransportContextFactoryTest {
     ArchaiusUtils.resetConfig();
   }
 
-  Invocation invocation;
-
-  RestClientTransportContext transportContext;
+  void init(String operationId, Map<String, Object> swaggerArgs, boolean ssl) {
+    operationMeta = scbEngine.getProducerMicroserviceMeta()
+        .ensureFindSchemaMeta(SCHEMA_ID)
+        .ensureFindOperation(operationId);
+    restOperationMeta = RestMetaUtils.getRestOperationMeta(operationMeta);
 
-  void initInvocation(Map<String, Object> swaggerArgs, boolean ssl) {
     invocation = InvocationFactory.forConsumer(
         referenceConfig, operationMeta, new InvocationRuntimeType(null), swaggerArgs);
 
     String url = "rest://localhost:1234?sslEnabled=" + ssl;
     invocation.setEndpoint(new Endpoint(restTransport, url));
-  }
-
-  String absoluteURI() {
-    return transportContext.getHttpClientRequest().absoluteURI();
-  }
-
-  @Test
-  void should_create_without_ssl() {
-    initInvocation(null, false);
 
     transportContext = factory.create(invocation);
-    assertThat(absoluteURI()).isEqualTo("http://localhost:1234/query");
+    invocation.setTransportContext(transportContext);
+    httpClientRequest = transportContext.getHttpClientRequest();
   }
 
-  @Test
-  void should_create_with_ssl() {
-    initInvocation(null, true);
-
-    transportContext = factory.create(invocation);
-    assertThat(absoluteURI()).isEqualTo("https://localhost:1234/query");
-  }
-
-  @Test
-  void should_create_with_query() {
-    initInvocation(ImmutableMap.of("query", "value"), true);
-
-    transportContext = factory.create(invocation);
-    assertThat(absoluteURI()).isEqualTo("https://localhost:1234/query?query=value");
-  }
-
-  @Test
-  void should_create_with_query_list() {
-    initInvocation(ImmutableMap.of("query", Arrays.asList("v1", "v2")), true);
-
-    transportContext = factory.create(invocation);
-    assertThat(absoluteURI()).isEqualTo("https://localhost:1234/query?query=v1&query=v2");
-  }
-
-  @Test
-  void should_create_with_query_array() {
-    initInvocation(ImmutableMap.of("query", new String[] {"v1", "v2"}), true);
-
-    transportContext = factory.create(invocation);
-    assertThat(absoluteURI()).isEqualTo("https://localhost:1234/query?query=v1&query=v2");
-  }
-
-  @Test
-  void should_get_local_address_as_not_connected_before_connect() {
-    initInvocation(null, true);
-
-    transportContext = factory.create(invocation);
-    assertThat(transportContext.getLocalAddress()).isEqualTo("not connected");
+  String absoluteURI() {
+    return httpClientRequest.absoluteURI();
   }
-}
\ No newline at end of file
+}
diff --git a/transports/transport-rest/transport-rest-client/src/test/java/org/apache/servicecomb/transport/rest/client/RestClientTransportContextFactoryTest.java b/transports/transport-rest/transport-rest-client/src/test/java/org/apache/servicecomb/transport/rest/client/RestClientTransportContextFactoryTest.java
index 563bf3b..e2c3c64 100644
--- a/transports/transport-rest/transport-rest-client/src/test/java/org/apache/servicecomb/transport/rest/client/RestClientTransportContextFactoryTest.java
+++ b/transports/transport-rest/transport-rest-client/src/test/java/org/apache/servicecomb/transport/rest/client/RestClientTransportContextFactoryTest.java
@@ -17,152 +17,83 @@
 
 package org.apache.servicecomb.transport.rest.client;
 
-import static org.apache.servicecomb.transport.rest.client.RestFeatureController.SCHEMA_ID;
 import static org.assertj.core.api.Assertions.assertThat;
 
 import java.util.Arrays;
+import java.util.HashMap;
 import java.util.Map;
 
-import org.apache.servicecomb.common.rest.definition.RestMetaUtils;
-import org.apache.servicecomb.common.rest.definition.RestOperationMeta;
-import org.apache.servicecomb.config.ConfigUtil;
-import org.apache.servicecomb.core.Const;
-import org.apache.servicecomb.core.Endpoint;
-import org.apache.servicecomb.core.Invocation;
-import org.apache.servicecomb.core.SCBEngine;
-import org.apache.servicecomb.core.Transport;
-import org.apache.servicecomb.core.bootstrap.SCBBootstrap;
-import org.apache.servicecomb.core.definition.InvocationRuntimeType;
-import org.apache.servicecomb.core.definition.OperationMeta;
-import org.apache.servicecomb.core.invocation.InvocationFactory;
-import org.apache.servicecomb.core.provider.consumer.ReferenceConfig;
-import org.apache.servicecomb.core.transport.AbstractTransport;
-import org.apache.servicecomb.foundation.common.net.URIEndpointObject;
-import org.apache.servicecomb.foundation.test.scaffolding.config.ArchaiusUtils;
-import org.apache.servicecomb.foundation.vertx.client.http.HttpClients;
-import org.apache.servicecomb.swagger.invocation.AsyncResponse;
-import org.junit.jupiter.api.AfterAll;
-import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Test;
 
 import com.google.common.collect.ImmutableMap;
 
-class RestClientTransportContextFactoryTest {
-  static SCBEngine scbEngine;
-
-  static Transport restTransport = new AbstractTransport() {
-    @Override
-    public String getName() {
-      return null;
-    }
-
-    @Override
-    public boolean init() {
-      return false;
-    }
-
-    @Override
-    public void send(Invocation invocation, AsyncResponse asyncResp) {
-
-    }
-
-    @Override
-    public Object parseAddress(String address) {
-      return new URIEndpointObject(address);
-    }
-  };
-
-  static RestClientTransportContextFactory factory = new RestClientTransportContextFactory()
-      .setBoundaryFactory(BoundaryFactory.DEFAULT);
-
-  static OperationMeta operationMeta;
-
-  static RestOperationMeta restOperationMeta;
-
-  static ReferenceConfig referenceConfig = new ReferenceConfig(Const.RESTFUL, Const.DEFAULT_VERSION_RULE);
-
-  @BeforeAll
-  static void beforeAll() {
-    ConfigUtil.installDynamicConfig();
-    scbEngine = SCBBootstrap.createSCBEngineForTest()
-        .addProducerMeta(SCHEMA_ID, new RestFeatureController())
-        .run();
-    operationMeta = scbEngine.getProducerMicroserviceMeta()
-        .ensureFindSchemaMeta(SCHEMA_ID)
-        .ensureFindOperation("query");
-    restOperationMeta = RestMetaUtils.getRestOperationMeta(operationMeta);
-    HttpClients.load();
-  }
-
-  @AfterAll
-  static void afterAll() {
-    scbEngine.destroy();
-    HttpClients.destroy();
-
-    ArchaiusUtils.resetConfig();
-  }
-
-  Invocation invocation;
-
-  RestClientTransportContext transportContext;
-
-  void initInvocation(Map<String, Object> swaggerArgs, boolean ssl) {
-    invocation = InvocationFactory.forConsumer(
-        referenceConfig, operationMeta, new InvocationRuntimeType(null), swaggerArgs);
-
-    String url = "rest://localhost:1234?sslEnabled=" + ssl;
-    invocation.setEndpoint(new Endpoint(restTransport, url));
-  }
-
-  String absoluteURI() {
-    return transportContext.getHttpClientRequest().absoluteURI();
+class RestClientTransportContextFactoryTest extends RestClientTestBase {
+  void init(Map<String, Object> swaggerArgs, boolean ssl) {
+    init("query", swaggerArgs, ssl);
   }
 
   @Test
   void should_create_without_ssl() {
-    initInvocation(null, false);
+    init(null, false);
 
-    transportContext = factory.create(invocation);
     assertThat(absoluteURI()).isEqualTo("http://localhost:1234/query");
   }
 
   @Test
   void should_create_with_ssl() {
-    initInvocation(null, true);
+    init(null, true);
 
-    transportContext = factory.create(invocation);
     assertThat(absoluteURI()).isEqualTo("https://localhost:1234/query");
   }
 
   @Test
   void should_create_with_query() {
-    initInvocation(ImmutableMap.of("query", "value"), true);
+    init(ImmutableMap.of("query", "value"), true);
 
-    transportContext = factory.create(invocation);
     assertThat(absoluteURI()).isEqualTo("https://localhost:1234/query?query=value");
   }
 
   @Test
+  void should_ignore_null_query_value() {
+    Map<String, Object> swaggerArgs = new HashMap<>();
+    swaggerArgs.put("query", null);
+    init(swaggerArgs, true);
+
+    assertThat(absoluteURI()).isEqualTo("https://localhost:1234/query");
+  }
+
+  @Test
   void should_create_with_query_list() {
-    initInvocation(ImmutableMap.of("query", Arrays.asList("v1", "v2")), true);
+    init(ImmutableMap.of("query", Arrays.asList("v1", "v2")), true);
 
-    transportContext = factory.create(invocation);
     assertThat(absoluteURI()).isEqualTo("https://localhost:1234/query?query=v1&query=v2");
   }
 
   @Test
+  void should_ignore_null_in_query_list() {
+    init(ImmutableMap.of("query", Arrays.asList("v1", null)), true);
+
+    assertThat(absoluteURI()).isEqualTo("https://localhost:1234/query?query=v1");
+  }
+
+  @Test
   void should_create_with_query_array() {
-    initInvocation(ImmutableMap.of("query", new String[] {"v1", "v2"}), true);
+    init(ImmutableMap.of("query", new String[] {"v1", "v2"}), true);
 
-    transportContext = factory.create(invocation);
     assertThat(absoluteURI()).isEqualTo("https://localhost:1234/query?query=v1&query=v2");
   }
 
   @Test
+  void should_ignore_null_in_query_array() {
+    init(ImmutableMap.of("query", new String[] {"v1", null}), true);
+
+    assertThat(absoluteURI()).isEqualTo("https://localhost:1234/query?query=v1");
+  }
+
+  @Test
   void should_get_local_address_as_not_connected_before_connect() {
-    initInvocation(null, true);
+    init(null, true);
 
-    transportContext = factory.create(invocation);
     assertThat(transportContext.getLocalAddress()).isEqualTo("not connected");
   }
 }
\ No newline at end of file
diff --git a/transports/transport-rest/transport-rest-client/src/test/java/org/apache/servicecomb/transport/rest/client/RestFeatureController.java b/transports/transport-rest/transport-rest-client/src/test/java/org/apache/servicecomb/transport/rest/client/RestFeatureController.java
index a11f10f..1b567d7 100644
--- a/transports/transport-rest/transport-rest-client/src/test/java/org/apache/servicecomb/transport/rest/client/RestFeatureController.java
+++ b/transports/transport-rest/transport-rest-client/src/test/java/org/apache/servicecomb/transport/rest/client/RestFeatureController.java
@@ -16,7 +16,14 @@
  */
 package org.apache.servicecomb.transport.rest.client;
 
+import java.io.File;
+import java.util.List;
+
+import javax.ws.rs.CookieParam;
+import javax.ws.rs.FormParam;
 import javax.ws.rs.GET;
+import javax.ws.rs.HeaderParam;
+import javax.ws.rs.POST;
 import javax.ws.rs.Path;
 import javax.ws.rs.QueryParam;
 
@@ -29,4 +36,40 @@ public class RestFeatureController {
   public String query(@QueryParam("query") String query) {
     return query;
   }
+
+  @GET
+  @Path("/header")
+  public String header(@HeaderParam("header") String header) {
+    return header;
+  }
+
+  @GET
+  @Path("/cookie")
+  public String cookie(@CookieParam("cookie1") String cookie1, @CookieParam("cookie2") String cookie2) {
+    return cookie1 + ":" + cookie2;
+  }
+
+  @POST
+  @Path("/form")
+  public String form(@FormParam("form1") String form1, @FormParam("form2") String form2) {
+    return form1 + ":" + form2;
+  }
+
+  @POST
+  @Path("/formWithUpload")
+  public String formWithUpload(@FormParam("form1") String form1, @FormParam("form2") File form2) {
+    return form1 + ":" + form2.getName();
+  }
+
+  @POST
+  @Path("/formWithUploadList")
+  public String formWithUploadList(@FormParam("files") List<File> files) {
+    return files.toString();
+  }
+
+  @POST
+  @Path("/body")
+  public String body(String body) {
+    return body;
+  }
 }