Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions src/Refitter.Core/RefitInterfaceGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,12 @@ protected string GetTypeName(OpenApiOperation operation)
return type is null or "void" ? GetAsyncOperationType(true) : $"{GetAsyncOperationType(false)}<{WellKnownNamespaces.TrimImportedNamespaces(type)}>";
}

// Check if response is a file stream
if (IsFileStreamResponse(operation))
{
return $"{GetAsyncOperationType(false)}<HttpResponseMessage>";
}

// First check for explicit success status codes
var successCodes = new[] { "200", "201", "203", "206" };
var returnTypeParameter = successCodes
Expand All @@ -138,6 +144,54 @@ protected string GetTypeName(OpenApiOperation operation)
return GetReturnType(returnTypeParameter);
}

/// <summary>
/// Determines if the operation response is a file stream (binary content).
/// </summary>
/// <param name="operation">The OpenAPI operation to check.</param>
/// <returns>True if the response is a file stream, false otherwise.</returns>
private static bool IsFileStreamResponse(OpenApiOperation operation)
{
var successCodes = new[] { "200", "201", "203", "206", "2XX" };

foreach (var code in successCodes)
{
if (!operation.Responses.TryGetValue(code, out var apiResponse))
continue;

var response = apiResponse.ActualResponse;

if (response.Content?.Any() != true)
continue;

foreach (var contentEntry in response.Content)
{
if (IsFileContentType(contentEntry.Key))
{
var schema = contentEntry.Value?.Schema;
if (schema?.Format == "binary" || schema?.Type == NJsonSchema.JsonObjectType.File)
return true;
}
Comment on lines +152 to +173

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Handle file content types with missing schemas.
OpenAPI allows a media type without a schema; those would be treated as non-file here. If you want schema-less file responses to map to HttpResponseMessage, add a fallback (optionally excluding application/vnd* if you want to keep vendor JSON strict).

💡 Possible fallback
                 if (IsFileContentType(contentEntry.Key))
                 {
                     var schema = contentEntry.Value?.Schema;
-                    if (schema?.Format == "binary" || schema?.Type == NJsonSchema.JsonObjectType.File)
+                    if (schema == null)
+                        return true;
+                    if (schema.Format == "binary" || schema.Type == NJsonSchema.JsonObjectType.File)
                         return true;
                 }
🤖 Prompt for AI Agents
In `@src/Refitter.Core/RefitInterfaceGenerator.cs` around lines 152 - 173, The
IsFileStreamResponse method misses media types that have no Schema and thus
won't be detected as file responses; update IsFileStreamResponse to treat
content entries whose Schema is null as file streams for appropriate content
types by adding a fallback check after retrieving var schema =
contentEntry.Value?.Schema — if schema is null and
IsFileContentType(contentEntry.Key) (optionally also ensure the content type is
not a vendor JSON like application/vnd* if you want to exclude those), return
true so responses with schema-less file media types map to HttpResponseMessage;
keep the existing binary/Type checks (schema.Format == "binary" || schema.Type
== JsonObjectType.File) and only use the schema-null fallback when those are
absent.

}
Comment on lines +166 to +174

Copilot AI Jan 20, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This foreach loop implicitly filters its target sequence - consider filtering the sequence explicitly using '.Where(...)'.

Copilot uses AI. Check for mistakes.
}
Comment on lines +156 to +175

Copilot AI Jan 20, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This foreach loop implicitly filters its target sequence - consider filtering the sequence explicitly using '.Where(...)'.

Copilot uses AI. Check for mistakes.

return false;
}

