Repository: googleapis/google-api-java-client (java)

Issue:
AbstractGoogleClientRequest.initializeMediaUpload is not using applicationName to User-Agent HTTP header
Internal bug b/252963877. mediaHttpUploader is not adding a proper User-Agent HTTP header.

```
        String userAgent = "Foo/1.0";
        Storage storage = new Storage.Builder(rf.getTransport(), jsonFactory, rf.getInitializer())
            .setApplicationName(userAgent)
            .build();
...
        Insert insert = storage.objects().insert(bucket, storageObject, content);
        GenericUrl url = insert.buildHttpRequestUrl();
        MediaHttpUploader mediaHttpUploader = insert.getMediaHttpUploader();

        // This does not add the expected user-agent header to the request.
        HttpResponse upload = mediaHttpUploader.upload(url);
```

#### Environment details

1. Specify the API at the beginning of the title. For example, "BigQuery: ...").
   General, Core, and Other are also allowed as types
2. OS type and version:
3. Java version:
4.  version(s):

The `com.google.apis:google-api-services-storage:v1-rev20220705-2.0.0` and `google-api-client:2.0.0`.


#### Steps to reproduce

Declare dependencies:

```
    <dependency>
      <groupId>com.google.apis</groupId>
      <artifactId>google-api-services-storage</artifactId>
      <version>v1-rev20220705-2.0.0</version>
    </dependency>
```



#### Code example

```java
    // Run with VM option to have -Djava.util.logging.config.file=src/main/resources/logging.properties
    public static void main( String[] args ) throws Exception {
        NetHttpTransport transport = new NetHttpTransport.Builder()
            .trustCertificates(GoogleUtils.getCertificateTrustStore())
            .build();
        HttpRequestFactory rf = transport.createRequestFactory();
        JsonFactory jsonFactory = new GsonFactory();

        String userAgent = "Foo/1.0";
        Storage storage = new Storage.Builder(rf.getTransport(), jsonFactory, rf.getInitializer())
            .setApplicationName(userAgent)
            .build();


        // Project suztomo-terraform-4f9e48 has this bucket.
        String bucket = "suztomo_test_b252963877"; // TODO: replace with your bucket

        StorageObject storageObject = new StorageObject().setName("temp.txt").setBucket(bucket);
        byte[] bytes = "hi".getBytes(StandardCharsets.UTF_8);
        ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
        InputStreamContent content = new InputStreamContent("text/plain", bais);

        Insert insert = storage.objects().insert(bucket, storageObject, content);
        GenericUrl url = insert.buildHttpRequestUrl();
        MediaHttpUploader mediaHttpUploader = insert.getMediaHttpUploader();

        // This does not add the expected user-agent header to the request.
        HttpResponse upload = mediaHttpUploader.upload(url);
        System.out.println("Upload response code: " + upload.getStatusCode() + " " + upload.getStatusMessage());
    }
```

This does not send the user agent to the server.


#### External references such as API reference guides

- ?

#### Any additional information below

Internal bug b/252963877.

Setting request initializer as the 3rd argument to `Storage.Builder` is the workaround:

```
        HttpCredentialsAdapter credentialsInitializer = new HttpCredentialsAdapter(credentials);
        HttpRequestInitializer requestInitializer = new HttpRequestInitializer() {
            @Override
            public void initialize(HttpRequest request) throws IOException {
                credentialsInitializer.initialize(request);
                HttpHeaders headers = request.getHeaders();
                headers.setUserAgent(userAgent);
            }
        };

...(omit)...

        Storage storage = new Storage.Builder(rf.getTransport(), jsonFactory, requestInitializer)
            .setApplicationName(userAgent)
            .build();
```

But the library users shouldn't need to do that.



Pull request description:
fix: media upload to have applicationName as User-Agent
Fixes #2222 

- Upload to have applicationName as User-Agent
- request initializer to set user-agent header

How I confirmed the fix:

I locally installed this branch via `mvn install` and specified the SNAPSHOT version of the BOM:

```xml
  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>com.google.api-client</groupId>
        <artifactId>google-api-client-bom</artifactId>
        <version>2.1.3-SNAPSHOT</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>

  <dependencies>
    <dependency>
      <groupId>com.google.apis</groupId>
      <artifactId>google-api-services-storage</artifactId>
      <version>v1-rev20220705-2.0.0</version>
    </dependency>
...
```

I ran @BenWhitehead 's reproducer (with my small modification):

```java
package org.example;

import static sun.net.www.protocol.http.HttpURLConnection.userAgent;

import com.google.api.client.googleapis.GoogleUtils;
import com.google.api.client.googleapis.media.MediaHttpUploader;
import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.HttpHeaders;
import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.HttpRequestFactory;
import com.google.api.client.http.HttpRequestInitializer;
import com.google.api.client.http.HttpResponse;
import com.google.api.client.http.InputStreamContent;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.gson.GsonFactory;
import com.google.api.services.storage.Storage;
import com.google.api.services.storage.Storage.Objects.Insert;
import com.google.api.services.storage.model.StorageObject;
import com.google.auth.http.HttpCredentialsAdapter;
import com.google.auth.oauth2.GoogleCredentials;
import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;

public class App 
{
    // Run with VM option to have -Djava.util.logging.config.file=src/main/resources/logging.properties
    public static void main( String[] args ) throws Exception {
        GoogleCredentials credentials = GoogleCredentials.getApplicationDefault();
        String userAgent = "Foo/1.0 (Bar:Baz/1.0) Tomo/1.0";
        HttpCredentialsAdapter credentialsInitializer = new HttpCredentialsAdapter(credentials);

        NetHttpTransport transport = new NetHttpTransport.Builder()
            .trustCertificates(GoogleUtils.getCertificateTrustStore())
            .build();
        HttpRequestFactory rf = transport.createRequestFactory();
        JsonFactory jsonFactory = new GsonFactory();

        Storage storage = new Storage.Builder(rf.getTransport(), jsonFactory, credentialsInitializer)
            .setApplicationName(userAgent)
            .build();


        // Project suztomo-terraform-4f9e48 has this bucket.
        String bucket = "suztomo_test_b252963877"; // TODO: replace with your bucket

        StorageObject storageObject = new StorageObject().setName("temp.txt").setBucket(bucket);
        byte[] bytes = "hi".getBytes(StandardCharsets.UTF_8);
        ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
        InputStreamContent content = new InputStreamContent("text/plain", bais);

        Insert insert = storage.objects().insert(bucket, storageObject, content);
        GenericUrl url = insert.buildHttpRequestUrl();
        MediaHttpUploader mediaHttpUploader = insert.getMediaHttpUploader();

        HttpResponse upload = mediaHttpUploader.upload(url);
        System.out.println("Upload response code: " + upload.getStatusCode() + " " + upload.getStatusMessage());
    }
}
```

 Note that userAgent is used in only `setApplicationName(userAgent)`.

I placed src/main/resources/logging.properties so that it prints debug (FINE-level) log:

```
handlers = java.util.logging.ConsoleHandler
java.util.logging.ConsoleHandler.level = FINE

# Set up logging of HTTP requests and responses (uncomment "level" to show)
com.google.api.level = FINE
```

This is specified via `-Djava.util.logging.config.file=src/main/resources/logging.properties` argument in IntelliJ.

It printed out the expected User-Agent when uploading the data:

```
...
Jan 20, 2023 10:59:01 AM com.google.api.client.http.HttpRequest execute
CONFIG: -------------- REQUEST  --------------
PUT https://storage.googleapis.com/upload/storage/v1/b/suztomo_test_b252963877/o?uploadType=resumable&upload_id=ADPycdstG5t6ppvQpO9MoZROy-k67Qonkt_EFy8fUv5NQOW6hODYzBs8T3iEBy3UD9qy4xz25pnrOc41j_te7WPdu4ZG
Accept-Encoding: gzip
Authorization: <Not Logged>
Content-Range: bytes 0-1/2
User-Agent: Foo/1.0 (Bar:Baz/1.0) Tomo/1.0 Google-HTTP-Java-Client/1.42.3 (gzip)
x-goog-user-project: suztomo-terraform-4f9e48
...
```



