Repository: googleapis/google-http-java-client
Pull request #1994 (2024-08-16 18:32:29)

deps: update actions/github-script action to v7
[![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [actions/github-script](https://togithub.com/actions/github-script) | action | major | `v6` -> `v7` |

---

### Release Notes

<details>
<summary>actions/github-script (actions/github-script)</summary>

### [`v7`](https://togithub.com/actions/github-script/compare/v6...v7)

[Compare Source](https://togithub.com/actions/github-script/compare/v6...v7)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR was generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View the [repository job log](https://developer.mend.io/github/googleapis/google-http-java-client).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOC4yNi4xIiwidXBkYXRlZEluVmVyIjoiMzguMjYuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->


dpebot: /gcbrun

Patch:
diff --git a/.github/workflows/auto-release.yaml b/.github/workflows/auto-release.yaml
index 7a106d00..18d92e5a 100644
--- a/.github/workflows/auto-release.yaml
+++ b/.github/workflows/auto-release.yaml
@@ -21,7 +21,7 @@ jobs:
     runs-on: ubuntu-latest
     if: contains(github.head_ref, 'release-please')
     steps:
-    - uses: actions/github-script@v6
+    - uses: actions/github-script@v7
       with:
         github-token: ${{secrets.YOSHI_APPROVER_TOKEN}}
         debug: true
diff --git a/.github/workflows/ci-java7.yaml b/.github/workflows/ci-java7.yaml
index 2c8257d4..e2ce98b2 100644
--- a/.github/workflows/ci-java7.yaml
+++ b/.github/workflows/ci-java7.yaml
@@ -55,7 +55,7 @@ jobs:
         # dailymotion-simple-cmdline-sample and google-http-client-assembly depend on
         # google-http-client-jackson2 
         mvn --batch-mode --show-version -ntp test \
-            --projects '!google-http-client-jackson2,!google-http-client-appengine,!samples/dailymotion-simple-cmdline-sample,!google-http-client-assembly' \
+            --projects '!google-http-client-jackson2,!google-http-client-appengine,!samples/dailymotion-simple-cmdline-sample,!google-http-client-assembly,!google-http-client-apache-v5' \
             -Dclirr.skip=true -Denforcer.skip=true -Dmaven.javadoc.skip=true \
             -Dgcloud.download.skip=true -T 1C \
             -Dproject.surefire.version=2.22.2 \
diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml
index 7dd91434..dc9e72ff 100644
--- a/.github/workflows/scorecard.yml
+++ b/.github/workflows/scorecard.yml
@@ -59,7 +59,7 @@ jobs:
       # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
       # format to the repository Actions tab.
       - name: "Upload artifact"
-        uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
+        uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
         with:
           name: SARIF file
           path: results.sarif
@@ -67,6 +67,6 @@ jobs:
 
       # Upload the results to GitHub's code scanning dashboard.
       - name: "Upload to code-scanning"
-        uses: github/codeql-action/upload-sarif@17573ee1cc1b9d061760f3a006fc4aac4f944fd5 # v2.2.4
+        uses: github/codeql-action/upload-sarif@883d8588e56d1753a8a58c1c86e88976f0c23449 # v3.26.3
         with:
           sarif_file: results.sarif
diff --git a/.kokoro/presubmit/graalvm-native-a.cfg b/.kokoro/presubmit/graalvm-native-a.cfg
index ad109968..54dca4f2 100644
--- a/.kokoro/presubmit/graalvm-native-a.cfg
+++ b/.kokoro/presubmit/graalvm-native-a.cfg
@@ -3,7 +3,7 @@
 # Configure the docker image for kokoro-trampoline.
 env_vars: {
   key: "TRAMPOLINE_IMAGE"
-  value: "gcr.io/cloud-devrel-public-resources/graalvm_a:1.9.0"
+  value: "gcr.io/cloud-devrel-public-resources/graalvm_a:1.11.0"
 }
 
 env_vars: {
diff --git a/.kokoro/presubmit/graalvm-native-b.cfg b/.kokoro/presubmit/graalvm-native-b.cfg
index e5229b14..8b05c77f 100644
--- a/.kokoro/presubmit/graalvm-native-b.cfg
+++ b/.kokoro/presubmit/graalvm-native-b.cfg
@@ -3,7 +3,7 @@
 # Configure the docker image for kokoro-trampoline.
 env_vars: {
   key: "TRAMPOLINE_IMAGE"
-  value: "gcr.io/cloud-devrel-public-resources/graalvm_b:1.9.0"
+  value: "gcr.io/cloud-devrel-public-resources/graalvm_b:1.11.0"
 }
 
 env_vars: {
diff --git a/docs/http-transport.md b/docs/http-transport.md
index 0167db4a..b6d94cce 100644
--- a/docs/http-transport.md
+++ b/docs/http-transport.md
@@ -19,8 +19,12 @@ There are three built-in low-level HTTP transports:
 
 1. [`NetHttpTransport`][net-http-transport]: based on [`HttpURLConnection`][http-url-connection]
 that is found in all Java SDKs, and thus usually the simplest choice.
+1. [`Apache5HttpTransport`][apache-http-transport]: based on the popular
+   [Apache 5.x HttpClient][apache5-http-client] that allows for more customization.
 1. [`ApacheHttpTransport`][apache-http-transport]: based on the popular
-[Apache HttpClient][apache-http-client] that allows for more customization.
+[Apache 4.x HttpClient][apache-http-client] that allows for more customization. Note that this transport implementation
+relies on [Apache 4.x HttpCore][apache-http-core] which has reached end of life. It is recommended to use
+[`Apache5HttpTransport`][apache-http-transport] instead.
 1. [`UrlFetchTransport`][url-fetch-transport]: based on the [URL Fetch Java API][url-fetch] in the
 Google App Engine SDK.
 
@@ -124,7 +128,10 @@ HttpRequestFactory requestFactory = transport.createRequestFactory(new MyInitial
 [net-http-transport]: https://googleapis.dev/java/google-http-client/latest/index.html?com/google/api/client/http/javanet/NetHttpTransport.html
 [http-url-connection]: http://docs.oracle.com/javase/7/docs/api/java/net/HttpURLConnection.html
 [apache-http-transport]: https://googleapis.dev/java/google-http-client/latest/index.html?com/google/api/client/http/apache/v2/ApacheHttpTransport.html
-[apache-http-client]: http://hc.apache.org/httpcomponents-client-ga/index.html
+[apache5-http-transport]: https://github.com/googleapis/google-http-java-client/blob/de8743587d1415e8a6046096ac1fc0a5e81490c3/google-http-client-apache-v5/src/main/java/com/google/api/client/http/apache/v5/Apache5HttpTransport.java
+[apache-http-client]: https://hc.apache.org/httpcomponents-client-4.5.x/index.html
+[apache-http-core]: https://hc.apache.org/httpcomponents-core-4.4.x/index.html
+[apache5-http-client]: https://hc.apache.org/httpcomponents-client-5.3.x/index.html
 [url-fetch-transport]: https://googleapis.dev/java/google-http-client/latest/index.html?com/google/api/client/extensions/appengine/http/UrlFetchTransport.html
 [url-fetch]: https://cloud.google.com/appengine/docs/java/javadoc/com/google/appengine/api/urlfetch/package-summary
 [logger]: https://docs.oracle.com/javase/7/docs/api/java/util/logging/Logger.html
diff --git a/google-http-client-apache-v5/pom.xml b/google-http-client-apache-v5/pom.xml
new file mode 100644
index 00000000..a6789797
--- /dev/null
+++ b/google-http-client-apache-v5/pom.xml
@@ -0,0 +1,116 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+  <parent>
+    <groupId>com.google.http-client</groupId>
+    <artifactId>google-http-client-parent</artifactId>
+    <version>1.44.3-SNAPSHOT</version><!-- {x-version-update:google-http-client-parent:current} -->
+    <relativePath>../pom.xml</relativePath>
+  </parent>
+  <artifactId>google-http-client-apache-v5</artifactId>
+  <version>1.44.3-SNAPSHOT</version><!-- {x-version-update:google-http-client-apache-v5:current} -->
+  <name>Apache HTTP transport v5 for the Google HTTP Client Library for Java.</name>
+
+  <build>
+    <plugins>
+      <plugin>
+        <artifactId>maven-javadoc-plugin</artifactId>
+        <configuration>
+          <links>
+            <link>https://download.oracle.com/javase/7/docs/api/</link>
+          </links>
+          <doctitle>${project.name} ${project.version}</doctitle>
+          <windowtitle>${project.artifactId} ${project.version}</windowtitle>
+        </configuration>
+      </plugin>
+      <plugin>
+        <artifactId>maven-source-plugin</artifactId>
+      </plugin>
+      <plugin>
+        <groupId>org.codehaus.mojo</groupId>
+        <artifactId>build-helper-maven-plugin</artifactId>
+        <version>3.3.0</version>
+        <executions>
+          <execution>
+            <id>add-test-source</id>
+            <phase>generate-test-sources</phase>
+            <goals>
+              <goal>add-test-source</goal>
+            </goals>
+            <configuration>
+              <sources>
+                <source>target/generated-test-sources</source>
+              </sources>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <artifactId>maven-jar-plugin</artifactId>
+        <configuration>
+          <archive>
+            <manifestFile>${project.build.outputDirectory}/META-INF/MANIFEST.MF</manifestFile>
+            <manifestEntries>
+              <Automatic-Module-Name>com.google.api.client.http.apache.v5</Automatic-Module-Name>
+            </manifestEntries>
+          </archive>
+        </configuration>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.felix</groupId>
+        <artifactId>maven-bundle-plugin</artifactId>
+        <version>5.1.9</version>
+        <executions>
+          <execution>
+            <id>bundle-manifest</id>
+            <phase>process-classes</phase>
+            <goals>
+              <goal>manifest</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <artifactId>maven-compiler-plugin</artifactId>
+        <version>3.13.0</version>
+        <configuration>
+          <source>1.8</source>
+          <target>1.8</target>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
+  <dependencies>
+    <dependency>
+      <groupId>com.google.http-client</groupId>
+      <artifactId>google-http-client</artifactId>
+      <exclusions>
+        <exclusion>
+          <groupId>org.apache.httpcomponents</groupId>
+          <artifactId>httpcore</artifactId>
+        </exclusion>
+        <exclusion>
+          <groupId>org.apache.httpcomponents</groupId>
+          <artifactId>httpclient</artifactId>
+        </exclusion>
+      </exclusions>
+    </dependency>
+    <dependency>
+      <groupId>com.google.guava</groupId>
+      <artifactId>guava</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.httpcomponents.client5</groupId>
+      <artifactId>httpclient5</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.httpcomponents.core5</groupId>
+      <artifactId>httpcore5</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+</project>
diff --git a/google-http-client-apache-v5/src/main/java/com/google/api/client/http/apache/v5/Apache5ContentEntity.java b/google-http-client-apache-v5/src/main/java/com/google/api/client/http/apache/v5/Apache5ContentEntity.java
new file mode 100644
index 00000000..4a5ab84e
--- /dev/null
+++ b/google-http-client-apache-v5/src/main/java/com/google/api/client/http/apache/v5/Apache5ContentEntity.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * 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
+ *
+ * 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 com.google.api.client.http.apache.v5;
+
+import com.google.api.client.util.Preconditions;
+import com.google.api.client.util.StreamingContent;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import org.apache.hc.core5.http.io.entity.AbstractHttpEntity;
+
+/**
+ * Translation class to make google-http-client entity conform with Apache 5.x {@link
+ * AbstractHttpEntity}
+ */
+final class Apache5ContentEntity extends AbstractHttpEntity {
+
+  /** Content length or less than zero if not known. */
+  private final long contentLength;
+
+  /** Streaming content. */
+  private final StreamingContent streamingContent;
+
+  /**
+   * @param contentLength content length or less than zero if not known
+   * @param streamingContent streaming content
+   */
+  Apache5ContentEntity(
+      long contentLength,
+      StreamingContent streamingContent,
+      String contentType,
+      String contentEncoding) {
+    super(contentType, contentEncoding, contentLength == -1);
+    this.contentLength = contentLength;
+    this.streamingContent = Preconditions.checkNotNull(streamingContent);
+  }
+
+  @Override
+  public InputStream getContent() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public long getContentLength() {
+    return contentLength;
+  }
+
+  @Override
+  public boolean isRepeatable() {
+    return false;
+  }
+
+  @Override
+  public boolean isStreaming() {
+    return true;
+  }
+
+  @Override
+  public void writeTo(OutputStream out) throws IOException {
+    if (contentLength != 0) {
+      streamingContent.writeTo(out);
+    }
+  }
+
+  @Override
+  public void close() throws IOException {}
+}
diff --git a/google-http-client-apache-v5/src/main/java/com/google/api/client/http/apache/v5/Apache5HttpRequest.java b/google-http-client-apache-v5/src/main/java/com/google/api/client/http/apache/v5/Apache5HttpRequest.java
new file mode 100644
index 00000000..99d6eca8
--- /dev/null
+++ b/google-http-client-apache-v5/src/main/java/com/google/api/client/http/apache/v5/Apache5HttpRequest.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * 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
+ *
+ * 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 com.google.api.client.http.apache.v5;
+
+import com.google.api.client.http.LowLevelHttpRequest;
+import com.google.api.client.http.LowLevelHttpResponse;
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+import org.apache.hc.client5.http.ClientProtocolException;
+import org.apache.hc.client5.http.classic.HttpClient;
+import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase;
+import org.apache.hc.client5.http.config.RequestConfig;
+import org.apache.hc.client5.http.routing.RoutingSupport;
+import org.apache.hc.core5.http.ClassicHttpResponse;
+import org.apache.hc.core5.http.HttpException;
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.util.Timeout;
+
+public final class Apache5HttpRequest extends LowLevelHttpRequest {
+
+  private final HttpUriRequestBase request;
+
+  private final RequestConfig.Builder requestConfig;
+
+  private final HttpClient httpClient;
+
+  Apache5HttpRequest(HttpClient httpClient, HttpUriRequestBase request) {
+    this.httpClient = httpClient;
+    this.request = request;
+    // disable redirects as google-http-client handles redirects
+    this.requestConfig = RequestConfig.custom().setRedirectsEnabled(false);
+  }
+
+  @Override
+  public void addHeader(String name, String value) {
+    request.addHeader(name, value);
+  }
+
+  @Override
+  public void setTimeout(int connectTimeout, int readTimeout) throws IOException {
+    requestConfig
+        .setConnectTimeout(Timeout.of(connectTimeout, TimeUnit.MILLISECONDS))
+        // ResponseTimeout behaves the same as 4.x's SocketTimeout
+        .setResponseTimeout(Timeout.of(readTimeout, TimeUnit.MILLISECONDS));
+  }
+
+  @Override
+  public LowLevelHttpResponse execute() throws IOException {
+    if (getStreamingContent() != null) {
+      Apache5ContentEntity entity =
+          new Apache5ContentEntity(
+              getContentLength(), getStreamingContent(), getContentType(), getContentEncoding());
+      request.setEntity(entity);
+    }
+    request.setConfig(requestConfig.build());
+    HttpHost target;
+    try {
+      target = RoutingSupport.determineHost(request);
+    } catch (HttpException e) {
+      throw new ClientProtocolException("The request's host is invalid.", e);
+    }
+    // we use a null context so the client creates the default one internally
+    ClassicHttpResponse httpResponse = httpClient.executeOpen(target, request, null);
+    return new Apache5HttpResponse(request, httpResponse);
+  }
+}
diff --git a/google-http-client-apache-v5/src/main/java/com/google/api/client/http/apache/v5/Apache5HttpResponse.java b/google-http-client-apache-v5/src/main/java/com/google/api/client/http/apache/v5/Apache5HttpResponse.java
new file mode 100644
index 00000000..1574c8c8
--- /dev/null
+++ b/google-http-client-apache-v5/src/main/java/com/google/api/client/http/apache/v5/Apache5HttpResponse.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * 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
+ *
+ * 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 com.google.api.client.http.apache.v5;
+
+import com.google.api.client.http.LowLevelHttpResponse;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.logging.Logger;
+import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase;
+import org.apache.hc.core5.http.ClassicHttpResponse;
+import org.apache.hc.core5.http.Header;
+import org.apache.hc.core5.http.HttpEntity;
+import org.apache.hc.core5.http.message.StatusLine;
+
+final class Apache5HttpResponse extends LowLevelHttpResponse {
+
+  private static final Logger LOGGER = Logger.getLogger(Apache5HttpResponse.class.getName());
+  private final HttpUriRequestBase request;
+  private final ClassicHttpResponse response;
+  private final Header[] allHeaders;
+  private final HttpEntity entity;
+
+  Apache5HttpResponse(HttpUriRequestBase request, ClassicHttpResponse response) {
+    this.request = request;
+    this.response = response;
+    this.allHeaders = response.getHeaders();
+    this.entity = response.getEntity();
+  }
+
+  @Override
+  public int getStatusCode() {
+    return response.getCode();
+  }
+
+  @Override
+  public InputStream getContent() throws IOException {
+    return new Apache5ResponseContent(entity.getContent(), response);
+  }
+
+  @Override
+  public String getContentEncoding() {
+    return entity != null ? entity.getContentEncoding() : null;
+  }
+
+  @Override
+  public long getContentLength() {
+    return entity == null ? -1 : entity.getContentLength();
+  }
+
+  @Override
+  public String getContentType() {
+    return entity == null ? null : entity.getContentType();
+  }
+
+  @Override
+  public String getReasonPhrase() {
+    return response.getReasonPhrase();
+  }
+
+  @Override
+  public String getStatusLine() {
+    return new StatusLine(response).toString();
+  }
+
+  public String getHeaderValue(String name) {
+    return response.getLastHeader(name).getValue();
+  }
+
+  @Override
+  public int getHeaderCount() {
+    return allHeaders.length;
+  }
+
+  @Override
+  public String getHeaderName(int index) {
+    return allHeaders[index].getName();
+  }
+
+  @Override
+  public String getHeaderValue(int index) {
+    return allHeaders[index].getValue();
+  }
+
+  /** Aborts execution of the request. */
+  @Override
+  public void disconnect() throws IOException {
+    request.abort();
+    response.close();
+  }
+}
diff --git a/google-http-client-apache-v5/src/main/java/com/google/api/client/http/apache/v5/Apache5HttpTransport.java b/google-http-client-apache-v5/src/main/java/com/google/api/client/http/apache/v5/Apache5HttpTransport.java
new file mode 100644
index 00000000..868a2cf9
--- /dev/null
+++ b/google-http-client-apache-v5/src/main/java/com/google/api/client/http/apache/v5/Apache5HttpTransport.java
@@ -0,0 +1,221 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * 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
+ *
+ * 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 com.google.api.client.http.apache.v5;
+
+import com.google.api.client.http.HttpMethods;
+import com.google.api.client.http.HttpTransport;
+import com.google.common.annotations.Beta;
+import com.google.common.base.Preconditions;
+import java.io.IOException;
+import java.net.ProxySelector;
+import java.net.URI;
+import java.util.concurrent.TimeUnit;
+import org.apache.hc.client5.http.classic.HttpClient;
+import org.apache.hc.client5.http.classic.methods.HttpDelete;
+import org.apache.hc.client5.http.classic.methods.HttpGet;
+import org.apache.hc.client5.http.classic.methods.HttpHead;
+import org.apache.hc.client5.http.classic.methods.HttpOptions;
+import org.apache.hc.client5.http.classic.methods.HttpPatch;
+import org.apache.hc.client5.http.classic.methods.HttpPost;
+import org.apache.hc.client5.http.classic.methods.HttpPut;
+import org.apache.hc.client5.http.classic.methods.HttpTrace;
+import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase;
+import org.apache.hc.client5.http.config.ConnectionConfig;
+import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
+import org.apache.hc.client5.http.impl.classic.HttpClients;
+import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
+import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
+import org.apache.hc.client5.http.impl.routing.SystemDefaultRoutePlanner;
+import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory;
+import org.apache.hc.core5.io.CloseMode;
+import org.apache.hc.core5.io.ModalCloseable;
+
+/**
+ * Thread-safe HTTP transport based on the Apache HTTP Client library.
+ *
+ * <p>Implementation is thread-safe, as long as any parameter modification to the {@link
+ * #getHttpClient() Apache HTTP Client} is only done at initialization time. For maximum efficiency,
+ * applications should use a single globally-shared instance of the HTTP transport.
+ *
+ * <p>Default settings are specified in {@link #newDefaultHttpClient()}. Use the {@link
+ * #Apache5HttpTransport(HttpClient)} constructor to override the Apache HTTP Client used. Please
+ * read the <a
+ * href="https://github.com/apache/httpcomponents-client/blob/f898f1aca38f77f62a007856a674629cae5a02e6/httpclient5/src/test/java/org/apache/hc/client5/http/examples/ClientConfiguration.java">
+ * Apache HTTP Client 5.x configuration example</a> for more complex configuration options.
+ */
+public final class Apache5HttpTransport extends HttpTransport {
+
+  /** Apache HTTP client. */
+  private final HttpClient httpClient;
+
+  /** If the HTTP client uses mTLS channel. */
+  private final boolean isMtls;
+
+  /** Constructor that uses {@link #newDefaultHttpClient()} for the Apache HTTP client. */
+  public Apache5HttpTransport() {
+    this(newDefaultHttpClient(), false);
+  }
+
+  /**
+   * Constructor that allows an alternative Apache HTTP client to be used.
+   *
+   * <p>If you choose to provide your own Apache HttpClient implementation, be sure that
+   *
+   * <ul>
+   *   <li>HTTP version is set to 1.1.
+   *   <li>Retries are disabled (google-http-client handles retries).
+   * </ul>
+   *
+   * @param httpClient Apache HTTP client to use
+   */
+  public Apache5HttpTransport(HttpClient httpClient) {
+    this.httpClient = httpClient;
+    this.isMtls = false;
+  }
+
+  /**
+   * {@link Beta} <br>
+   * Constructor that allows an alternative CLoseable Apache HTTP client to be used.
+   *
+   * <p>If you choose to provide your own Apache HttpClient implementation, be sure that
+   *
+   * <ul>
+   *   <li>HTTP version is set to 1.1.
+   *   <li>Retries are disabled (google-http-client handles retries).
+   *   <li>Redirects are disabled (google-http-client handles retries).
+   * </ul>
+   *
+   * @param httpClient Apache HTTP client to use
+   * @param isMtls If the HTTP client is mutual TLS
+   */
+  @Beta
+  public Apache5HttpTransport(HttpClient httpClient, boolean isMtls) {
+    this.httpClient = httpClient;
+    this.isMtls = isMtls;
+  }
+
+  /**
+   * Creates a new instance of the Apache HTTP client that is used by the {@link
+   * #Apache5HttpTransport()} constructor.
+   *
+   * <p>Settings:
+   *
+   * <ul>
+   *   <li>The client connection manager is set to {@link PoolingHttpClientConnectionManager}.
+   *   <li>The retry mechanism is turned off using {@link
+   *       HttpClientBuilder#disableAutomaticRetries()}.
+   *   <li>Redirects are turned off using {@link HttpClientBuilder#disableRedirectHandling}.
+   *   <li>The route planner uses {@link SystemDefaultRoutePlanner} with {@link
+   *       ProxySelector#getDefault()}, which uses the proxy settings from <a
+   *       href="https://docs.oracle.com/javase/8/docs/api/java/net/doc-files/net-properties.html">system
+   *       properties</a>.
+   * </ul>
+   *
+   * @return new instance of the Apache HTTP client
+   */
+  public static HttpClient newDefaultHttpClient() {
+    return newDefaultHttpClientBuilder().build();
+  }
+
+  /**
+   * Creates a new Apache HTTP client builder that is used by the {@link #Apache5HttpTransport()}
+   * constructor.
+   *
+   * <p>Settings:
+   *
+   * <ul>
+   *   <li>The client connection manager is set to {@link PoolingHttpClientConnectionManager}.
+   *   <li>The retry mechanism is turned off using {@link
+   *       HttpClientBuilder#disableAutomaticRetries()}.
+   *   <li>Redirects are turned off using {@link HttpClientBuilder#disableRedirectHandling}.
+   *   <li>The route planner uses {@link SystemDefaultRoutePlanner} with {@link
+   *       ProxySelector#getDefault()}, which uses the proxy settings from <a
+   *       href="http://docs.oracle.com/javase/8/docs/api/java/net/doc-files/net-properties.html">system
+   *       properties</a>.
+   * </ul>
+   *
+   * @return new instance of the Apache HTTP client builder
+   */
+  public static HttpClientBuilder newDefaultHttpClientBuilder() {
+    PoolingHttpClientConnectionManager connectionManager =
+        PoolingHttpClientConnectionManagerBuilder.create()
+            .setSSLSocketFactory(SSLConnectionSocketFactory.getSocketFactory())
+            .setMaxConnTotal(200)
+            .setMaxConnPerRoute(20)
+            .setDefaultConnectionConfig(
+                ConnectionConfig.custom().setTimeToLive(-1, TimeUnit.MILLISECONDS).build())
+            .build();
+
+    return HttpClients.custom()
+        .useSystemProperties()
+        .setConnectionManager(connectionManager)
+        .setRoutePlanner(new SystemDefaultRoutePlanner(ProxySelector.getDefault()))
+        .disableRedirectHandling()
+        .disableAutomaticRetries();
+  }
+
+  @Override
+  public boolean supportsMethod(String method) {
+    return true;
+  }
+
+  @Override
+  protected Apache5HttpRequest buildRequest(String method, String url) {
+    HttpUriRequestBase requestBase;
+    if (method.equals(HttpMethods.DELETE)) {
+      requestBase = new HttpDelete(url);
+    } else if (method.equals(HttpMethods.GET)) {
+      requestBase = new HttpGet(url);
+    } else if (method.equals(HttpMethods.HEAD)) {
+      requestBase = new HttpHead(url);
+    } else if (method.equals(HttpMethods.PATCH)) {
+      requestBase = new HttpPatch(url);
+    } else if (method.equals(HttpMethods.POST)) {
+      requestBase = new HttpPost(url);
+    } else if (method.equals(HttpMethods.PUT)) {
+      requestBase = new HttpPut(url);
+    } else if (method.equals(HttpMethods.TRACE)) {
+      requestBase = new HttpTrace(url);
+    } else if (method.equals(HttpMethods.OPTIONS)) {
+      requestBase = new HttpOptions(url);
+    } else {
+      requestBase = new HttpUriRequestBase(Preconditions.checkNotNull(method), URI.create(url));
+    }
+    return new Apache5HttpRequest(httpClient, requestBase);
+  }
+
+  /**
+   * Gracefully shuts down the connection manager and releases allocated resources. This closes all
+   * connections, whether they are currently used or not.
+   */
+  @Override
+  public void shutdown() throws IOException {
+    if (httpClient instanceof ModalCloseable) {
+      ((ModalCloseable) httpClient).close(CloseMode.GRACEFUL);
+    }
+    // otherwise no-op
+  }
+
+  /** Returns the Apache HTTP client. */
+  public HttpClient getHttpClient() {
+    return httpClient;
+  }
+
+  /** Returns if the underlying HTTP client is mTLS. */
+  @Override
+  public boolean isMtls() {
+    return isMtls;
+  }
+}
diff --git a/google-http-client-apache-v5/src/main/java/com/google/api/client/http/apache/v5/Apache5ResponseContent.java b/google-http-client-apache-v5/src/main/java/com/google/api/client/http/apache/v5/Apache5ResponseContent.java
new file mode 100644
index 00000000..c2d3091d
--- /dev/null
+++ b/google-http-client-apache-v5/src/main/java/com/google/api/client/http/apache/v5/Apache5ResponseContent.java
@@ -0,0 +1,75 @@
+package com.google.api.client.http.apache.v5;
+
+import com.google.common.annotations.VisibleForTesting;
+import java.io.IOException;
+import java.io.InputStream;
+import org.apache.hc.core5.http.ClassicHttpResponse;
+import org.apache.hc.core5.http.HttpResponse;
+
+/**
+ * Class that wraps an {@link org.apache.hc.core5.http.HttpEntity}'s content {@link InputStream}
+ * along with the {@link ClassicHttpResponse} that contains this entity. The main purpose is to be
+ * able to close the response as well as the content input stream when {@link #close()} is called,
+ * in order to not break the existing contract with clients using apache v4 that only required them
+ * to close the input stream to clean up all resources.
+ */
+public class Apache5ResponseContent extends InputStream {
+  private final ClassicHttpResponse response;
+  private final InputStream wrappedStream;
+
+  public Apache5ResponseContent(InputStream wrappedStream, ClassicHttpResponse response) {
+    this.response = response;
+    this.wrappedStream = wrappedStream;
+  }
+
+  @Override
+  public int read() throws IOException {
+    return wrappedStream.read();
+  }
+
+  @Override
+  public int read(byte b[]) throws IOException {
+    return wrappedStream.read(b);
+  }
+
+  @Override
+  public int read(byte b[], int off, int len) throws IOException {
+    return wrappedStream.read(b, off, len);
+  }
+
+  @Override
+  public long skip(long n) throws IOException {
+    return wrappedStream.skip(n);
+  }
+
+  @Override
+  public int available() throws IOException {
+    return wrappedStream.available();
+  }
+
+  @Override
+  public synchronized void mark(int readlimit) {
+    wrappedStream.mark(readlimit);
+  }
+
+  @Override
+  public synchronized void reset() throws IOException {
+    wrappedStream.reset();
+  }
+
+  @Override
+  public void close() throws IOException {
+    wrappedStream.close();
+    response.close();
+  }
+
+  @Override
+  public boolean markSupported() {
+    return wrappedStream.markSupported();
+  }
+
+  @VisibleForTesting
+  HttpResponse getResponse() {
+    return response;
+  }
+}
diff --git a/google-http-client-apache-v5/src/main/java/com/google/api/client/http/apache/v5/package-info.java b/google-http-client-apache-v5/src/main/java/com/google/api/client/http/apache/v5/package-info.java
new file mode 100644
index 00000000..223edc82
--- /dev/null
+++ b/google-http-client-apache-v5/src/main/java/com/google/api/client/http/apache/v5/package-info.java
@@ -0,0 +1,16 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * 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
+ *
+ * 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.
+ */
+
+/** HTTP Transport library for Google API's based on Apache HTTP Client/Core version 5.x */
+package com.google.api.client.http.apache.v5;
diff --git a/google-http-client-assembly/classpath-include b/google-http-client-assembly/classpath-include
index c1bd8032..c7bbd573 100644
--- a/google-http-client-assembly/classpath-include
+++ b/google-http-client-assembly/classpath-include
@@ -7,8 +7,8 @@
 	<classpathentry kind="lib" path="libs/google-http-client-xml-${project.version}.jar" sourcepath="libs-sources/google-http-client-xml-${project.version}-sources.jar"/>
 	<classpathentry kind="lib" path="libs/commons-logging-${project.commons-logging.version}.jar" sourcepath="libs-sources/commons-logging-${project.commons-logging.version}-sources.jar"/>
 	<classpathentry kind="lib" path="libs/gson-${project.gson.version}.jar" sourcepath="libs-sources/gson-${project.gson.version}-sources.jar"/>
-	<classpathentry kind="lib" path="libs/httpclient-${project.httpclient.version}.jar" sourcepath="libs-sources/httpclient-${project.httpclient.version}-sources.jar"/>
-	<classpathentry kind="lib" path="libs/httpcore-${project.httpcore.version}.jar" sourcepath="libs-sources/httpcore-${project.httpcore.version}-sources.jar"/>
+	<classpathentry kind="lib" path="libs/httpclient-${project.apache-httpclient-4.version}.jar" sourcepath="libs-sources/httpclient-${project.apache-httpclient-4.version}-sources.jar"/>
+	<classpathentry kind="lib" path="libs/httpcore-${project.apache-httpcore-4.version}.jar" sourcepath="libs-sources/httpcore-${project.apache-httpcore-4.version}-sources.jar"/>
 	<classpathentry kind="lib" path="libs/jackson-core-asl-${project.jackson-core-asl.version}.jar" sourcepath="libs-sources/jackson-core-asl-${project.jackson-core-asl.version}-sources.jar"/>
 	<classpathentry kind="lib" path="libs/jackson-core-${project.jackson-core2.version}.jar" sourcepath="libs-sources/jackson-core-${project.jackson-core2.version}-sources.jar"/>
 	<classpathentry kind="lib" path="libs/jsr305-${project.jsr305.version}.jar"/>
diff --git a/google-http-client-assembly/readme.html b/google-http-client-assembly/readme.html
index 5e7af564..8a146df1 100644
--- a/google-http-client-assembly/readme.html
+++ b/google-http-client-assembly/readme.html
@@ -135,8 +135,8 @@
   required for general purpose Java applications :
   <ul>
     <li>commons-logging-${project.commons-logging.version}.jar</li>
-    <li>httpclient-${project.httpclient.version}.jar</li>
-    <li>httpcore-${project.httpcore.version}.jar</li>
+    <li>httpclient-${project.apache-httpclient-4.version}.jar</li>
+    <li>httpcore-${project.apache-httpcore-4.version}.jar</li>
   </ul>
 
 </body>
diff --git a/pom.xml b/pom.xml
index 5760c3e6..b7973cc7 100644
--- a/pom.xml
+++ b/pom.xml
@@ -62,6 +62,7 @@
     <module>google-http-client-appengine</module>
     <module>google-http-client-android</module>
     <module>google-http-client-apache-v2</module>
+    <module>google-http-client-apache-v5</module>
     <module>google-http-client-protobuf</module>
     <module>google-http-client-gson</module>
     <module>google-http-client-jackson2</module>
@@ -92,7 +93,7 @@
   <parent>
     <groupId>com.google.cloud</groupId>
     <artifactId>native-image-shared-config</artifactId>
-    <version>1.9.0</version>
+    <version>1.11.0</version>
   </parent>
 
   <!--
@@ -156,12 +157,22 @@
       <dependency>
         <groupId>org.apache.httpcomponents</groupId>
         <artifactId>httpclient</artifactId>
-        <version>${project.httpclient.version}</version>
+        <version>${project.apache-httpclient-4.version}</version>
       </dependency>
       <dependency>
         <groupId>org.apache.httpcomponents</groupId>
         <artifactId>httpcore</artifactId>
-        <version>${project.httpcore.version}</version>
+        <version>${project.apache-httpcore-4.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.httpcomponents.client5</groupId>
+        <artifactId>httpclient5</artifactId>
+        <version>${project.apache-httpclient-5.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.httpcomponents.core5</groupId>
+        <artifactId>httpcore5</artifactId>
+        <version>${project.apache-httpcore-5.version}</version>
       </dependency>
       <dependency>
         <groupId>com.google.guava</groupId>
@@ -236,7 +247,7 @@
       <dependency>
         <groupId>com.google.j2objc</groupId>
         <artifactId>j2objc-annotations</artifactId>
-        <version>2.8</version>
+        <version>3.0.0</version>
       </dependency>
       <dependency>
         <groupId>io.grpc</groupId>
@@ -361,7 +372,7 @@
         <plugin>
           <groupId>org.codehaus.mojo</groupId>
           <artifactId>animal-sniffer-maven-plugin</artifactId>
-          <version>1.23</version>
+          <version>1.24</version>
         </plugin>
         <plugin>
           <groupId>org.apache.maven.plugins</groupId>
@@ -597,12 +608,14 @@
     <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
     <project.jsr305.version>3.0.2</project.jsr305.version>
     <project.gson.version>2.10.1</project.gson.version>
-    <project.jackson-core2.version>2.14.2</project.jackson-core2.version>
+    <project.jackson-core2.version>2.17.2</project.jackson-core2.version>
     <project.protobuf-java.version>3.21.12</project.protobuf-java.version>
     <project.guava.version>30.1.1-android</project.guava.version>
     <project.xpp3.version>1.1.4c</project.xpp3.version>
-    <project.httpclient.version>4.5.14</project.httpclient.version>
-    <project.httpcore.version>4.4.16</project.httpcore.version>
+    <project.apache-httpclient-4.version>4.5.14</project.apache-httpclient-4.version>
+    <project.apache-httpcore-4.version>4.4.16</project.apache-httpcore-4.version>
+    <project.apache-httpclient-5.version>5.3.1</project.apache-httpclient-5.version>
+    <project.apache-httpcore-5.version>5.2.4</project.apache-httpcore-5.version>
     <project.opencensus.version>0.31.1</project.opencensus.version>
     <project.root-directory>..</project.root-directory>
     <project.surefire.version>3.2.5</project.surefire.version>
diff --git a/versions.txt b/versions.txt
index 963efeb8..87dbbb66 100644
--- a/versions.txt
+++ b/versions.txt
@@ -7,6 +7,7 @@ google-http-client-parent:1.44.2:1.44.3-SNAPSHOT
 google-http-client-android:1.44.2:1.44.3-SNAPSHOT
 google-http-client-android-test:1.44.2:1.44.3-SNAPSHOT
 google-http-client-apache-v2:1.44.2:1.44.3-SNAPSHOT
+google-http-client-apache-v5:1.44.2:1.44.3-SNAPSHOT
 google-http-client-appengine:1.44.2:1.44.3-SNAPSHOT
 google-http-client-assembly:1.44.2:1.44.3-SNAPSHOT
 google-http-client-findbugs:1.44.2:1.44.3-SNAPSHOT


Test patch:
diff --git a/google-http-client-apache-v5/src/test/java/com/google/api/client/http/apache/v5/Apache5HttpRequestTest.java b/google-http-client-apache-v5/src/test/java/com/google/api/client/http/apache/v5/Apache5HttpRequestTest.java
new file mode 100644
index 00000000..3b7ca4a2
--- /dev/null
+++ b/google-http-client-apache-v5/src/test/java/com/google/api/client/http/apache/v5/Apache5HttpRequestTest.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2019 Google LLC
+ *
+ * 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
+ *
+ * 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 com.google.api.client.http.apache.v5;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import com.google.api.client.http.ByteArrayContent;
+import com.google.api.client.http.HttpContent;
+import com.google.api.client.http.InputStreamContent;
+import com.google.api.client.http.LowLevelHttpResponse;
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.apache.hc.client5.http.classic.methods.HttpPost;
+import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase;
+import org.apache.hc.core5.http.ClassicHttpRequest;
+import org.apache.hc.core5.http.ClassicHttpResponse;
+import org.apache.hc.core5.http.ContentType;
+import org.apache.hc.core5.http.HttpEntity;
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.io.entity.BasicHttpEntity;
+import org.apache.hc.core5.http.protocol.HttpContext;
+import org.junit.Test;
+
+public class Apache5HttpRequestTest {
+  @Test
+  public void testContentLengthSet() throws Exception {
+    HttpUriRequestBase base = new HttpPost("http://www.google.com");
+    Apache5HttpRequest request =
+        new Apache5HttpRequest(
+            new MockHttpClient() {
+              @Override
+              public ClassicHttpResponse executeOpen(
+                  HttpHost target, ClassicHttpRequest request, HttpContext context) {
+                return new MockClassicHttpResponse();
+              }
+            },
+            base);
+    HttpContent content =
+        new ByteArrayContent("text/plain", "sample".getBytes(StandardCharsets.UTF_8));
+    request.setStreamingContent(content);
+    request.setContentLength(content.getLength());
+    request.execute();
+
+    assertFalse(base.getEntity().isChunked());
+    assertEquals(6, base.getEntity().getContentLength());
+  }
+
+  @Test
+  public void testChunked() throws Exception {
+    byte[] buf = new byte[300];
+    Arrays.fill(buf, (byte) ' ');
+    HttpUriRequestBase base = new HttpPost("http://www.google.com");
+    Apache5HttpRequest request =
+        new Apache5HttpRequest(
+            new MockHttpClient() {
+              @Override
+              public ClassicHttpResponse executeOpen(
+                  HttpHost target, ClassicHttpRequest request, HttpContext context) {
+                return new MockClassicHttpResponse();
+              }
+            },
+            base);
+    HttpContent content = new InputStreamContent("text/plain", new ByteArrayInputStream(buf));
+    request.setStreamingContent(content);
+    request.execute();
+
+    assertTrue(base.getEntity().isChunked());
+    assertEquals(-1, base.getEntity().getContentLength());
+  }
+
+  @Test
+  public void testExecute_closeContent_closesResponse() throws Exception {
+    HttpUriRequestBase base = new HttpPost("http://www.google.com");
+    final InputStream responseContentStream = new ByteArrayInputStream(new byte[] {1, 2, 3});
+    BasicHttpEntity testEntity =
+        new BasicHttpEntity(responseContentStream, ContentType.DEFAULT_BINARY);
+    AtomicInteger closedResponseCounter = new AtomicInteger(0);
+    ClassicHttpResponse classicResponse =
+        new MockClassicHttpResponse() {
+          @Override
+          public HttpEntity getEntity() {
+            return testEntity;
+          }
+
+          @Override
+          public void close() {
+            closedResponseCounter.incrementAndGet();
+          }
+        };
+
+    Apache5HttpRequest request =
+        new Apache5HttpRequest(
+            new MockHttpClient() {
+              @Override
+              public ClassicHttpResponse executeOpen(
+                  HttpHost target, ClassicHttpRequest request, HttpContext context) {
+                return classicResponse;
+              }
+            },
+            base);
+    LowLevelHttpResponse response = request.execute();
+    assertTrue(response instanceof Apache5HttpResponse);
+
+    // we confirm that the classic response we prepared in this test is the same as the content's
+    // response
+    assertTrue(response.getContent() instanceof Apache5ResponseContent);
+    assertEquals(classicResponse, ((Apache5ResponseContent) response.getContent()).getResponse());
+
+    // we close the response's content stream and confirm the response is also closed
+    assertEquals(0, closedResponseCounter.get());
+    response.getContent().close();
+    assertEquals(1, closedResponseCounter.get());
+  }
+}
diff --git a/google-http-client-apache-v5/src/test/java/com/google/api/client/http/apache/v5/Apache5HttpTransportTest.java b/google-http-client-apache-v5/src/test/java/com/google/api/client/http/apache/v5/Apache5HttpTransportTest.java
new file mode 100644
index 00000000..99045d99
--- /dev/null
+++ b/google-http-client-apache-v5/src/test/java/com/google/api/client/http/apache/v5/Apache5HttpTransportTest.java
@@ -0,0 +1,353 @@
+/*
+ * Copyright 2019 Google LLC
+ *
+ * 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
+ *
+ * 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 com.google.api.client.http.apache.v5;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
+
+import com.google.api.client.http.GenericUrl;
+import com.google.api.client.http.HttpResponseException;
+import com.google.api.client.http.HttpTransport;
+import com.google.api.client.http.LowLevelHttpResponse;
+import com.google.api.client.util.ByteArrayStreamingContent;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.apache.hc.client5.http.ConnectTimeoutException;
+import org.apache.hc.client5.http.HttpHostConnectException;
+import org.apache.hc.client5.http.classic.HttpClient;
+import org.apache.hc.client5.http.impl.classic.HttpClients;
+import org.apache.hc.core5.http.ClassicHttpRequest;
+import org.apache.hc.core5.http.ClassicHttpResponse;
+import org.apache.hc.core5.http.ContentType;
+import org.apache.hc.core5.http.EntityDetails;
+import org.apache.hc.core5.http.Header;
+import org.apache.hc.core5.http.HttpException;
+import org.apache.hc.core5.http.HttpHeaders;
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.HttpRequest;
+import org.apache.hc.core5.http.HttpRequestInterceptor;
+import org.apache.hc.core5.http.HttpRequestMapper;
+import org.apache.hc.core5.http.HttpResponse;
+import org.apache.hc.core5.http.HttpStatus;
+import org.apache.hc.core5.http.impl.bootstrap.HttpServer;
+import org.apache.hc.core5.http.impl.io.HttpRequestExecutor;
+import org.apache.hc.core5.http.impl.io.HttpService;
+import org.apache.hc.core5.http.io.HttpClientConnection;
+import org.apache.hc.core5.http.io.HttpRequestHandler;
+import org.apache.hc.core5.http.io.entity.ByteArrayEntity;
+import org.apache.hc.core5.http.io.support.BasicHttpServerRequestHandler;
+import org.apache.hc.core5.http.protocol.HttpContext;
+import org.apache.hc.core5.http.protocol.HttpProcessor;
+import org.junit.Assert;
+import org.junit.Test;
+
+/** Tests {@link Apache5HttpTransport}. */
+public class Apache5HttpTransportTest {
+
+  @Test
+  public void testApacheHttpTransport() {
+    Apache5HttpTransport transport = new Apache5HttpTransport();
+    checkHttpTransport(transport);
+    assertFalse(transport.isMtls());
+  }
+
+  @Test
+  public void testApacheHttpTransportWithParam() {
+    Apache5HttpTransport transport = new Apache5HttpTransport(HttpClients.custom().build(), true);
+    checkHttpTransport(transport);
+    assertTrue(transport.isMtls());
+  }
+
+  @Test
+  public void testNewDefaultHttpClient() {
+    HttpClient client = Apache5HttpTransport.newDefaultHttpClient();
+    checkHttpClient(client);
+  }
+
+  private void checkHttpTransport(Apache5HttpTransport transport) {
+    assertNotNull(transport);
+    HttpClient client = transport.getHttpClient();
+    checkHttpClient(client);
+  }
+
+  private void checkHttpClient(HttpClient client) {
+    assertNotNull(client);
+    // TODO(chingor): Is it possible to test this effectively? The newer HttpClient implementations
+    // are read-only and we're testing that we built the client with the right configuration
+  }
+
+  @Test
+  public void testRequestsWithContent() throws IOException {
+    // This test confirms that we can set the content on any type of request
+    HttpClient mockClient =
+        new MockHttpClient() {
+          @Override
+          public ClassicHttpResponse executeOpen(
+              HttpHost target, ClassicHttpRequest request, HttpContext context) {
+            return new MockClassicHttpResponse();
+          }
+        };
+    Apache5HttpTransport transport = new Apache5HttpTransport(mockClient);
+
+    // Test GET.
+    execute(transport.buildRequest("GET", "http://www.test.url"));
+    // Test DELETE.
+    execute(transport.buildRequest("DELETE", "http://www.test.url"));
+    // Test HEAD.
+    execute(transport.buildRequest("HEAD", "http://www.test.url"));
+
+    // Test PATCH.
+    execute(transport.buildRequest("PATCH", "http://www.test.url"));
+    // Test PUT.
+    execute(transport.buildRequest("PUT", "http://www.test.url"));
+    // Test POST.
+    execute(transport.buildRequest("POST", "http://www.test.url"));
+    // Test PATCH.
+    execute(transport.buildRequest("PATCH", "http://www.test.url"));
+  }
+
+  private void execute(Apache5HttpRequest request) throws IOException {
+    byte[] bytes = "abc".getBytes(StandardCharsets.UTF_8);
+    request.setStreamingContent(new ByteArrayStreamingContent(bytes));
+    request.setContentType("text/html");
+    request.setContentLength(bytes.length);
+    request.execute();
+  }
+
+  @Test
+  public void testRequestShouldNotFollowRedirects() throws IOException {
+    final AtomicInteger requestsAttempted = new AtomicInteger(0);
+    HttpRequestExecutor requestExecutor =
+        new HttpRequestExecutor() {
+          @Override
+          public ClassicHttpResponse execute(
+              ClassicHttpRequest request, HttpClientConnection connection, HttpContext context)
+              throws IOException, HttpException {
+            ClassicHttpResponse response = new MockClassicHttpResponse();
+            response.setCode(302);
+            response.setReasonPhrase(null);
+            response.addHeader("location", "https://google.com/path");
+            response.addHeader(HttpHeaders.SET_COOKIE, "");
+            requestsAttempted.incrementAndGet();
+            return response;
+          }
+        };
+    HttpClient client = HttpClients.custom().setRequestExecutor(requestExecutor).build();
+    Apache5HttpTransport transport = new Apache5HttpTransport(client);
+    Apache5HttpRequest request = transport.buildRequest("GET", "https://google.com");
+    LowLevelHttpResponse response = request.execute();
+    assertEquals(1, requestsAttempted.get());
+    assertEquals(302, response.getStatusCode());
+  }
+
+  @Test
+  public void testRequestCanSetHeaders() {
+    final AtomicBoolean interceptorCalled = new AtomicBoolean(false);
+    HttpClient client =
+        HttpClients.custom()
+            .addRequestInterceptorFirst(
+                new HttpRequestInterceptor() {
+                  @Override
+                  public void process(
+                      HttpRequest request, EntityDetails details, HttpContext context)
+                      throws HttpException, IOException {
+                    Header header = request.getFirstHeader("foo");
+                    assertNotNull("Should have found header", header);
+                    assertEquals("bar", header.getValue());
+                    interceptorCalled.set(true);
+                    throw new IOException("cancelling request");
+                  }
+                })
+            .build();
+
+    Apache5HttpTransport transport = new Apache5HttpTransport(client);
+    Apache5HttpRequest request = transport.buildRequest("GET", "https://google.com");
+    request.addHeader("foo", "bar");
+    try {
+      LowLevelHttpResponse response = request.execute();
+      fail("should not actually make the request");
+    } catch (IOException exception) {
+      assertEquals("cancelling request", exception.getMessage());
+    }
+    assertTrue("Expected to have called our test interceptor", interceptorCalled.get());
+  }
+
+  @Test(timeout = 10_000L)
+  public void testConnectTimeout() {
+    // TODO(chanseok): Java 17 returns an IOException (SocketException: Network is unreachable).
+    // Figure out a way to verify connection timeout works on Java 17+.
+    assumeTrue(System.getProperty("java.version").compareTo("17") < 0);
+
+    HttpTransport httpTransport = new Apache5HttpTransport();
+    GenericUrl url = new GenericUrl("http://google.com:81");
+    try {
+      httpTransport.createRequestFactory().buildGetRequest(url).setConnectTimeout(100).execute();
+      fail("should have thrown an exception");
+    } catch (HttpHostConnectException | ConnectTimeoutException expected) {
+      // expected
+    } catch (IOException e) {
+      fail("unexpected IOException: " + e.getClass().getName() + ": " + e.getMessage());
+    }
+  }
+
+  private static class FakeServer implements AutoCloseable {
+    private final HttpServer server;
+
+    FakeServer(final HttpRequestHandler httpHandler) throws IOException {
+      HttpRequestMapper<HttpRequestHandler> mapper =
+          new HttpRequestMapper<HttpRequestHandler>() {
+            @Override
+            public HttpRequestHandler resolve(HttpRequest request, HttpContext context)
+                throws HttpException {
+              return httpHandler;
+            };
+          };
+      server =
+          new HttpServer(
+              0,
+              HttpService.builder()
+                  .withHttpProcessor(
+                      new HttpProcessor() {
+                        @Override
+                        public void process(
+                            HttpRequest request, EntityDetails entity, HttpContext context)
+                            throws HttpException, IOException {}
+
+                        @Override
+                        public void process(
+                            HttpResponse response, EntityDetails entity, HttpContext context)
+                            throws HttpException, IOException {}
+                      })
+                  .withHttpServerRequestHandler(new BasicHttpServerRequestHandler(mapper))
+                  .build(),
+              null,
+              null,
+              null,
+              null,
+              null,
+              null);
+      //       server.createContext("/", httpHandler);
+      server.start();
+    }
+
+    public int getPort() {
+      return server.getLocalPort();
+    }
+
+    @Override
+    public void close() {
+      server.initiateShutdown();
+    }
+  }
+
+  @Test
+  public void testNormalizedUrl() throws IOException {
+    final HttpRequestHandler handler =
+        new HttpRequestHandler() {
+          @Override
+          public void handle(
+              ClassicHttpRequest request, ClassicHttpResponse response, HttpContext context)
+              throws HttpException, IOException {
+            // Extract the request URI and convert to bytes
+            byte[] responseData = request.getRequestUri().getBytes(StandardCharsets.UTF_8);
+
+            // Set the response headers (status code and content length)
+            response.setCode(HttpStatus.SC_OK);
+            response.setHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(responseData.length));
+
+            // Set the response entity (body)
+            ByteArrayEntity entity = new ByteArrayEntity(responseData, ContentType.TEXT_PLAIN);
+            response.setEntity(entity);
+          }
+        };
+    try (FakeServer server = new FakeServer(handler)) {
+      HttpTransport transport = new Apache5HttpTransport();
+      GenericUrl testUrl = new GenericUrl("http://localhost/foo//bar");
+      testUrl.setPort(server.getPort());
+      com.google.api.client.http.HttpResponse response =
+          transport.createRequestFactory().buildGetRequest(testUrl).execute();
+      assertEquals(200, response.getStatusCode());
+      assertEquals("/foo//bar", response.parseAsString());
+    }
+  }
+
+  @Test
+  public void testReadErrorStream() throws IOException {
+    final HttpRequestHandler handler =
+        new HttpRequestHandler() {
+          @Override
+          public void handle(
+              ClassicHttpRequest request, ClassicHttpResponse response, HttpContext context)
+              throws HttpException, IOException {
+            byte[] responseData = "Forbidden".getBytes(StandardCharsets.UTF_8);
+            response.setCode(HttpStatus.SC_FORBIDDEN); // 403 Forbidden
+            response.setHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(responseData.length));
+            ByteArrayEntity entity = new ByteArrayEntity(responseData, ContentType.TEXT_PLAIN);
+            response.setEntity(entity);
+          }
+        };
+    try (FakeServer server = new FakeServer(handler)) {
+      HttpTransport transport = new Apache5HttpTransport();
+      GenericUrl testUrl = new GenericUrl("http://localhost/foo//bar");
+      testUrl.setPort(server.getPort());
+      com.google.api.client.http.HttpRequest getRequest =
+          transport.createRequestFactory().buildGetRequest(testUrl);
+      getRequest.setThrowExceptionOnExecuteError(false);
+      com.google.api.client.http.HttpResponse response = getRequest.execute();
+      assertEquals(403, response.getStatusCode());
+      assertEquals("Forbidden", response.parseAsString());
+    }
+  }
+
+  @Test
+  public void testReadErrorStream_withException() throws IOException {
+    final HttpRequestHandler handler =
+        new HttpRequestHandler() {
+          @Override
+          public void handle(
+              ClassicHttpRequest request, ClassicHttpResponse response, HttpContext context)
+              throws HttpException, IOException {
+            byte[] responseData = "Forbidden".getBytes(StandardCharsets.UTF_8);
+            response.setCode(HttpStatus.SC_FORBIDDEN); // 403 Forbidden
+            response.setHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(responseData.length));
+            ByteArrayEntity entity = new ByteArrayEntity(responseData, ContentType.TEXT_PLAIN);
+            response.setEntity(entity);
+          }
+        };
+    try (FakeServer server = new FakeServer(handler)) {
+      HttpTransport transport = new Apache5HttpTransport();
+      GenericUrl testUrl = new GenericUrl("http://localhost/foo//bar");
+      testUrl.setPort(server.getPort());
+      com.google.api.client.http.HttpRequest getRequest =
+          transport.createRequestFactory().buildGetRequest(testUrl);
+      try {
+        getRequest.execute();
+        Assert.fail();
+      } catch (HttpResponseException ex) {
+        assertEquals("Forbidden", ex.getContent());
+      }
+    }
+  }
+
+  private boolean isWindows() {
+    return System.getProperty("os.name").startsWith("Windows");
+  }
+}
diff --git a/google-http-client-apache-v5/src/test/java/com/google/api/client/http/apache/v5/MockClassicHttpResponse.java b/google-http-client-apache-v5/src/test/java/com/google/api/client/http/apache/v5/MockClassicHttpResponse.java
new file mode 100644
index 00000000..09172174
--- /dev/null
+++ b/google-http-client-apache-v5/src/test/java/com/google/api/client/http/apache/v5/MockClassicHttpResponse.java
@@ -0,0 +1,182 @@
+package com.google.api.client.http.apache.v5;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.stream.Collectors;
+import org.apache.hc.core5.http.ClassicHttpResponse;
+import org.apache.hc.core5.http.Header;
+import org.apache.hc.core5.http.HttpEntity;
+import org.apache.hc.core5.http.HttpVersion;
+import org.apache.hc.core5.http.ProtocolException;
+import org.apache.hc.core5.http.ProtocolVersion;
+
+public class MockClassicHttpResponse implements ClassicHttpResponse {
+  List<Header> headers = new ArrayList<>();
+  int code = 200;
+
+  @Override
+  public int getCode() {
+    return code;
+  }
+
+  @Override
+  public void setCode(int code) {
+    this.code = code;
+  }
+
+  @Override
+  public String getReasonPhrase() {
+    return null;
+  }
+
+  @Override
+  public void setReasonPhrase(String reason) {}
+
+  @Override
+  public Locale getLocale() {
+    return null;
+  }
+
+  @Override
+  public void setLocale(Locale loc) {}
+
+  @Override
+  public void setVersion(ProtocolVersion version) {}
+
+  @Override
+  public ProtocolVersion getVersion() {
+    return HttpVersion.HTTP_1_1;
+  }
+
+  @Override
+  public void addHeader(Header header) {
+    headers.add(header);
+  }
+
+  @Override
+  public void addHeader(String name, Object value) {
+    addHeader(newHeader(name, value));
+  }
+
+  private Header newHeader(String key, Object value) {
+    return new Header() {
+      @Override
+      public boolean isSensitive() {
+        return false;
+      }
+
+      @Override
+      public String getName() {
+        return key;
+      }
+
+      @Override
+      public String getValue() {
+        return value.toString();
+      }
+    };
+  }
+
+  @Override
+  public void setHeader(Header header) {
+    if (headers.contains(header)) {
+      int index = headers.indexOf(header);
+      headers.set(index, header);
+    } else {
+      addHeader(header);
+    }
+  }
+
+  @Override
+  public void setHeader(String name, Object value) {
+    setHeader(newHeader(name, value));
+  }
+
+  @Override
+  public void setHeaders(Header... headers) {
+    for (Header header : headers) {
+      setHeader(header);
+    }
+  }
+
+  @Override
+  public boolean removeHeader(Header header) {
+    if (headers.contains(header)) {
+      headers.remove(headers.indexOf(header));
+      return true;
+    }
+    return false;
+  }
+
+  @Override
+  public boolean removeHeaders(String name) {
+    int initialSize = headers.size();
+    for (Header header :
+        headers.stream().filter(h -> h.getName() == name).collect(Collectors.toList())) {
+      removeHeader(header);
+    }
+    return headers.size() < initialSize;
+  }
+
+  @Override
+  public boolean containsHeader(String name) {
+    return headers.stream().anyMatch(h -> h.getName() == name);
+  }
+
+  @Override
+  public int countHeaders(String name) {
+    return headers.size();
+  }
+
+  @Override
+  public Header getFirstHeader(String name) {
+    return headers.stream().findFirst().orElse(null);
+  }
+
+  @Override
+  public Header getHeader(String name) throws ProtocolException {
+    return headers.stream().filter(h -> h.getName() == name).findFirst().orElse(null);
+  }
+
+  @Override
+  public Header[] getHeaders() {
+    return headers.toArray(new Header[0]);
+  }
+
+  @Override
+  public Header[] getHeaders(String name) {
+    return headers.stream()
+        .filter(h -> h.getName() == name)
+        .collect(Collectors.toList())
+        .toArray(new Header[0]);
+  }
+
+  @Override
+  public Header getLastHeader(String name) {
+    return headers.isEmpty() ? null : headers.get(headers.size() - 1);
+  }
+
+  @Override
+  public Iterator<Header> headerIterator() {
+    return headers.iterator();
+  }
+
+  @Override
+  public Iterator<Header> headerIterator(String name) {
+    return headers.stream().filter(h -> h.getName() == name).iterator();
+  }
+
+  @Override
+  public void close() throws IOException {}
+
+  @Override
+  public HttpEntity getEntity() {
+    return null;
+  }
+
+  @Override
+  public void setEntity(HttpEntity entity) {}
+}
diff --git a/google-http-client-apache-v5/src/test/java/com/google/api/client/http/apache/v5/MockHttpClient.java b/google-http-client-apache-v5/src/test/java/com/google/api/client/http/apache/v5/MockHttpClient.java
new file mode 100644
index 00000000..8d26096c
--- /dev/null
+++ b/google-http-client-apache-v5/src/test/java/com/google/api/client/http/apache/v5/MockHttpClient.java
@@ -0,0 +1,86 @@
+package com.google.api.client.http.apache.v5;
+
+import com.google.api.client.util.Preconditions;
+import java.io.IOException;
+import org.apache.hc.client5.http.classic.HttpClient;
+import org.apache.hc.core5.http.ClassicHttpRequest;
+import org.apache.hc.core5.http.ClassicHttpResponse;
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.HttpResponse;
+import org.apache.hc.core5.http.io.HttpClientResponseHandler;
+import org.apache.hc.core5.http.protocol.HttpContext;
+
+public class MockHttpClient implements HttpClient {
+
+  /** HTTP response code to use. */
+  int responseCode;
+
+  /** Returns the HTTP response code to use. */
+  public final int getResponseCode() {
+    return responseCode;
+  }
+
+  /** Sets the HTTP response code to use. */
+  public MockHttpClient setResponseCode(int responseCode) {
+    Preconditions.checkArgument(responseCode >= 0);
+    this.responseCode = responseCode;
+    return this;
+  }
+
+  @Override
+  public HttpResponse execute(ClassicHttpRequest request) throws IOException {
+    return null;
+  }
+
+  @Override
+  public HttpResponse execute(ClassicHttpRequest request, HttpContext context) throws IOException {
+    return null;
+  }
+
+  @Override
+  public ClassicHttpResponse execute(HttpHost target, ClassicHttpRequest request)
+      throws IOException {
+    return null;
+  }
+
+  @Override
+  public HttpResponse execute(HttpHost target, ClassicHttpRequest request, HttpContext context)
+      throws IOException {
+    return null;
+  }
+
+  @Override
+  public <T> T execute(
+      ClassicHttpRequest request, HttpClientResponseHandler<? extends T> responseHandler)
+      throws IOException {
+    return null;
+  }
+
+  @Override
+  public <T> T execute(
+      ClassicHttpRequest request,
+      HttpContext context,
+      HttpClientResponseHandler<? extends T> responseHandler)
+      throws IOException {
+    return null;
+  }
+
+  @Override
+  public <T> T execute(
+      HttpHost target,
+      ClassicHttpRequest request,
+      HttpClientResponseHandler<? extends T> responseHandler)
+      throws IOException {
+    return null;
+  }
+
+  @Override
+  public <T> T execute(
+      HttpHost target,
+      ClassicHttpRequest request,
+      HttpContext context,
+      HttpClientResponseHandler<? extends T> responseHandler)
+      throws IOException {
+    return null;
+  }
+}