Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Send Buffer/binary response to client #800

Closed
2 of 4 tasks
sundowndev opened this issue Sep 29, 2020 · 7 comments
Closed
2 of 4 tasks

Send Buffer/binary response to client #800

sundowndev opened this issue Sep 29, 2020 · 7 comments

Comments

@sundowndev
Copy link

sundowndev commented Sep 29, 2020

#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 ...

    • bug report
    • feature request
    • support request
  • I confirm that I

    • 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]
@sundowndev
Copy link
Author

sundowndev commented Sep 30, 2020

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

@WoH
Copy link
Collaborator

WoH commented Oct 3, 2020

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.

@sundowndev
Copy link
Author

sundowndev commented Oct 5, 2020

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": []
			}
		},

@github-actions
Copy link

github-actions bot commented Nov 7, 2020

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

@mb21
Copy link

mb21 commented Jul 25, 2022

The title of this issue is a bit confusing, especially since it's resolved. From what I call tell looking at this line, pretty sure it only works when you return a Readable, not a Buffer...

@mb21
Copy link

mb21 commented Jul 25, 2022

Adding something like this to the default express template should fix it:

import { Readable } from 'stream';

...

} else if (data instanceof Buffer) {
  Readable.from(data).pipe(response);

@Sawtaytoes
Copy link

Sawtaytoes commented Jul 9, 2023

I dunno if this is related, but I saw the code for .readable in routes.ts and don't think that's quite enough.

I'm using S3's GetObjectCommand, and it returns me the "body" of an object like so:

return (
  object
  .Body
  .transformToByteArray()
)

When I do transformToByteArray, I can send that as a JSON-stringified Uint8Array to the client, but that's causing issues for me.

Instead, I had to modify the response this way:

new FileService()
.get(
  filename,
)
.then((
  fileData
) => {
  request
  ?.res
  ?.write(
    fileData
  )

  request
  ?.res
  ?.end()
})

Is it possible there's something I'm missing that's not documented? There's a whole section on uploading files to tsoa (which works for me), but I'm missing the part about how to download files from tsoa.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants