Repository: microsoft/kiota (csharp)

Issue:
Requests that redirect to download resources are not performed
### What are you generating using Kiota, clients or plugins?

API Client/SDK

### In what context or format are you using Kiota?

Nuget tool

### Client library/SDK language

Csharp

### Describe the bug

I am trying to download a tarball of a GitHub repo using a Kiota-generated SDK with Kiota version v1.14.0. 

Actual behavior: the request simply returns a `Task`, so I cannot get the content of the repo even though the request succeeds. 

### Expected behavior

I should be able to receive a `Task<byte[]>` or a `Task<Stream>` so that I may download the repository contents. 

### How to reproduce

Use [octokit/dotnet-sdk](https://github.com/octokit/dotnet-sdk) and edit the the example file `stage/dotnet/dotnet-sdk/cli/Authentication/PersonalAccessToken.cs` provided, or else construct and authenticate your own GitHub client from Kiota, and make the following request:

```csharp
// create client
var tokenProvider = new TokenProvider(Environment.GetEnvironmentVariable("GITHUB_TOKEN") ?? "");
var adapter = RequestAdapter.Create(new TokenAuthProvider(tokenProvider));
var gitHubClient = new GitHubClient(adapter);

// no return value
await gitHubClient.Repos["octokit"]["dotnet-sdk"].Tarball["main"].GetAsync();
```


### Open API description file

Full description: https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.json

Relevant content: 

```json
    "/repos/{owner}/{repo}/tarball/{ref}": {
      "get": {
        "summary": "Download a repository archive (tar)",
        "description": "Gets a redirect URL to download a tar archive for a repository. If you omit `:ref`, the repository’s default branch (usually\n`main`) will be used. Please make sure your HTTP framework is configured to follow redirects or you will need to use\nthe `Location` header to make a second `GET` request.\n\n> [!NOTE]\n> For private repositories, these links are temporary and expire after five minutes.",
        "tags": [
          "repos"
        ],
        "externalDocs": {
          "description": "API method documentation",
          "url": "https://docs.github.com/rest/repos/contents#download-a-repository-archive-tar"
        },
        "operationId": "repos/download-tarball-archive",
        "parameters": [
          {
            "$ref": "#/components/parameters/owner"
          },
          {
            "$ref": "#/components/parameters/repo"
          },
          {
            "name": "ref",
            "in": "path",
            "required": true,
            "x-multi-segment": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "302": {
            "description": "Response",
            "headers": {
              "Location": {
                "example": "https://codeload.github.com/me/myprivate/legacy.zip/master?login=me&token=thistokenexpires",
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "x-github": {
          "githubCloudOnly": false,
          "enabledForGitHubApps": true,
          "category": "repos",
          "subcategory": "contents"
        }
      }
    },
```


### Kiota Version

v1.14.0

### Latest Kiota version known to work for scenario above?(Not required)

_No response_

### Known Workarounds

_No response_

### Configuration

_No response_

### Debug output

_No response_

### Other information

As specified in https://github.com/microsoft/kiota/issues/2597#issuecomment-1625603326, the MSGraph [reports API](https://learn.microsoft.com/en-us/graph/api/reportroot-getsharepointsiteusagedetail?view=graph-rest-1.0&tabs=http) is a similar case, and Kiota already (correctly) [generates a `Task<Stream>`](https://github.com/microsoftgraph/msgraph-sdk-dotnet/blob/e8d45240d46a84c12ccd683bac59c454ab02b017/src/Microsoft.Graph/Generated/Reports/GetSharePointSiteUsageDetailWithPeriod/GetSharePointSiteUsageDetailWithPeriodRequestBuilder.cs#L41). 

This is probably another spec issue on our part, and I'd love some help/pointers for how to correct it!

Pull request:
Fixes #5208. 

If a response contains `application/octet-stream` as a response type, the Kiota-generated response type should be a binary one. 

An concrete example of when this may happen: an API is migrating from hosting their own assets to returning redirects to a blob storage service, and they may return either a 302 Found and redirect to a URL that contains the asset, or the binary asset directly. For a standard consumer with redirects enabled, the end result is the same. For Kiota, generation needs to know to return binary content types. Currently Kiota will not do so and instead return void types.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3aaefb356d..ba94d4b03b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - Fixed shorthand for refresh option in workspace experience. [#5240](https://github.com/microsoft/kiota/issues/5240)
 - Fixed missing type options in help for plugin commands. [#5230](https://github.com/microsoft/kiota/issues/5230)
 - Removed OpenAI plugins generation since the service does not support them anymore.
+- Redirect status codes documenting an application/octet-stream content type now generate a stream return type. [#5246](https://github.com/microsoft/kiota/issues/5246)
 - Fixed an issue where models would be missing when they had no properties and a single allOf entry. [#5014](https://github.com/microsoft/kiota/issues/5014)
 - Reverts modification of responses in output openApi file when generating plugins [#4945](https://github.com/microsoft/kiota/issues/4945)
 - Expand properties types with null type for Typescript. [#4993](https://github.com/microsoft/kiota-typescript/issues/1188)
diff --git a/src/Kiota.Builder/KiotaBuilder.cs b/src/Kiota.Builder/KiotaBuilder.cs
index e4ade3b5f5..79bce3666e 100644
--- a/src/Kiota.Builder/KiotaBuilder.cs
+++ b/src/Kiota.Builder/KiotaBuilder.cs
@@ -1159,7 +1159,9 @@ openApiExtension is OpenApiPrimaryErrorMessageExtension primaryErrorMessageExten
         };
     }
     private const string RequestBodyPlainTextContentType = "text/plain";
-    private static readonly HashSet<string> noContentStatusCodes = new(StringComparer.OrdinalIgnoreCase) { "201", "202", "204", "205", "301", "302", "303", "304", "307" };
+    private const string RequestBodyOctetStreamContentType = "application/octet-stream";
+    private static readonly HashSet<string> redirectStatusCodes = new(StringComparer.OrdinalIgnoreCase) { "301", "302", "303", "307" };
+    private static readonly HashSet<string> noContentStatusCodes = new(redirectStatusCodes, StringComparer.OrdinalIgnoreCase) { "201", "202", "204", "205", "304" };
     private static readonly HashSet<string> errorStatusCodes = new(Enumerable.Range(400, 599).Select(static x => x.ToString(CultureInfo.InvariantCulture))
                                                                                  .Concat([CodeMethod.ErrorMappingClientRange, CodeMethod.ErrorMappingServerRange]), StringComparer.OrdinalIgnoreCase);
     private void AddErrorMappingsForExecutorMethod(OpenApiUrlTreeNode currentNode, OpenApiOperation operation, CodeMethod executorMethod)
@@ -1248,29 +1250,28 @@ private void AddErrorMappingToExecutorMethod(OpenApiUrlTreeNode currentNode, Ope
             }
             else if (modelType is null)
             {
-                string returnType;
-                if (operation.Responses.Any(static x => noContentStatusCodes.Contains(x.Key)))
-                    returnType = VoidType;
-                else if (operation.Responses.Any(static x => x.Value.Content.ContainsKey(RequestBodyPlainTextContentType)))
-                    returnType = "string";
-                else
-                    returnType = "binary";
-                return (new CodeType { Name = returnType, IsExternal = true, }, null);
+                return (GetExecutorMethodDefaultReturnType(operation), null);
             }
             return (modelType, null);
         }
         else
         {
-            string returnType;
-            if (operation.Responses.Any(static x => noContentStatusCodes.Contains(x.Key)))
-                returnType = VoidType;
-            else if (operation.Responses.Any(static x => x.Value.Content.ContainsKey(RequestBodyPlainTextContentType)))
-                returnType = "string";
-            else
-                returnType = "binary";
-            return (new CodeType { Name = returnType, IsExternal = true, }, null);
+            return (GetExecutorMethodDefaultReturnType(operation), null);
         }
     }
+    private static CodeType GetExecutorMethodDefaultReturnType(OpenApiOperation operation)
+    {
+        string returnType;
+        if (operation.Responses.Any(static x => x.Value.Content.ContainsKey(RequestBodyOctetStreamContentType) && redirectStatusCodes.Contains(x.Key)))
+            returnType = "binary";
+        else if (operation.Responses.Any(static x => noContentStatusCodes.Contains(x.Key)))
+            returnType = VoidType;
+        else if (operation.Responses.Any(static x => x.Value.Content.ContainsKey(RequestBodyPlainTextContentType)))
+            returnType = "string";
+        else
+            returnType = "binary";
+        return new CodeType { Name = returnType, IsExternal = true, };
+    }
     private void CreateOperationMethods(OpenApiUrlTreeNode currentNode, OperationType operationType, OpenApiOperation operation, CodeClass parentClass)
     {
         try


Test patch:
diff --git a/tests/Kiota.Builder.Tests/ContentTypeMappingTests.cs b/tests/Kiota.Builder.Tests/ContentTypeMappingTests.cs
index 6707f96684..07cd3bd7c4 100644
--- a/tests/Kiota.Builder.Tests/ContentTypeMappingTests.cs
+++ b/tests/Kiota.Builder.Tests/ContentTypeMappingTests.cs
@@ -57,6 +57,7 @@ public void Dispose()
     [InlineData("application/octet-stream", "204", false, "default", "void")]
     [InlineData("application/octet-stream", "200", true, "default", "binary")]
     [InlineData("application/octet-stream", "200", false, "default", "binary")]
+    [InlineData("application/octet-stream", "302", false, "default", "binary")] // on a redirect to a binary content we generate a binary return type for download
     [InlineData("text/html", "204", true, "default", "void")]
     [InlineData("text/html", "204", false, "default", "void")]
     [InlineData("text/html", "200", true, "default", "binary")]