Patch:
diff --git a/google-api-client/src/main/java/com/google/api/client/googleapis/services/AbstractGoogleClientRequest.java b/google-api-client/src/main/java/com/google/api/client/googleapis/services/AbstractGoogleClientRequest.java
index 3b8d3cc3..cd6d12b6 100644
--- a/google-api-client/src/main/java/com/google/api/client/googleapis/services/AbstractGoogleClientRequest.java
+++ b/google-api-client/src/main/java/com/google/api/client/googleapis/services/AbstractGoogleClientRequest.java
@@ -351,15 +351,42 @@ public abstract class AbstractGoogleClientRequest<T> extends GenericData {
    */
   protected final void initializeMediaUpload(AbstractInputStreamContent mediaContent) {
     HttpRequestFactory requestFactory = abstractGoogleClient.getRequestFactory();
+    String applicationName = abstractGoogleClient.getApplicationName();
+    HttpRequestInitializer requestInitializer =
+        mediaUploadRequestUserAgentInitializer(applicationName, requestFactory.getInitializer());
     this.uploader =
-        new MediaHttpUploader(
-            mediaContent, requestFactory.getTransport(), requestFactory.getInitializer());
+        new MediaHttpUploader(mediaContent, requestFactory.getTransport(), requestInitializer);
     this.uploader.setInitiationRequestMethod(requestMethod);
     if (httpContent != null) {
       this.uploader.setMetadata(httpContent);
     }
   }
 
+  private static HttpRequestInitializer mediaUploadRequestUserAgentInitializer(
+      final String applicationName, final HttpRequestInitializer originalInitializer) {
+    if (applicationName == null) {
+      return originalInitializer;
+    }
+    if (originalInitializer == null) {
+      return new HttpRequestInitializer() {
+        @Override
+        public void initialize(HttpRequest request) {
+          HttpHeaders headers = request.getHeaders();
+          headers.setUserAgent(applicationName);
+        }
+      };
+    } else {
+      return new HttpRequestInitializer() {
+        @Override
+        public void initialize(HttpRequest request) throws IOException {
+          originalInitializer.initialize(request);
+          HttpHeaders headers = request.getHeaders();
+          headers.setUserAgent(applicationName);
+        }
+      };
+    }
+  }
+
   /** Returns the media HTTP downloader or {@code null} for none. */
   public final MediaHttpDownloader getMediaHttpDownloader() {
     return downloader;


Test patch:
diff --git a/google-api-client/src/test/java/com/google/api/client/googleapis/services/AbstractGoogleClientTest.java b/google-api-client/src/test/java/com/google/api/client/googleapis/services/AbstractGoogleClientTest.java
index 2164af79..7d2ff638 100644
--- a/google-api-client/src/test/java/com/google/api/client/googleapis/services/AbstractGoogleClientTest.java
+++ b/google-api-client/src/test/java/com/google/api/client/googleapis/services/AbstractGoogleClientTest.java
@@ -16,6 +16,7 @@ import com.google.api.client.googleapis.media.MediaHttpUploader;
 import com.google.api.client.googleapis.testing.services.MockGoogleClient;
 import com.google.api.client.googleapis.testing.services.MockGoogleClientRequest;
 import com.google.api.client.http.EmptyContent;
+import com.google.api.client.http.GenericUrl;
 import com.google.api.client.http.HttpExecuteInterceptor;
 import com.google.api.client.http.HttpRequest;
 import com.google.api.client.http.HttpRequestInitializer;
@@ -33,6 +34,8 @@ import com.google.api.client.testing.http.MockLowLevelHttpResponse;
 import com.google.api.client.util.Key;
 import java.io.ByteArrayInputStream;
 import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
 import junit.framework.TestCase;
 
 /**
@@ -132,6 +135,7 @@ public class AbstractGoogleClientTest extends TestCase {
     int bytesUploaded;
     int contentLength = MediaHttpUploader.DEFAULT_CHUNK_SIZE;
     boolean contentLengthNotSpecified;
+    List<String> userAgentsRecorded = new ArrayList<>();
 
     protected MediaTransport() {}
 
@@ -169,6 +173,7 @@ public class AbstractGoogleClientTest extends TestCase {
           String expectedContentRange = "bytes " + bytesRange + "/" + contentLength;
           assertEquals(expectedContentRange, getFirstHeaderValue("Content-Range"));
           bytesUploaded += MediaHttpUploader.DEFAULT_CHUNK_SIZE;
+          userAgentsRecorded.add(getFirstHeaderValue("User-Agent"));
 
           if (bytesUploaded == contentLength) {
             // Return 200 since the upload is complete.
@@ -207,6 +212,32 @@ public class AbstractGoogleClientTest extends TestCase {
     assertEquals("somevalue", result.foo);
   }
 
+  public void testMediaUpload_applicationNameAsUserAgent() throws Exception {
+    MediaTransport fakeTransport = new MediaTransport();
+    String applicationName = "Foo/1.0 (BAR:Baz/1.0) XYZ/1.0";
+    AbstractGoogleClient client =
+        new MockGoogleClient.Builder(
+                fakeTransport, TEST_RESUMABLE_REQUEST_URL, "", JSON_OBJECT_PARSER, null)
+            .setApplicationName(applicationName)
+            .build();
+    InputStream is = new ByteArrayInputStream(new byte[MediaHttpUploader.DEFAULT_CHUNK_SIZE]);
+    InputStreamContent mediaContent = new InputStreamContent(TEST_CONTENT_TYPE, is);
+    mediaContent.setLength(MediaHttpUploader.DEFAULT_CHUNK_SIZE);
+    MockGoogleClientRequest<A> rq =
+        new MockGoogleClientRequest<A>(client, "POST", "", null, A.class);
+
+    rq.initializeMediaUpload(mediaContent);
+    MediaHttpUploader mediaHttpUploader = rq.getMediaHttpUploader();
+    mediaHttpUploader.upload(new GenericUrl(TEST_RESUMABLE_REQUEST_URL));
+
+    assertEquals(1, fakeTransport.userAgentsRecorded.size());
+    for (String userAgent : fakeTransport.userAgentsRecorded) {
+      assertTrue(
+          "UserAgent header does not have expected value in requests",
+          userAgent.contains(applicationName));
+    }
+  }
+
   private static class GZipCheckerInitializer implements HttpRequestInitializer {
 
     private boolean gzipDisabled;