Fixup: Request builder review fixes
diff --git a/xds/src/main/java/io/grpc/xds/internal/extauthz/CertificateUtils.java b/xds/src/main/java/io/grpc/xds/internal/extauthz/CertificateUtils.java
index c025383..9d4ea4f 100644
--- a/xds/src/main/java/io/grpc/xds/internal/extauthz/CertificateUtils.java
+++ b/xds/src/main/java/io/grpc/xds/internal/extauthz/CertificateUtils.java
@@ -30,7 +30,7 @@
 /**
  * A utility class for certificate-related information.
  */
-public final class CertificateUtils {
+final class CertificateUtils {
   private static final Logger logger = Logger.getLogger(CertificateUtils.class.getName());
   // From RFC 5280, section 4.2.1.6, Subject Alternative Name
   // dNSName (2)
@@ -48,21 +48,21 @@
    * @param cert The certificate.
    * @return The principal.
    */
-  public static String getPrincipal(X509Certificate cert) {
+  static String getPrincipal(X509Certificate cert) {
     try {
       Collection<List<?>> sans = cert.getSubjectAlternativeNames();
       if (sans != null) {
         // Look for URI SAN (Priority 1).
         for (List<?> san : sans) {
           if (san.size() == 2 && san.get(0) instanceof Integer
-              && (Integer) san.get(0) == SAN_TYPE_URI) {
+              && san.get(0).equals(SAN_TYPE_URI)) {
             return (String) san.get(1);
           }
         }
         // If no URI SAN, look for DNS SAN (Priority 2).
         for (List<?> san : sans) {
           if (san.size() == 2 && san.get(0) instanceof Integer
-              && (Integer) san.get(0) == SAN_TYPE_DNS_NAME) {
+              && san.get(0).equals(SAN_TYPE_DNS_NAME)) {
             return (String) san.get(1);
           }
         }
@@ -82,10 +82,10 @@
    * @throws CertificateEncodingException If an error occurs while encoding the certificate.
    * @throws UnsupportedEncodingException If an error occurs while encoding the URL.
    */
-  public static String getUrlPemEncodedCertificate(X509Certificate cert)
+  static String getUrlPemEncodedCertificate(X509Certificate cert)
       throws CertificateEncodingException, UnsupportedEncodingException {
     String pemCert = CertPemConverter.toPem(cert);
-    return URLEncoder.encode(pemCert, StandardCharsets.UTF_8.toString());
+    return URLEncoder.encode(pemCert, StandardCharsets.UTF_8.name());
   }
 
   /**
@@ -106,7 +106,9 @@
      * @throws CertificateEncodingException If an error occurs while encoding the certificate.
      */
     public static String toPem(X509Certificate cert) throws CertificateEncodingException {
-      return X509_PEM_HEADER + BaseEncoding.base64().encode(cert.getEncoded()) + X509_PEM_FOOTER;
+      return X509_PEM_HEADER
+          + BaseEncoding.base64().withSeparator("\n", 64).encode(cert.getEncoded())
+          + X509_PEM_FOOTER;
     }
   }
 }
diff --git a/xds/src/main/java/io/grpc/xds/internal/extauthz/CheckRequestBuilder.java b/xds/src/main/java/io/grpc/xds/internal/extauthz/CheckRequestBuilder.java
index c16cabd..bb0eb5c 100644
--- a/xds/src/main/java/io/grpc/xds/internal/extauthz/CheckRequestBuilder.java
+++ b/xds/src/main/java/io/grpc/xds/internal/extauthz/CheckRequestBuilder.java
@@ -18,8 +18,11 @@
 
 
 import com.google.common.io.BaseEncoding;
+import com.google.protobuf.ByteString;
 import com.google.protobuf.Timestamp;
 import io.envoyproxy.envoy.config.core.v3.Address;
+import io.envoyproxy.envoy.config.core.v3.HeaderMap;
+import io.envoyproxy.envoy.config.core.v3.HeaderValue;
 import io.envoyproxy.envoy.config.core.v3.SocketAddress;
 import io.envoyproxy.envoy.service.auth.v3.AttributeContext;
 import io.envoyproxy.envoy.service.auth.v3.CheckRequest;
@@ -33,24 +36,24 @@
 import java.security.cert.Certificate;
 import java.security.cert.CertificateEncodingException;
 import java.security.cert.X509Certificate;
-import java.util.ArrayList;
-import java.util.List;
 import java.util.Locale;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 import javax.annotation.Nullable;
+import javax.annotation.concurrent.ThreadSafe;
 import javax.net.ssl.SSLPeerUnverifiedException;
 import javax.net.ssl.SSLSession;
 
 /**
- * Interface for building external authorization check requests.
+ * Builds external authorization check requests from gRPC call metadata.
  */
+@ThreadSafe
 public class CheckRequestBuilder {
 
   /**
    * An interface for providing certificate-related information.
    */
-  public interface CertificateProvider {
+  interface CertificateProvider {
     /**
      * Gets the principal from a certificate.
      *
@@ -72,11 +75,17 @@
   }
 
   private static final Logger logger = Logger.getLogger(CheckRequestBuilder.class.getName());
+  private static final BaseEncoding BASE64_NO_PAD = BaseEncoding.base64().omitPadding();
 
 
   private final ExtAuthzConfig config;
   private final CertificateProvider certificateProvider;
 
+  /**
+   * Constructs a new {@link CheckRequestBuilder} with the default certificate provider.
+   *
+   * @param config The external authorization configuration.
+   */
   public CheckRequestBuilder(ExtAuthzConfig config) {
     this(config, new CertificateProvider() {
       @Override
@@ -92,18 +101,40 @@
     });
   }
 
-  public CheckRequestBuilder(ExtAuthzConfig config, CertificateProvider certificateProvider) {
+  /**
+   * Constructs a new {@link CheckRequestBuilder} with a custom certificate provider.
+   *
+   * @param config The external authorization configuration.
+   * @param certificateProvider The certificate provider.
+   */
+  CheckRequestBuilder(ExtAuthzConfig config, CertificateProvider certificateProvider) {
     this.config = config;
     this.certificateProvider = certificateProvider;
   }
 
 
+  /**
+   * Builds a check request for a client-side call.
+   *
+   * @param methodDescriptor The method descriptor of the RPC.
+   * @param headers The initial metadata headers.
+   * @param requestTime The timestamp when the request was initiated.
+   * @return The constructed {@link CheckRequest}.
+   */
   public CheckRequest buildRequest(MethodDescriptor<?, ?> methodDescriptor, Metadata headers,
       Timestamp requestTime) {
     return build(methodDescriptor, headers, requestTime, null, null, null);
   }
 
 
+  /**
+   * Builds a check request for a server-side call.
+   *
+   * @param serverCall The server call.
+   * @param headers The initial metadata headers.
+   * @param requestTime The timestamp when the request was initiated.
+   * @return The constructed {@link CheckRequest}.
+   */
   public CheckRequest buildRequest(ServerCall<?, ?> serverCall, Metadata headers,
       Timestamp requestTime) {
     java.net.SocketAddress localAddress =
@@ -171,10 +202,24 @@
     AttributeContext.Peer.Builder peerBuilder = AttributeContext.Peer.newBuilder();
     if (socketAddress instanceof InetSocketAddress) {
       InetSocketAddress inetSocketAddress = (InetSocketAddress) socketAddress;
+      // Prefer the resolved IP address, but fall back to the hostname string for
+      // unresolved addresses. In practice, Netty transports always provide resolved
+      // InetSocketAddress instances for active connections, and other gRPC
+      // implementations (C++, Go) always produce IP addresses because they operate
+      // on real TCP sockets. However, Envoy's address.proto permits hostnames (the
+      // only constraint is a non-empty string), so we gracefully fall back to
+      // getHostString() for robustness. See also TcpMetrics.java for precedent:
+      // https://github.com/grpc/grpc-java/blob/master/netty/src/main/java/io/grpc/netty/TcpMetrics.java
+      String address;
+      if (inetSocketAddress.getAddress() != null) {
+        address = inetSocketAddress.getAddress().getHostAddress();
+      } else {
+        address = inetSocketAddress.getHostString();
+      }
       peerBuilder
           .setAddress(Address.newBuilder()
               .setSocketAddress(SocketAddress.newBuilder()
-                  .setAddress(inetSocketAddress.getAddress().getHostAddress())
+                  .setAddress(address)
                   .setPortValue(inetSocketAddress.getPort()))
               .build());
     }
@@ -190,36 +235,55 @@
     httpReqBuilder.setMethod("POST");
     httpReqBuilder.setProtocol("HTTP/2");
     httpReqBuilder.setSize(-1);
+
+    HeaderMap.Builder headerMapBuilder = HeaderMap.newBuilder();
     for (String key : headers.keys()) {
       if (!isAllowed(key)) {
         continue;
       }
-      String value;
+      String lowerCaseKey = key.toLowerCase(Locale.ROOT);
       if (key.endsWith(Metadata.BINARY_HEADER_SUFFIX)) {
-        value = getBinaryHeaderValue(headers, key);
+        populateBinaryHeaderValues(headers, key, lowerCaseKey, headerMapBuilder);
       } else {
-        value = getAsciiHeaderValue(headers, key);
+        populateAsciiHeaderValues(headers, key, lowerCaseKey, headerMapBuilder);
       }
-      httpReqBuilder.putHeaders(key.toLowerCase(Locale.ROOT), value);
     }
+    httpReqBuilder.setHeaderMap(headerMapBuilder);
     reqBuilder.setHttp(httpReqBuilder);
     return reqBuilder.build();
   }
 
-  private String getBinaryHeaderValue(Metadata headers, String key) {
+  private void populateBinaryHeaderValues(Metadata headers, String key, String lowerCaseKey,
+      HeaderMap.Builder headerMapBuilder) {
     Iterable<byte[]> binaryValues =
         headers.getAll(Metadata.Key.of(key, Metadata.BINARY_BYTE_MARSHALLER));
-    List<String> base64Values = new ArrayList<>();
-    for (byte[] value : binaryValues) {
-      base64Values.add(BaseEncoding.base64().encode(value));
+    if (binaryValues != null) {
+      for (byte[] value : binaryValues) {
+        // Binary header values are base64-encoded before storing in rawValue,
+        // matching Envoy's behavior for CheckRequest header serialization.
+        String base64Value = BASE64_NO_PAD.encode(value);
+        headerMapBuilder.addHeaders(
+            HeaderValue.newBuilder()
+                .setKey(lowerCaseKey)
+                .setRawValue(ByteString.copyFromUtf8(base64Value))
+                .build());
+      }
     }
-    return String.join(",", base64Values);
   }
 
-  private String getAsciiHeaderValue(Metadata headers, String key) {
+  private void populateAsciiHeaderValues(Metadata headers, String key, String lowerCaseKey,
+      HeaderMap.Builder headerMapBuilder) {
     Iterable<String> stringValues =
         headers.getAll(Metadata.Key.of(key, Metadata.ASCII_STRING_MARSHALLER));
-    return String.join(",", stringValues);
+    if (stringValues != null) {
+      for (String value : stringValues) {
+        headerMapBuilder.addHeaders(
+            HeaderValue.newBuilder()
+                .setKey(lowerCaseKey)
+                .setRawValue(ByteString.copyFromUtf8(value))
+                .build());
+      }
+    }
   }
 
   private boolean isAllowed(String header) {
diff --git a/xds/src/test/java/io/grpc/xds/internal/extauthz/CheckRequestBuilderTest.java b/xds/src/test/java/io/grpc/xds/internal/extauthz/CheckRequestBuilderTest.java
index 8747e05..c205c57 100644
--- a/xds/src/test/java/io/grpc/xds/internal/extauthz/CheckRequestBuilderTest.java
+++ b/xds/src/test/java/io/grpc/xds/internal/extauthz/CheckRequestBuilderTest.java
@@ -20,30 +20,24 @@
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
-import com.google.protobuf.Any;
+import com.google.common.collect.ImmutableList;
 import com.google.protobuf.Timestamp;
 import io.envoyproxy.envoy.config.core.v3.Address;
-import io.envoyproxy.envoy.config.core.v3.GrpcService;
-import io.envoyproxy.envoy.extensions.filters.http.ext_authz.v3.ExtAuthz;
-import io.envoyproxy.envoy.extensions.grpc_service.call_credentials.access_token.v3.AccessTokenCredentials;
-import io.envoyproxy.envoy.extensions.grpc_service.channel_credentials.google_default.v3.GoogleDefaultCredentials;
+import io.envoyproxy.envoy.config.core.v3.HeaderMap;
 import io.envoyproxy.envoy.service.auth.v3.AttributeContext;
 import io.envoyproxy.envoy.service.auth.v3.CheckRequest;
-import io.envoyproxy.envoy.type.matcher.v3.ListStringMatcher;
-import io.envoyproxy.envoy.type.matcher.v3.StringMatcher;
 import io.grpc.Attributes;
 import io.grpc.Grpc;
 import io.grpc.Metadata;
 import io.grpc.MethodDescriptor;
 import io.grpc.testing.TestMethodDescriptors;
-import io.grpc.xds.ExtAuthzConfigParser;
-import io.grpc.xds.client.Bootstrapper.BootstrapInfo;
-import io.grpc.xds.client.CommonBootstrapperTestUtils;
+import io.grpc.xds.internal.Matchers;
+import io.grpc.xds.internal.extauthz.ExtAuthzTestHelper.TestServerCall;
+import io.grpc.xds.internal.grpcservice.GrpcServiceConfig;
 import java.net.InetSocketAddress;
 import java.net.SocketAddress;
 import java.security.cert.Certificate;
 import java.security.cert.X509Certificate;
-import java.util.Collections;
 import javax.net.ssl.SSLPeerUnverifiedException;
 import javax.net.ssl.SSLSession;
 import org.junit.Before;
@@ -105,12 +99,12 @@
     headers.put(Metadata.Key.of("bin-header-bin", Metadata.BINARY_BYTE_MARSHALLER), binaryValue);
 
     // Configure CheckRequestBuilder to allow specific headers
-    ListStringMatcher allowedHeaders = ListStringMatcher.newBuilder()
-        .addPatterns(StringMatcher.newBuilder().setExact("allowed-header").build())
-        .addPatterns(StringMatcher.newBuilder().setExact("overridden-header").build()).build();
-    ListStringMatcher disallowedHeaders = ListStringMatcher.newBuilder()
-        .addPatterns(StringMatcher.newBuilder().setExact("disallowed-header").build())
-        .addPatterns(StringMatcher.newBuilder().setExact("overridden-header").build()).build();
+    ImmutableList<Matchers.StringMatcher> allowedHeaders = ImmutableList.of(
+        Matchers.StringMatcher.forExact("allowed-header", false),
+        Matchers.StringMatcher.forExact("overridden-header", false));
+    ImmutableList<Matchers.StringMatcher> disallowedHeaders = ImmutableList.of(
+        Matchers.StringMatcher.forExact("disallowed-header", false),
+        Matchers.StringMatcher.forExact("overridden-header", false));
     ExtAuthzConfig config = buildExtAuthzConfig(allowedHeaders, disallowedHeaders, true);
     checkRequestBuilder =
         new CheckRequestBuilder(config, certificateProvider);
@@ -135,10 +129,9 @@
     assertThat(attrContext.getDestination().getPrincipal()).isEqualTo("local-principal");
 
     AttributeContext.HttpRequest http = attrContext.getRequest().getHttp();
-    assertThat(http.getHeadersMap()).containsEntry("allowed-header", "v1");
-    assertThat(http.getHeadersMap()).doesNotContainKey("bin-header-bin");
-    assertThat(http.getHeadersMap()).doesNotContainKey("disallowed-header");
-    assertThat(http.getHeadersMap()).doesNotContainKey("overridden-header");
+    assertThat(http.getHeaderMap().getHeadersList()).containsExactly(
+        io.envoyproxy.envoy.config.core.v3.HeaderValue.newBuilder().setKey("allowed-header")
+            .setRawValue(com.google.protobuf.ByteString.copyFromUtf8("v1")).build());
   }
 
   @Test
@@ -155,7 +148,8 @@
     assertThat(request.getAttributes().getRequest().getHttp().getMethod()).isEqualTo("POST");
     assertThat(request.getAttributes().getRequest().getHttp().getProtocol()).isEqualTo("HTTP/2");
     assertThat(request.getAttributes().getRequest().getHttp().getSize()).isEqualTo(-1);
-    assertThat(request.getAttributes().getRequest().getHttp().getHeadersMap()).isEmpty();
+    assertThat(request.getAttributes().getRequest().getHttp().getHeaderMap().getHeadersList())
+        .isEmpty();
     assertThat(request.getAttributes().hasSource()).isFalse();
     assertThat(request.getAttributes().hasDestination()).isFalse();
   }
@@ -170,9 +164,9 @@
     headers.put(Metadata.Key.of("bin-header-bin", Metadata.BINARY_BYTE_MARSHALLER), binaryValue);
 
     // Configure CheckRequestBuilder with empty allowed headers
-    ListStringMatcher allowedHeaders = ListStringMatcher.newBuilder().build(); // empty
-    ListStringMatcher disallowedHeaders = ListStringMatcher.newBuilder()
-        .addPatterns(StringMatcher.newBuilder().setExact("disallowed-header").build()).build();
+    ImmutableList<Matchers.StringMatcher> allowedHeaders = ImmutableList.of();
+    ImmutableList<Matchers.StringMatcher> disallowedHeaders = ImmutableList.of(
+        Matchers.StringMatcher.forExact("disallowed-header", false));
     ExtAuthzConfig config = buildExtAuthzConfig(allowedHeaders, disallowedHeaders, true);
     checkRequestBuilder =
         new CheckRequestBuilder(config, certificateProvider);
@@ -187,9 +181,11 @@
     AttributeContext.HttpRequest http = attrContext.getRequest().getHttp();
     assertThat(http.getPath()).isEqualTo("/" + methodDescriptor.getFullMethodName());
 
-    assertThat(http.getHeadersMap()).containsEntry("some-header", "v1");
-    assertThat(http.getHeadersMap()).containsEntry("bin-header-bin", "AQID");
-    assertThat(http.getHeadersMap()).doesNotContainKey("disallowed-header");
+    assertThat(http.getHeaderMap().getHeadersList()).containsExactly(
+        io.envoyproxy.envoy.config.core.v3.HeaderValue.newBuilder().setKey("some-header")
+            .setRawValue(com.google.protobuf.ByteString.copyFromUtf8("v1")).build(),
+        io.envoyproxy.envoy.config.core.v3.HeaderValue.newBuilder().setKey("bin-header-bin")
+            .setRawValue(com.google.protobuf.ByteString.copyFromUtf8("AQID")).build());
   }
 
   @Test
@@ -237,8 +233,8 @@
 
   @Test
   public void buildRequest_forServer_includePeerCertFalse() throws Exception {
-    ExtAuthzConfig config = buildExtAuthzConfig(ListStringMatcher.newBuilder().build(),
-        ListStringMatcher.newBuilder().build(), false);
+    ExtAuthzConfig config = buildExtAuthzConfig(ImmutableList.of(),
+        ImmutableList.of(), false);
     checkRequestBuilder =
         new CheckRequestBuilder(config, certificateProvider);
     SocketAddress remoteAddress = new InetSocketAddress("192.168.1.1", 12345);
@@ -329,6 +325,33 @@
   }
 
   @Test
+  public void buildRequest_forServer_unresolvedInetSocketAddress() {
+    SocketAddress localAddress =
+        InetSocketAddress.createUnresolved("local-hostname", 443);
+    SocketAddress remoteAddress =
+        InetSocketAddress.createUnresolved("remote-hostname", 8080);
+    Attributes attributes =
+        Attributes.newBuilder().set(Grpc.TRANSPORT_ATTR_LOCAL_ADDR, localAddress)
+            .set(Grpc.TRANSPORT_ATTR_REMOTE_ADDR, remoteAddress).build();
+    serverCall = new TestServerCall<>(attributes, methodDescriptor);
+
+    CheckRequest request =
+        checkRequestBuilder.buildRequest(serverCall, new Metadata(), requestTime);
+
+    AttributeContext attrContext = request.getAttributes();
+    assertThat(attrContext.hasSource()).isTrue();
+    assertThat(attrContext.getSource().getAddress().getSocketAddress().getAddress())
+        .isEqualTo("remote-hostname");
+    assertThat(attrContext.getSource().getAddress().getSocketAddress().getPortValue())
+        .isEqualTo(8080);
+    assertThat(attrContext.hasDestination()).isTrue();
+    assertThat(attrContext.getDestination().getAddress().getSocketAddress().getAddress())
+        .isEqualTo("local-hostname");
+    assertThat(attrContext.getDestination().getAddress().getSocketAddress().getPortValue())
+        .isEqualTo(443);
+  }
+
+  @Test
   public void buildRequest_forServer_handlesCertificateEncodingException() throws Exception {
     SocketAddress localAddress = new InetSocketAddress("10.0.0.2", 443);
     SocketAddress remoteAddress = new InetSocketAddress("192.168.1.1", 12345);
@@ -353,28 +376,105 @@
     assertThat(source.getCertificate()).isEmpty();
   }
 
-  private ExtAuthzConfig buildExtAuthzConfig() throws ExtAuthzParseException {
-    return buildExtAuthzConfig(ListStringMatcher.newBuilder().build(),
-        ListStringMatcher.newBuilder().build(), true);
+  @Test
+  public void buildRequest_forClient_allowedHeadersFiltering() {
+    Metadata headers = new Metadata();
+    headers.put(Metadata.Key.of("allowed-header", Metadata.ASCII_STRING_MARSHALLER), "v1");
+    headers.put(Metadata.Key.of("not-allowed-header", Metadata.ASCII_STRING_MARSHALLER), "v2");
+
+    // Configure with non-empty allowed headers — only "allowed-header" should pass
+    ImmutableList<Matchers.StringMatcher> allowedHeaders = ImmutableList.of(
+        Matchers.StringMatcher.forExact("allowed-header", false));
+    ExtAuthzConfig config = buildExtAuthzConfig(allowedHeaders, ImmutableList.of(), true);
+    CheckRequestBuilder builder = new CheckRequestBuilder(config, certificateProvider);
+
+    CheckRequest request = builder.buildRequest(methodDescriptor, headers, requestTime);
+
+    HeaderMap headerMap = request.getAttributes().getRequest().getHttp().getHeaderMap();
+    assertThat(headerMap.getHeadersList()).hasSize(1);
+    assertThat(headerMap.getHeadersList().get(0).getKey()).isEqualTo("allowed-header");
+    assertThat(headerMap.getHeadersList().get(0).getRawValue().toStringUtf8()).isEqualTo("v1");
   }
 
-  private ExtAuthzConfig buildExtAuthzConfig(ListStringMatcher allowed,
-      ListStringMatcher disallowed, boolean includePeerCertificate) throws ExtAuthzParseException {
-    Any googleDefaultChannelCreds = Any.pack(GoogleDefaultCredentials.newBuilder().build());
-    Any fakeAccessTokenCreds =
-        Any.pack(AccessTokenCredentials.newBuilder().setToken("fake-token").build());
-    ExtAuthz.Builder builder = ExtAuthz.newBuilder()
-        .setGrpcService(GrpcService.newBuilder()
-            .setGoogleGrpc(GrpcService.GoogleGrpc.newBuilder()
-                .setTargetUri("test-cluster").addChannelCredentialsPlugin(googleDefaultChannelCreds)
-                .addCallCredentialsPlugin(fakeAccessTokenCreds).build())
-            .build())
-        .setIncludePeerCertificate(includePeerCertificate).setAllowedHeaders(allowed)
-        .setDisallowedHeaders(disallowed);
-    BootstrapInfo bootstrapInfo =
-        CommonBootstrapperTestUtils.buildBootStrap(
-            Collections.singletonList("test-cluster"));
-    return ExtAuthzConfigParser.parse(
-        builder.build(), bootstrapInfo, bootstrapInfo.servers().get(0));
+  @Test
+  public void buildRequest_forClient_emptyMetadata() {
+    CheckRequest request =
+        checkRequestBuilder.buildRequest(methodDescriptor, new Metadata(), requestTime);
+
+    AttributeContext attrContext = request.getAttributes();
+    assertThat(attrContext.hasSource()).isFalse();
+    assertThat(attrContext.hasDestination()).isFalse();
+    assertThat(attrContext.getRequest().getHttp().getPath())
+        .isEqualTo("/" + methodDescriptor.getFullMethodName());
+    assertThat(attrContext.getRequest().getHttp().getMethod()).isEqualTo("POST");
+    assertThat(attrContext.getRequest().getHttp().getProtocol()).isEqualTo("HTTP/2");
+    assertThat(attrContext.getRequest().getHttp().getHeaderMap().getHeadersList()).isEmpty();
+  }
+
+  @Test
+  public void buildRequest_multiValuedHeaders() throws Exception {
+    Metadata headers = new Metadata();
+    Metadata.Key<String> asciiKey = Metadata.Key.of("x-custom", Metadata.ASCII_STRING_MARSHALLER);
+    headers.put(asciiKey, "value1");
+    headers.put(asciiKey, "value2");
+
+    Metadata.Key<byte[]> binaryKey =
+        Metadata.Key.of("x-custom-bin", Metadata.BINARY_BYTE_MARSHALLER);
+    headers.put(binaryKey, new byte[]{1, 2});
+    headers.put(binaryKey, new byte[]{3, 4});
+
+    ExtAuthzConfig configWithAllowedHeaders = buildExtAuthzConfig(
+        ImmutableList.of(Matchers.StringMatcher.forExact("x-custom", false),
+            Matchers.StringMatcher.forExact("x-custom-bin", false)),
+        ImmutableList.of(), true);
+    CheckRequestBuilder builderWithConfig =
+        new CheckRequestBuilder(configWithAllowedHeaders, certificateProvider);
+
+    CheckRequest request =
+        builderWithConfig.buildRequest(methodDescriptor, headers, requestTime);
+
+    HeaderMap headerMap = request.getAttributes().getRequest().getHttp().getHeaderMap();
+    assertThat(headerMap.getHeadersList()).hasSize(4);
+    assertThat(headerMap.getHeadersList().get(0).getKey()).isEqualTo("x-custom-bin");
+    assertThat(headerMap.getHeadersList().get(0).getRawValue().toStringUtf8()).isEqualTo("AQI");
+    assertThat(headerMap.getHeadersList().get(1).getKey()).isEqualTo("x-custom-bin");
+    assertThat(headerMap.getHeadersList().get(1).getRawValue().toStringUtf8()).isEqualTo("AwQ");
+    assertThat(headerMap.getHeadersList().get(2).getKey()).isEqualTo("x-custom");
+    assertThat(headerMap.getHeadersList().get(2).getRawValue().toStringUtf8()).isEqualTo("value1");
+    assertThat(headerMap.getHeadersList().get(3).getKey()).isEqualTo("x-custom");
+    assertThat(headerMap.getHeadersList().get(3).getRawValue().toStringUtf8()).isEqualTo("value2");
+  }
+
+  private ExtAuthzConfig buildExtAuthzConfig() {
+    return buildExtAuthzConfig(ImmutableList.of(), ImmutableList.of(), true);
+  }
+
+  private ExtAuthzConfig buildExtAuthzConfig(
+      ImmutableList<Matchers.StringMatcher> allowed,
+      ImmutableList<Matchers.StringMatcher> disallowed,
+      boolean includePeerCertificate) {
+    GrpcServiceConfig.GoogleGrpcConfig googleGrpc = GrpcServiceConfig.GoogleGrpcConfig.builder()
+        .target("test-cluster")
+        .configuredChannelCredentials(io.grpc.xds.client.ConfiguredChannelCredentials.create(
+            mock(io.grpc.ChannelCredentials.class),
+            mock(io.grpc.xds.client.ConfiguredChannelCredentials.ChannelCredsConfig.class)))
+        .build();
+
+    GrpcServiceConfig dummyServiceConfig = GrpcServiceConfig.builder()
+        .googleGrpc(googleGrpc)
+        .initialMetadata(ImmutableList.of())
+        .build();
+
+    return ExtAuthzConfig.builder()
+        .grpcService(dummyServiceConfig)
+        .includePeerCertificate(includePeerCertificate)
+        .allowedHeaders(allowed)
+        .disallowedHeaders(disallowed)
+        .failureModeAllow(true)
+        .failureModeAllowHeaderAdd(false)
+        .denyAtDisable(false)
+        .filterEnabled(Matchers.FractionMatcher.create(100, 100))
+        .statusOnError(io.grpc.Status.INTERNAL)
+        .build();
   }
 }