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 file / return raw binary "application/octect-stream" or "image/jpeg" ... #44

Closed
AntoineABI opened this issue Jan 16, 2017 · 15 comments

Comments

@AntoineABI
Copy link
Contributor

Hello,

I am struggling on something I wan't to do and it's seems not possible to do it with the tsoa framework.
Returning raw data or file is not allowed by using a standard controller. Obviously, I can define my own express route and custom controller but I can't add the spec to swagger.json automatically because when I fill the spec field in tsoa.json it is overriding every paths !

Proposal:

  • merge tsoa.json "swagger"."spec" recursively instead of overriding at the first level.
  • allow more customization of controller and extend the router/promiseHandler.

My solution:

  • Defining custom route with express
  • Custom tsoa.spec.json merged with my own script
    (not yet done wanna see if you guys have another way around or if I missed something?)

Regards.

PS: It is whats looks like my tsoa.json if it can help

{
    "swagger": {
        "outputDirectory": "./dist",
        "entryFile": "./src/server.ts",
        "host": "localhost:3000",
        "basePath": "/v1",
        "spec": {
            "paths": {
                "/Data/GetUserProfilePicture/{userId}": {
                    "get": {
                        "operationId": "getUserProfilePicture",
                        "produces": [
                            "image/jpeg"
                        ],
                        "responses": {
                            "200": {
                                "description": "",
                                "examples": {},
                                "schema": {
                                    "type": "binary"
                                }
                            }
                        },
                        "description": "Get an user profile picture",
                        "parameters": [
                            {
                                "description": "",
                                "in": "path",
                                "name": "userId",
                                "required": true,
                                "type": "integer",
                                "format": "int64"
                            }
                        ]
                    }
                }
            }
        }
    },
    "routes": {
        "basePath": "/v1",
        "entryFile": "./src/server.ts",
        "routesDir": "./src",
        "middleware": "express"
    }
}
@lukeautry
Copy link
Owner

@AntoineABI I think the solution here is going to be to improve the merge logic between custom/"provided" spec and the generated spec; ideally you should be able to override the generated spec when you need to.

If you're inclined to take a stab at a solution, the merge is happening here: https://github.com/lukeautry/tsoa/blob/master/src/swagger/specGenerator.ts#L47

We'd just need to do something custom there instead of using Object.assign, or it may be as easy as swapping the source/destination.

@AntoineABI
Copy link
Contributor Author

AntoineABI commented Jan 17, 2017

@lukeautry That's whats I though ! We could use something like https://www.npmjs.com/package/merge which do exactly what I was expected.

@AntoineABI
Copy link
Contributor Author

AntoineABI commented Jan 17, 2017

made PR #45

@lukeautry
Copy link
Owner

@AntoineABI This is live in 1.0.14.

@AmazingTurtle
Copy link
Contributor

AmazingTurtle commented Jan 15, 2018

I also found a workaround for manually doing the routes. Basically you can get the response from the request object in express. Cast like this: const response = (<any>request).res as express.Response.

I'm using a custom routes template, to do nothing if the promise returns null explicitly.
Then I just overwrite the swagger "generates" part for the specific route.

This is how I return binary data like that.

    @Security('jwt')
    @Get('getAttachment/{attachmentId}')
    public async getAttachment(@Request() request: express.Request,
                               @Path() attachmentId: string) {
        const response = (<any>request).res as express.Response;

        console.log((<any>request).res, (<any>request).res === <any>response);

        const attachment = await this.attachmentService.getAttachment(attachmentId);
        if (attachment.found) {
            response.end(new Buffer(attachment._source.data, 'base64'));
        }
        return null;
        
    }

@alokrajiv
Copy link

ok, am I missing something here? The exact same code that works on a typical express response object, fails with no warning on tsoa.

Plain Express (this works):

router.get("/test", (req: express.Request) => {
  fs.createReadStream("/path/to/test.jpg").pipe(req.res);
});

Tsoa (doesn't work - 204 No Content):

@Get()
public view(@Request() req: express.Request ) {
  fs.createReadStream("/path/to/test.jpg").pipe(req.res);
}

@AmazingTurtle Do you think there anything happening under the hood that's making the difference?

@dgreene1
Copy link
Collaborator

dgreene1 commented Jul 9, 2019

@alokrajiv, while visually scanning your two examples, I noticed that you’re missing a return statement in the second example since it’s not a “fat arrow” function like you have in your express example.

Side note: Since tsoa was created for typed data responses, maybe returning a jpeg is not the correct use of tsoa. Forgive me if that was just an example. But if it isn’t, might I recommend uploading your images to Amazon S3 (or any static file service) and serving them from there?

@alokrajiv
Copy link

alokrajiv commented Jul 9, 2019

@dgreene1 I've tried these too.

@Get()
public view(@Request() req: express.Request ) {
  fs.createReadStream("/path/to/test.jpg").pipe(req.res);
  return;
}
@Get()
public view(@Request() req: express.Request ) {
  const mystream = fs.createReadStream("/path/to/test.jpg");
  mystream.on("end", () => req.res.end());
  mystream.pipe(req.res);
  return;
}

None seems to be able to get it across. How are usually files served in tsoa at the moment? Even if its not a stream? (though i prefer a stream, because I'm pulling this data from an upstream server that is picking the file from S3/GridFS. So, streams would have been ideal to decrease footprint and make it fast)

@dgreene1
Copy link
Collaborator

dgreene1 commented Jul 9, 2019

Forgive me for not seeing this earlier, but it actually has nothing to do with the return. I believe the code you’re writing (and this is a guess, just like my previous guess) is asynchronous because you’re using .on. So I would recommend using res.end( like in @AmazingTurtle ‘s example.

@alokrajiv
Copy link

@dgreene1 Interestingly, you see my second example I'm already using req.res.end.

But, you are right that, it is async because of its a stream after all, so I can't use promises. Maybe, I need to manually return a promise when the end event hits on the stream. I'll update here if I am able to crack it. Thanks anyways.

@alokrajiv
Copy link

So, the soln is exactly that. The stream is being piped continuously but is not waited on. So, before my end hits from the stream's end event, tsoa's generated controller issues its own response.status(statusCode || 204).end(); inside promiseHandler of its auto-generated routes.jsAdjust for that and everything works. Viola!

const mystream = fs.createReadStream("/path/to/test.jpg");
mystream.pipe(req.res);
await new Promise((resolve, reject) => {
  mystream.on("end", () => {
    req.res.end();
    resolve();
  });
});

Fyi, to anyone coming here for a similar issue: the above code is quite simple and doesn't handle any edge case handling of the stream itself. Its only to demonstrate and fix the usage in this context. If you are really implementing, might want to consider- pausing the stream first, pipe to res only on success, then resume, and resolve the promise only on end event ( or of course reject the promise on error events). I'm not doing that in the example in the interest of simplicity :).

@stories2
Copy link

stories2 commented Mar 11, 2020

So, the soln is exactly that. The stream is being piped continuously but is not waited on. So, before my end hits from the stream's end event, tsoa's generated controller issues its own response.status(statusCode || 204).end(); inside promiseHandler of its auto-generated routes.jsAdjust for that and everything works. Viola!

const mystream = fs.createReadStream("/path/to/test.jpg");
mystream.pipe(req.res);
await new Promise((resolve, reject) => {
  mystream.on("end", () => {
    req.res.end();
    resolve();
  });
});

Fyi, to anyone coming here for a similar issue: the above code is quite simple and doesn't handle any edge case handling of the stream itself. Its only to demonstrate and fix the usage in this context. If you are really implementing, might want to consider- pausing the stream first, pipe to res only on success, then resume, and resolve the promise only on end event ( or of course reject the promise on error events). I'm not doing that in the example in the interest of simplicity :).

@alokrajiv

tsoa == 2.5.13

First, ty for save my life.

In my case, must set response status 206. If not response status will set 200 automatically and response close.

@Get('/download')
    public download(@Request() request: any, @Query() rfid: number): Promise<any> {
        .
        .
        .
            response.status(HttpStatusCode.PARTIAL_CONTENT);
            response.setHeader('Content-Range', `bytes ${start}-${end}/${fileStat.size}`)
            response.setHeader('Content-Length', start === end ? 0 : (end - start) + 1);
            response.setHeader('Content-Type', resource.Type);
            response.setHeader('Accept-Ranges', 'bytes');
            response.setHeader('Cache-Control', 'no-cache');
            stream = fs.createReadStream(filePath, {
                start,
                end
            });
            stream.pipe(response);
            await new Promise((resolve, reject) => {
                stream.on('end', () => {
                    response.end();
                    resolve();
                })
            })
        .
        .
        .
    }

This link is my partial content response reference.

@cjam
Copy link

cjam commented Apr 2, 2020

I struggled with this one for quite a while. Found the root of the problem for me to be here:

if (data || data === false) {

The automatic setting of the status resets many of the properties on the response including header and length, also resets body to null in the case of the 204. I wanted a way that I could leverage existing libraries like koa-send so this is the solution that I ended on which feels like it could potentially be a simple enhancement for tsoa.

In my Controller Base which inherits from TSOA's I included a simple get and set for bypassing default handling logic:

    private _bypassDefaultHandling:boolean = false
    get bypassDefaultHandling():boolean{
        return this._bypassDefaultHandling;
    }

    set bypassDefaultHandling(val:boolean){
        this._bypassDefaultHandling = val;
    }

In the promiseHandler in the route template I just check to see if the default handling should be bypassed for this request:

function shouldBypassHandling(controller:any){
    return 'bypassDefaultHandling' in controller && controller.bypassDefaultHandling;
}

export function promiseHandler(controllerObj: any, promise: Promise<any>, context: any, next: () => Promise<any>) {
    return Promise.resolve(promise)
      .then((data: any) => {
          if(shouldBypassHandling(controllerObj)){
              return next();
          }

          if (data || data === false) {
              context.body = data;
              context.status = 200;
          } else {
              context.status = 204;
          }

          if (isController(controllerObj)) {
              const headers = controllerObj.getHeaders();
              Object.keys(headers).forEach((name: string) => {
                  context.set(name, headers[name]);
              });

              const statusCode = controllerObj.getStatus();
              if (statusCode) {
                  context.status = statusCode;
              }
          }
          return next();
      })
      .catch((error: any) => {
          context.status = error.status || 500;
          context.body = error;
          return next();
      });
  }

Which allows me to handle files like you normally would with vanilla koa which is kind of nice: (note: send is via https://github.com/koajs/send)

    @Get('/test/{testId}/results')
    public async downloadPdf(testId: string, @Request() request: KoaRequest) {
        this.bypassDefaultHandling = true;
        await send(request.ctx,"some-file.pdf",{
            root:"/somedirectory/",
        });
    }

I've ran into a couple places where I think bypassing the default behavior could be useful, but even just for a way to support files in a clean way is nice.

@sundowndev
Copy link

sundowndev commented Sep 29, 2020

Before using tsoa all I had to do was

res.download('/path/to/file');

But

this.setStatus(200);

req.res.download(filePath, filename);

does not seem to work. #44 (comment) doesn't seem to work either.

EDIT1: this is not working for me because req.res is undefined.

@mb21
Copy link

mb21 commented Jul 25, 2022

Since we have this line in the express template now, you can simple return a Readable stream in your controller. However, a Buffer will not work.

Still need to do e.g. this.setHeader('Content-Type', 'image/png') though.

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

No branches or pull requests

9 participants