Nswag: Can't get PDF download to work

Created on 24 Jul 2018  Â·  4Comments  Â·  Source: RicoSuter/NSwag

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?

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.

services.AddSwaggerGen(c => {
    [...]
    c.MapType<FileContentResult>(() => new Schema {
        Type = "file",
    });
});

All 4 comments

The response type must be "file" otherwise the generated code is wrong...

Maybe we need to add this type to the IsFileResponse list:

https://github.com/RSuter/NSwag/blob/master/src/NSwag.SwaggerGeneration/SwaggerJsonSchemaGenerator.cs#L86

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)…

https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.filecontentresult?view=aspnetcore-2.1

Was this page helpful?
0 / 5 - 0 ratings

Related issues

molszews picture molszews  Â·  4Comments

rh78 picture rh78  Â·  3Comments

akamyshanov picture akamyshanov  Â·  4Comments

alanedwardes picture alanedwardes  Â·  3Comments

p0wertiger picture p0wertiger  Â·  3Comments