Several of my API methods result in a file download. How can I get Swashbuckle to download a file through the UI for these methods?
I've added an IOperationFilter with the following Apply method to add a "produces" attribute to certain methods:
public void Apply(Operation operation, SchemaRegistry schemaRegistry, ApiDescription apiDescription)
{
if (apiDescription.ID == "GETapi/Templates/{id}")
{
operation.produces.Add("application/octet-stream");
}
}
But when I execute this method through the Swashbuckle UI it displays the file contents in the "Response Body" textarea. Is there a way of getting a file to actually download?
Thanks
This is more of a swagger-ui issue. In fact, a quick search brought me to the following issue
https://github.com/swagger-api/swagger-ui/issues/1196
Looks like the fix has gone in and so should be available with the next SB release.
Hi. I'm trying to do the similar thing - generate correct swagger description for file download endpoints.
As per swagger spec - to show that the response is file - it must have special "type": "file" and correct 'produces' mime-type. I can set correct mime-type but can't find any way to force response schema type to be 'file'. looked through the Swashbuckle code and it seems that there is nothing that allow you to set type for schema on per operation\action basis.
@centur - you can wire up an IOperationFilter (see readme for details). It would look something like this ...
public class UpdateFileDownloadOperations : IOperationFilter
{
public void Apply(Operation operation, SchemaRegistry schemaRegistry, ApiDescription apiDescription)
{
if (operation.operationId == "FileDownload_GetFile")
{
operation.produces = new[] { "application/octet-stream" };
operation.responses["200"].schema = new Schema { type = "file" };
}
}
}
I've used this to create a valid spec, but it seems the UI still doesn't support the downloads yet ...
Thanks a lot.
@domaindrivendev Please inform about the format returned in the API method attribute
@lakeba - you'll have to elaborate your question, I really have no idea what you're asking?
Here's one, I rolled my own based on @domaindrivendev code. I have another IOperationFilter extension for setting the ResponseType
[SwaggerFileResponse(HttpStatusCode.OK, "File Response")]
public HttpResponseMessage GetFile(string id)...
/// <summary>
/// SwaggerFileResponseAttribute
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)]
public sealed class SwaggerFileResponseAttribute : SwaggerResponseAttribute
{
public SwaggerFileResponseAttribute(HttpStatusCode statusCode) : base(statusCode)
{
}
public SwaggerFileResponseAttribute(HttpStatusCode statusCode, string description = null, Type type = null)
: base(statusCode, description, type)
{
}
public SwaggerFileResponseAttribute(int statusCode) : base(statusCode)
{
}
public SwaggerFileResponseAttribute(int statusCode, string description = null, Type type = null)
: base(statusCode, description, type)
{
}
}
public void Apply(Operation operation, SchemaRegistry schemaRegistry, ApiDescription apiDescription)
{
if (apiDescription.GetControllerAndActionAttributes<SwaggerResponseRemoveDefaultsAttribute>().Any())
operation.responses.Clear();
var responseAttributes = apiDescription.GetControllerAndActionAttributes<SwaggerFileResponseAttribute>()
.OrderBy(attr => attr.StatusCode);
foreach (var attr in responseAttributes)
{
var statusCode = attr.StatusCode.ToString();
Schema responseSchema = new Schema { format = "byte", type = "file" };
operation.responses[statusCode] = new Response
{
description = attr.Description ?? InferDescriptionFrom(statusCode),
schema = responseSchema
};
}
}
private string InferDescriptionFrom(string statusCode)
{
HttpStatusCode enumValue;
if (Enum.TryParse(statusCode, true, out enumValue))
{
return enumValue.ToString();
}
return null;
}
GlobalConfiguration.Configuration.EnableSwagger(c =>
c.OperationFilter<UpdateFileResponseTypeFilter>();
);
@OzBob Your code generates what I need. Except for one line:
_httpResponse = await this.Client.HttpClient.SendAsync(_httpRequest, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
That is inside in a method of the generated code that I use to call the my service, but I can't get my file. I just found that only works with:
_httpResponse = await this.Client.HttpClient.SendAsync(_httpRequest, cancellationToken).ConfigureAwait(false);
Is there a way to generate it like that? In an earlier attempt it generates like that when HttpOperationResponse< object >() instead HttpOperationResponse
@MrLinDowsMac Do you mean 'generates' by using AutoRest on the swagger file to generate a client? There may be more swagger instructions required to get the output you need. If you manually play with the swagger file until that works then we can update the sample Filter above ...
@OzBob Yes, I mean that your code generates the json metadata file that autorest generates as c# files, with the only problem that I described. I have no clue which changes I have to do in the custom filters to avoid autorest generate file with that line like that. That httpCompletitionOption is messing it up. It works only with ResponseContentRead.
@MrLinDowsMac
Generated with:
AutoRest\0.17\AutoRest.exe" -codeGenerator CSharp -input ..\swagger.json -OutputDirectory Client -namespace MyApi -AddCredentials true -Header NONE -ClientName MyClient
This is the Client Code I use when processing the SendAsync:
`
//lots deleted ...
_httpResponse = await base.HttpClient.SendAsync(_httpRequest, cancellationToken).ConfigureAwait(false);
if (_shouldTrace)
{
ServiceClientTracing.ReceiveResponse(_invocationId, _httpResponse);
}
HttpStatusCode _statusCode = _httpResponse.StatusCode;
cancellationToken.ThrowIfCancellationRequested();
if ((int)_statusCode != 200)
{
var ex = new HttpOperationException(string.Format("Operation returned an invalid status code '{0}'", _statusCode));
ex.Request = new HttpRequestMessageWrapper(_httpRequest, _requestContent);
ex.Response = new HttpResponseMessageWrapper(_httpResponse, "{\"status\":" + _statusCode + ", \"error\":\"unkown\" }");
if (_shouldTrace)
{
ServiceClientTracing.Error(_invocationId, ex);
}
_httpRequest.Dispose();
if (_httpResponse != null)
{
_httpResponse.Dispose();
}
throw ex;
}
// Create Result
var _result = new HttpOperationResponse<byte[]>();
_result.Request = _httpRequest;
_result.Response = _httpResponse;
// Deserialize Response
if ((int)_statusCode == 200)
{
try
{
_result.Body = await _httpResponse.Content.ReadAsByteArrayAsync().ConfigureAwait(false);
}
catch (Exception ex)
{
if (_shouldTrace)
{
ServiceClientTracing.Error(_invocationId, ex);
}
_httpRequest.Dispose();
if (_httpResponse != null)
{
_httpResponse.Dispose();
}
throw ex;
}
}
//more code ....
`
Mmmm... I don't know why is generating you a ReadAsByteArrayAsync, I get a ReadAsStreamAsync instead. I used something like this:
C:\_Sources\MyAPIproject\packages\AutoRest.0.17.3\tools>AutoRest.exe -Input http:/
/localhost:52977/swagger/docs/v1 -Output ..\clientproject -AddCredentials false -Namespace MyAPIproject.Pdfservice -codeGenerator CSharp
which generates this:
//omitted code...
// This line is messing up my requests:
_httpResponse = await this.Client.HttpClient.SendAsync(_httpRequest, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
if (_shouldTrace)
{
Microsoft.Rest.ServiceClientTracing.ReceiveResponse(_invocationId, _httpResponse);
}
System.Net.HttpStatusCode _statusCode = _httpResponse.StatusCode;
cancellationToken.ThrowIfCancellationRequested();
string _responseContent = null;
if ((int)_statusCode != 200)
{
var ex = new Microsoft.Rest.HttpOperationException(string.Format("Operation returned an invalid status code '{0}'", _statusCode));
if (_httpResponse.Content != null) {
_responseContent = await _httpResponse.Content.ReadAsStringAsync().ConfigureAwait(false);
}
else {
_responseContent = string.Empty;
}
ex.Request = new Microsoft.Rest.HttpRequestMessageWrapper(_httpRequest, _requestContent);
ex.Response = new Microsoft.Rest.HttpResponseMessageWrapper(_httpResponse, _responseContent);
if (_shouldTrace)
{
Microsoft.Rest.ServiceClientTracing.Error(_invocationId, ex);
}
_httpRequest.Dispose();
if (_httpResponse != null)
{
_httpResponse.Dispose();
}
throw ex;
}
// Create Result
var _result = new Microsoft.Rest.HttpOperationResponse<System.IO.Stream>();
_result.Request = _httpRequest;
_result.Response = _httpResponse;
// Deserialize Response
if ((int)_statusCode == 200)
{ //** ---------------- HERE ------------------ **
_result.Body = await _httpResponse.Content.ReadAsStreamAsync().ConfigureAwait(false);
}
if (_shouldTrace)
{
Microsoft.Rest.ServiceClientTracing.Exit(_invocationId, _result);
}
return _result;
}
//more code
@MrLinDowsMac I think that was because I changed my local copy of AutoRest and rebuilt it from Source:Client/MethodTemplate.cshtml
Can you get your file with the 'ReadAsStreamAsync'? If you need a byte[] use Stream.ToArray().
Hi, been struggling with this for a while - and can't get it working.
The issue in swagger-ui referenced above (swagger-api/swagger-ui#1196) is that included in the .Net Core version of Swashbuckle?
I am currently using 6.6.0-beta902 in my project - is this the right version.
I can get the swagger.json file to contain schema { "type": "file" } as I am supposed - but it doesn't make a difference in SwaggerUI when I try to call the endpoint.
Not sure how to help on this one - 1196 changes PR1122 is already in main swagger.io branch (https://github.com/swagger-api/swagger-ui/blob/master/src/main/javascript/view/OperationView.js#L698). Could you take your swagger file and load and test it here? http://editor.swagger.io
My site is under developement - so I don't have a public url for my JSON yet - but will try to find a place to upload the JSON file - and then I can paste it in at http://editor.swagger.io. (Have few other tasks this morning - so will reply later)
I am well aware that the above description of the issue is vague, and not well reproducable. But I was stuck/frustrated and have tried all the options that seems to work for others.
So i guess the question was more if Swashbuckle 5.5.3 and 6.6.0-beta902 contains the same swagger-ui - as I couldn't figure this out myself. And if the trouble I encounter is just due to 6.6.0 being from august 2016, and 5.5.3 being from november - and thus might contain different swagger-ui.
And whether or not I am correct in using 6.6.0-beta902 when working on a .Net Core WebApi project? It doesn't seem like 5.5.3 supports the monikers for net451.
Ok, tried it in http://editor.swagger.io now. Just pasted in my file, and it seems to be the same.
So clearly there is something not connected right - I will try continue working a bit with the editor to cut down my sample - and then maybe repost a question in general.
Now I have the idea of how I change the schema in .Net Core WebApi, it just seems to be a matter of getting Swagger to understand what i am trying to do.
The editor recognizes (or just prints) "file" as schema - but on execution it just renders the PDF as an UTF-8 string - not a binary download link.

@OzBob As I said, I can get my file with "ReadAsStreamAsync" only if change this line
_httpResponse = await this.Client.HttpClient.SendAsync(_httpRequest, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
to this:
_httpResponse = await this.Client.HttpClient.SendAsync(_httpRequest, System.Net.Http.HttpCompletionOption.ResponseContentRead, cancellationToken).ConfigureAwait(false);
Nuget version of Autorest (0.17.3) generates code like the first one. I downloaded the autoinstaller of autorest (currently in preview) and also generates lines like that for streams...
@MrLinDowsMac
Autorest uses HttpClient.SendAsync's http.httpcompletionoption. to indicate that once Response Headers are read the SendAsync operation should finalise, but it does NOT download the content.
The Stream should be able to be completely downloaded using ReadAsStreamAsync, please test this I'd like to know what you find. Are there any Headers in the Response maybe there is an error there?
Either way - this is now definitely not a Swashbuckle issue. Raise this in Autorest Issues, and link it here.
Weird, we are having the same problem. We have the latest version of SB 2.4.0. Why is this closed?
We have the following function:
[HttpGet("{id}")]
[ProducesResponseType(typeof(FileStreamResult), 200)]
public async Task<IActionResult> GetFileById(string id)
Which produces the following Swagger Spec:
'/Files/{id}':
get:
tags:
- Files
operationId: FilesByIdGet
consumes: []
produces:
- text/plain
- application/json
- text/json
parameters:
- name: id
in: path
required: true
type: string
responses:
'200':
description: Success
schema:
$ref: '#/definitions/FileStreamResult'
This is not correct, so when I generate a client with AutoRest or NSwag, it tries to serialize the data as JSON.
I monkey patched it with this operation filter and it works fine:
public class SwaggerFileOperationFilter : IOperationFilter
{
public void Apply(Operation operation, OperationFilterContext context)
{
var anyFileStreamResult = context.ApiDescription.SupportedResponseTypes
.Any(x => x.Type == typeof(FileStreamResult));
if (anyFileStreamResult)
{
operation.Produces = new[] { "application/octet-stream" };
operation.Responses["200"].Schema = new Schema { Type = "file" };
}
}
}
TL;DR - The SwaggerFileOperationFilter still works, thanks for that! But if you have problems with generating a client, update your packages. Second, if you use OpenAPI 3.0 the implementation needs to change slightly.
Had exactly the same scenario as @gaui mentioned. And using the SwaggerOperationFilter it produced the correct output:
"/reports/summary": {
"get": {
"tags": [
"Reports"
],
"summary": "...",
"operationId": "Reports_Summary",
"consumes": [],
"produces": [
"application/octet-stream"
],
"responses": {
"200": {
"description": "Success",
"schema": {
"type": "file"
}
}
}
}
},
Important parts here are produces and responses. But when generating a client with NSwag it failed with the error:
Value cannot be null.
Parameter name: schema
Updating NSwag from v12.0.15 => v12.0.18 fixed that.
Finally, I noticed in the OpenAPI docs, section "Response that Returns a File" that if you're planning on using OpenAPI 3.0 you need to change the Schema to:
c#
operation.Responses["200"].Schema = new Schema { Type = "string", Format = "binary" };
@gaui do you have an operation filter using the OpenApiOperation? operation.Produces and operation.Responses changed their way of working...
EDIT: I've got one example from this issue: https://github.com/domaindrivendev/Swashbuckle.WebApi/issues/1360
Most helpful comment
Weird, we are having the same problem. We have the latest version of SB 2.4.0. Why is this closed?
We have the following function:
Which produces the following Swagger Spec:
This is not correct, so when I generate a client with AutoRest or NSwag, it tries to serialize the data as JSON.
I monkey patched it with this operation filter and it works fine: