What I'm trying to do:
Angular client calls WebAPI method, which fetches a PDF document from the DB and returns it.
Here's my controller code:
[Route("downloadPdfFromDatabase/{id}")]
[HttpGet]
[Produces(@"application/pdf")]
[ProducesResponseType(typeof(FileContentResult), 200)]
public async Task<FileContentResult> DownloadPdfFromDatabase(int id)
{
Document doc = await _documentRepository.FindByFirstOrDefaultAsync(d => d.Id == id);
var contentType = "application/pdf";
var fileContentResult = new FileContentResult(doc.rawByteArray, contentType) {
FileDownloadName = "myDoc.pdf"
};
return fileContentResult;
}
And the generated specification JSON for this method:
"/api/Documents/downloadPdfFromDatabase/{id}": {
"get": {
"tags": [
"Documents"
],
"operationId": "ApiDocumentsDownloadPdfFromDatabaseByIdGet",
"consumes": [],
"produces": [],
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"type": "integer",
"format": "int32"
}
],
"responses": {
"200": {
"description": "Success",
"schema": {
"$ref": "#/definitions/FileContentResult"
}
}
}
}
}
... which results in these:
/**
* @return Success
*/
downloadPdfFromDatabase(id: number): Observable<void> {
let url_ = this.baseUrl + "/api/Documents/downloadPdfFromDatabase/{id}";
if (id === undefined || id === null)
throw new Error("The parameter 'id' must be defined.");
url_ = url_.replace("{id}", encodeURIComponent("" + id));
url_ = url_.replace(/[?&]$/, "");
let options_ : any = {
observe: "response",
responseType: "blob",
headers: new HttpHeaders({
"Content-Type": "application/json",
})
};
return this.http.request("get", url_, options_).flatMap((response_ : any) => {
return this.processDownloadPdfFromDatabase(response_);
}).catch((response_: any) => {
if (response_ instanceof HttpResponseBase) {
try {
return this.processDownloadPdfFromDatabase(<any>response_);
} catch (e) {
return <Observable<void>><any>Observable.throw(e);
}
} else
return <Observable<void>><any>Observable.throw(response_);
});
}
protected processDownloadPdfFromDatabase(response: HttpResponseBase): Observable<void> {
const status = response.status;
const responseBlob =
response instanceof HttpResponse ? response.body :
(<any>response).error instanceof Blob ? (<any>response).error : undefined;
let _headers: any = {}; if (response.headers) { for (let key of response.headers.keys()) { _headers[key] = response.headers.get(key); }};
if (status === 200) {
return blobToText(responseBlob).flatMap(_responseText => {
return Observable.of<void>(<any>null);
});
} else if (status !== 200 && status !== 204) {
return blobToText(responseBlob).flatMap(_responseText => {
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
});
}
return Observable.of<void>(<any>null);
}
And, finally, this error when calling it via the proxy:
SyntaxError: Unexpected token % in JSON at position 0
at JSON.parse (<anonymous>)
at MergeMapSubscriber.project (service-proxies.ts:789)
[...]
The HTTP response from the Web API looks exactly as I'd expect it and, in fact, I can download the file without any issues from Swagger UI.
I've tried adding/removing several things, mainly annotations in the .NET controller.
Removing [Produces] and [ProducesResponseType] at least generated a TS client that seemed to get _so close_.
Specification JSON:
"/api/Dokumente/downloadPdfFromDatabase/{id}": {
"get": {
"tags": [
"Dokumente"
],
"operationId": "ApiDokumenteDownloadPdfFromDatabaseByIdGet",
"consumes": [],
"produces": [],
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"type": "integer",
"format": "int32"
}
],
"responses": {
"200": {
"description": "Success"
}
}
}
},
and TS client:
/**
* @return Success
*/
downloadPdfFromDatabase(id: number): Observable<Blob> {
let url_ = this.baseUrl + "/api/Dokumente/downloadPdfFromDatabase/{id}";
if (id === undefined || id === null)
throw new Error("The parameter 'id' must be defined.");
url_ = url_.replace("{id}", encodeURIComponent("" + id));
url_ = url_.replace(/[?&]$/, "");
let options_ : any = {
observe: "response",
responseType: "blob",
headers: new HttpHeaders({
"Content-Type": "application/json",
})
};
return this.http.request("get", url_, options_).pipe(_observableMergeMap((response_ : any) => {
return this.processDownloadPdfFromDatabase(response_);
})).pipe(_observableCatch((response_: any) => {
if (response_ instanceof HttpResponseBase) {
try {
return this.processDownloadPdfFromDatabase(<any>response_);
} catch (e) {
return <Observable<void>><any>_observableThrow(e);
}
} else
return <Observable<void>><any>_observableThrow(response_);
}));
}
protected processDownloadPdfFromDatabase(response: HttpResponseBase): Observable<Blob> {
const status = response.status;
const responseBlob =
response instanceof HttpResponse ? response.body :
(<any>response).error instanceof Blob ? (<any>response).error : undefined;
let _headers: any = {}; if (response.headers) { for (let key of response.headers.keys()) { _headers[key] = response.headers.get(key); }};
if (status === 200){
return blobToText(responseBlob).pipe(_observableMergeMap(_responseText => {
return _observableOf<void>(<any>null);
}));
} else if (status !== 200 && status !== 204) {
return blobToText(responseBlob).pipe(_observableMergeMap(_responseText => {
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
}));
}
return _observableOf<Blob>(<Blob>null);
}
But, of course, only returns null. If I return responseBlob manually, I can actually access it from the calling class, albeit, of course, without any metadata (read: file name).
Though even if that _would_ work as I wanted it to, I'd have to manually replace it every time the client is updated.
Can anyone give me some pointers as to what I'm doing incorrectly and how I could get this to work?
The response type must be "file" otherwise the generated code is wrong...
Maybe we need to add this type to the IsFileResponse list:
Try
[ProducesResponseType(typeof(FileResult), 200)]
Try
[ProducesResponseType(typeof(FileResult), 200)]
Doing so generated the same specification as FileContentResult, apart from "$ref": "#/definitions/FileResult".
Could the problem be public async Task<FileContentResult> in the controller or is it supposed to work with that anyway?
Edit: Tried that with a sync method, specification still did not generate with type "file".
Looking a bit further into it, this appears to be a problem with Swashbuckle, not NSwag. Sorry!
Edit: In case someone comes here with the same problem, I added the following to my AddSwaggerGen call.
services.AddSwaggerGen(c => {
[...]
c.MapType<FileContentResult>(() => new Schema {
Type = "file",
});
});
Regarding NSwag swagger generator: FileContentResult is handled by FileResult (base class)…
Most helpful comment
Looking a bit further into it, this appears to be a problem with Swashbuckle, not NSwag. Sorry!
Edit: In case someone comes here with the same problem, I added the following to my AddSwaggerGen call.