private static bool IsFileContentType(string contentType)
{
return
contentType.StartsWith("application/octet-stream", StringComparison.OrdinalIgnoreCase) ||
contentType.StartsWith("application/pdf", StringComparison.OrdinalIgnoreCase) ||
contentType.StartsWith("application/vnd", StringComparison.OrdinalIgnoreCase) ||
contentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase) ||
contentType.StartsWith("video/", StringComparison.OrdinalIgnoreCase) ||
contentType.StartsWith("audio/", StringComparison.OrdinalIgnoreCase) ||
contentType.StartsWith("application/zip", StringComparison.OrdinalIgnoreCase) ||
Comment thread
frogcrush marked this conversation as resolved.
contentType.StartsWith("application/gzip", StringComparison.OrdinalIgnoreCase) ||
(contentType.StartsWith("application/x-", StringComparison.OrdinalIgnoreCase) &&
!contentType.StartsWith("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase));
}

private string GetTypeName(string code, OpenApiOperation operation)
{
var schema = operation.Responses[code].ActualResponse.Schema;
Expand Down Expand Up @@ -254,6 +308,16 @@ private string GetDefaultReturnType()
/// <returns>True if the type is an ApiResponse Task or similar, false otherwise.</returns>
protected static bool IsApiResponseType(string typeName)
{
// Check for HttpResponseMessage
if (Regex.IsMatch(
typeName,
"(Task|IObservable)<HttpResponseMessage>",
RegexOptions.None,
TimeSpan.FromSeconds(1)))
{
return true;
}

return Regex.IsMatch(
typeName,
"(Task|IObservable)<(I)?ApiResponse(<[\\w<>]+>)?>",
Expand Down
163 changes: 163 additions & 0 deletions src/Refitter.Tests/Examples/FileStreamResponseTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
using FluentAssertions;
using Refitter.Core;
using Refitter.Tests.Build;
using Refitter.Tests.TestUtilities;
using TUnit.Core;

namespace Refitter.Tests.Examples;

public class FileStreamResponseTests
{
private const string OpenApiSpec = @"
openapi: '3.0.0'
info:
title: File Download API
version: 1.0.0
paths:
'/files/{fileId}/download':
get:
operationId: downloadFile
summary: Download a file
parameters:
- name: fileId
in: path
required: true
schema:
type: string
responses:
'200':
description: File downloaded successfully
content:
application/octet-stream:
schema:
type: string
format: binary
'/images/{imageId}':
get:
operationId: getImage
summary: Get an image
parameters:
- name: imageId
in: path
required: true
schema:
type: string
responses:
'200':
description: Image retrieved successfully
content:
image/jpeg:
schema:
type: string
format: binary
image/png:
schema:
type: string
format: binary
'/documents/{docId}':
get:
operationId: getDocument
summary: Get a PDF document
parameters:
- name: docId
in: path
required: true
schema:
type: string
responses:
'200':
description: Document retrieved successfully
content:
application/pdf:
schema:
type: string
format: binary
'/forms/{formId}/submit':
get:
operationId: submitForm
summary: Submit form data
parameters:
- name: formId
in: path
required: true
schema:
type: string
responses:
'200':
description: Form submission result
content:
application/x-www-form-urlencoded:
schema:
type: object
properties:
result:
type: string
";

[Test]
public async Task Can_Generate_Code()
{
string generatedCode = await GenerateCode();
generatedCode.Should().NotBeNullOrWhiteSpace();
}

[Test]
public async Task Generates_HttpResponseMessage_For_OctetStream()
{
string generatedCode = await GenerateCode();
generatedCode.Should().Contain("Task<HttpResponseMessage> DownloadFile(string fileId");
}

[Test]
public async Task Generates_HttpResponseMessage_For_ImageJpeg()
{
string generatedCode = await GenerateCode();
generatedCode.Should().Contain("Task<HttpResponseMessage> GetImage(string imageId");
}

[Test]
public async Task Generates_HttpResponseMessage_For_ApplicationPdf()
{
string generatedCode = await GenerateCode();
generatedCode.Should().Contain("Task<HttpResponseMessage> GetDocument(string docId");
}

[Test]
public async Task Does_Not_Generate_HttpResponseMessage_For_FormUrlEncoded()
{
string generatedCode = await GenerateCode();
generatedCode.Should().Contain("submitForm");
generatedCode.Should().NotContain("Task<HttpResponseMessage> SubmitForm(");
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

[Test]
public async Task Can_Build_Generated_Code()
{
string generatedCode = await GenerateCode();
BuildHelper
.BuildCSharp(generatedCode)
.Should()
.BeTrue();
}

[Test]
public async Task Generates_HttpResponseMessage_With_IApiResponse_Setting()
{
string generatedCode = await GenerateCode(returnIApiResponse: true);
generatedCode.Should().Contain("Task<HttpResponseMessage> DownloadFile(string fileId");
}

private static async Task<string> GenerateCode(bool returnIApiResponse = false)
{
var swaggerFile = await SwaggerFileHelper.CreateSwaggerFile(OpenApiSpec);
var settings = new RefitGeneratorSettings
{
OpenApiPath = swaggerFile,
ReturnIApiResponse = returnIApiResponse
};

var sut = await RefitGenerator.CreateAsync(settings);
var generatedCode = sut.Generate();
return generatedCode;
}
}
Loading