Tsoa: Send Buffer/binary response to client

Created on 29 Sep 2020  路  4Comments  路  Source: lukeautry/tsoa

https://github.com/lukeautry/tsoa/issues/435 and #44 were both closed but the issue is still here. Is there any way to send files to client using tsoa? Why doesn't @Response decorator exists ?

Sorting

  • I'm submitting a ...

    • [x] bug report
    • [ ] feature request
    • [ ] support request
  • I confirm that I

    • [x] used the search to make sure that a similar issue hasn't already been submit

Expected Behavior


Router to send file buffer to be downloaded by the client.

Current Behavior


When using Attempt 1: the file is sent but the file is invalid and cannot be opened.

When using Attempt 2:

TypeError: Cannot read property 'on' of undefined
    at ReadStream.Readable.pipe (_stream_readable.js:669:8)
    at OrderETicketsController.read (/app/dist/controllers/orderEtickets/controllers.js:22:24)
    at processTicksAndRejections (internal/process/task_queues.js:97:5)
error Command failed with signal "SIGINT".

Possible Solution

Steps to Reproduce


Attempt 1

this.setStatus(200);
this.setHeader('Content-Type', 'application/pdf');
this.setHeader('Content-Disposition', `attachment; filename=${fileName}`);

return fs.promises.readFile(filePath);

Attempt 2

req.res.download(filePath) // req.res is undefined!

Context (Environment)

Version of the library: 3.3.0 (latest)
Version of NodeJS: v12.18.1

  • Confirm you were using yarn not npm: [x]
Stale bug express-router more-information-needed

Most helpful comment

Ok I finally managed to do it!

The generated routes file already handles streams, you just have to return a valid readable stream.

function returnHandler(response: any, statusCode?: number, data?: any, headers: any = {}) {
    Object.keys(headers).forEach((name: string) => {
      response.set(name, headers[name]);
    });

    if (data && typeof data.pipe === 'function' && data.readable && typeof data._read === 'function') {
      // Here, data.pipe must be a function
      data.pipe(response);

    } else if (data || data === false) { // === false allows boolean result
      response.status(statusCode || 200).json(data);
    } else {
      response.status(statusCode || 204).end();
    }
  }

Controller:

public async download(): Promise<fs.ReadStream> {
      const filePath: string = '/path/to/file';
      const fileName: string = 'doc.pdf';

      const stat: fs.Stats = await fs.promises.stat(filePath);

      this.setStatus(200);
      this.setHeader('Content-Type', mime.lookup(filePath));
      this.setHeader('Content-Length', stat.size.toString());
      // Removing this line will cause to not launch the download, just serve the file as it
      this.setHeader('Content-Disposition', `attachment; filename=${fileName}`);

      return fs.createReadStream(filePath);
  }

The solution is more complicated than using res.download but it doesn't even work because type fs.ReadStream cannot be imported by tsoa. So there's an option in routes.ts to use readable stream but it's impossible to return a stream type from the controller. I'd suggest to implement a feature to make file download easier (like it is already with Express).

Generate routes error.
 Error: No matching module declarations found for fs.
    at new GenerateMetadataError (/.../node_modules/tsoa/dist/metadataGeneration/exceptions.js:22:28)

So the only way I found to fix this is to use Readable interface from stream :

import { Readable } from 'stream';

// ...
public async download(): Promise<Readable> {
  // ...

The documentation is wrong but it works as needed.

image

All 4 comments

Ok I finally managed to do it!

The generated routes file already handles streams, you just have to return a valid readable stream.

function returnHandler(response: any, statusCode?: number, data?: any, headers: any = {}) {
    Object.keys(headers).forEach((name: string) => {
      response.set(name, headers[name]);
    });

    if (data && typeof data.pipe === 'function' && data.readable && typeof data._read === 'function') {
      // Here, data.pipe must be a function
      data.pipe(response);

    } else if (data || data === false) { // === false allows boolean result
      response.status(statusCode || 200).json(data);
    } else {
      response.status(statusCode || 204).end();
    }
  }

Controller:

public async download(): Promise<fs.ReadStream> {
      const filePath: string = '/path/to/file';
      const fileName: string = 'doc.pdf';

      const stat: fs.Stats = await fs.promises.stat(filePath);

      this.setStatus(200);
      this.setHeader('Content-Type', mime.lookup(filePath));
      this.setHeader('Content-Length', stat.size.toString());
      // Removing this line will cause to not launch the download, just serve the file as it
      this.setHeader('Content-Disposition', `attachment; filename=${fileName}`);

      return fs.createReadStream(filePath);
  }

The solution is more complicated than using res.download but it doesn't even work because type fs.ReadStream cannot be imported by tsoa. So there's an option in routes.ts to use readable stream but it's impossible to return a stream type from the controller. I'd suggest to implement a feature to make file download easier (like it is already with Express).

Generate routes error.
 Error: No matching module declarations found for fs.
    at new GenerateMetadataError (/.../node_modules/tsoa/dist/metadataGeneration/exceptions.js:22:28)

So the only way I found to fix this is to use Readable interface from stream :

import { Readable } from 'stream';

// ...
public async download(): Promise<Readable> {
  // ...

The documentation is wrong but it works as needed.

image

Why doesn't @Response decorator exists ?

It does.

I assume this affects the Express template only?
We are checking for Readable Streams, seems like we should handle Buffer aswell, the type resolution seems fine, but can't tell from SwUi, please post the relevant spec.

I assume this affects the Express template only?

Yes.

please post the relevant spec

Here's an example of output from tsoa :)

        "/download": {
            "get": {
                "operationId": "downloadFile",
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string",
                                    "format": "byte"
                                }
                            }
                        }
                    },
                },
                "description": "Download a file.",
                "tags": [
                    "Download"
                ],
                "security": [],
                "parameters": []
            }
        },

This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days

Was this page helpful?
0 / 5 - 0 ratings