Skip to content

Commit b6ff09d

Browse files
authored
Merge pull request #285 from 19majkel94/feature/stream-binary-response-support
Add support for returning Buffer and streams from action
2 parents 9658808 + d72c475 commit b6ff09d

File tree

6 files changed

+130
-28
lines changed

6 files changed

+130
-28
lines changed

src/driver/BaseDriver.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {ValidatorOptions} from "class-validator";
2-
import {ClassTransformOptions} from "class-transformer";
2+
import {ClassTransformOptions, classToPlain} from "class-transformer";
33

44
import {HttpError} from "../http-error/HttpError";
55
import {CurrentUserChecker} from "../CurrentUserChecker";
@@ -127,6 +127,25 @@ export abstract class BaseDriver {
127127
// Protected Methods
128128
// -------------------------------------------------------------------------
129129

130+
protected transformResult(result: any, action: ActionMetadata, options: Action): any {
131+
// check if we need to transform result
132+
const shouldTransform = (this.useClassTransformer && result != null) // transform only if enabled and value exist
133+
&& result instanceof Object // don't transform primitive types (string/number/boolean)
134+
&& !(
135+
result instanceof Uint8Array // don't transform binary data
136+
||
137+
result.pipe instanceof Function // don't transform streams
138+
);
139+
140+
// transform result if needed
141+
if (shouldTransform) {
142+
const options = action.responseClassTransformOptions || this.classToPlainTransformOptions;
143+
result = classToPlain(result, options);
144+
}
145+
146+
return result;
147+
}
148+
130149
protected processJsonError(error: any) {
131150
if (!this.isDefaultErrorHandlingEnabled)
132151
return error;

src/driver/express/ExpressDriver.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -230,11 +230,8 @@ export class ExpressDriver extends BaseDriver {
230230
*/
231231
handleSuccess(result: any, action: ActionMetadata, options: Action): void {
232232

233-
// check if we need to transform result and do it
234-
if (result && result instanceof Object && this.useClassTransformer) {
235-
const options = action.responseClassTransformOptions || this.classToPlainTransformOptions;
236-
result = classToPlain(result, options);
237-
}
233+
// transform result if needed
234+
result = this.transformResult(result, action, options);
238235

239236
// set http status code
240237
if (result === undefined && action.undefinedResultCode && action.undefinedResultCode instanceof Function) {
@@ -301,6 +298,15 @@ export class ExpressDriver extends BaseDriver {
301298
}
302299
options.next();
303300
}
301+
else if (result instanceof Buffer) { // check if it's binary data (Buffer)
302+
options.response.end(result, "binary");
303+
}
304+
else if (result instanceof Uint8Array) { // check if it's binary data (typed array)
305+
options.response.end(Buffer.from(result as any), "binary");
306+
}
307+
else if (result.pipe instanceof Function) {
308+
result.pipe(options.response);
309+
}
304310
else { // send regular result
305311
if (action.isJsonTyped) {
306312
options.response.json(result);

src/driver/koa/KoaDriver.ts

Lines changed: 14 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -213,11 +213,8 @@ export class KoaDriver extends BaseDriver {
213213
*/
214214
handleSuccess(result: any, action: ActionMetadata, options: Action): void {
215215

216-
// check if we need to transform result and do it
217-
if (this.useClassTransformer && result && result instanceof Object) {
218-
const options = action.responseClassTransformOptions || this.classToPlainTransformOptions;
219-
result = classToPlain(result, options);
220-
}
216+
// transform result if needed
217+
result = this.transformResult(result, action, options);
221218

222219
// set http status code
223220
if (result === undefined && action.undefinedResultCode && action.undefinedResultCode instanceof Function) {
@@ -230,12 +227,7 @@ export class KoaDriver extends BaseDriver {
230227
} else if (action.successHttpCode) {
231228
options.response.status = action.successHttpCode;
232229
}
233-
234-
// apply http headers
235-
Object.keys(action.headers).forEach(name => {
236-
options.response.set(name, action.headers[name]);
237-
});
238-
230+
239231
if (action.redirect) { // if redirect is set then do it
240232
if (typeof result === "string") {
241233
options.response.redirect(result);
@@ -244,17 +236,12 @@ export class KoaDriver extends BaseDriver {
244236
} else {
245237
options.response.redirect(action.redirect);
246238
}
247-
248-
return options.next();
249-
250239
} else if (action.renderedTemplate) { // if template is set then render it // TODO: not working in koa
251240
const renderOptions = result && result instanceof Object ? result : {};
252-
241+
253242
this.koa.use(async function (ctx: any, next: any) {
254243
await ctx.render(action.renderedTemplate, renderOptions);
255244
});
256-
257-
return options.next();
258245
}
259246
else if (result === undefined) { // throw NotFoundError on undefined response
260247
const notFoundError = new NotFoundError();
@@ -277,18 +264,24 @@ export class KoaDriver extends BaseDriver {
277264
} else {
278265
options.response.status = 204;
279266
}
280-
281-
return options.next();
267+
}
268+
else if (result instanceof Uint8Array) { // check if it's binary data (typed array)
269+
options.response.body = Buffer.from(result as any);
282270
}
283271
else { // send regular result
284272
if (result instanceof Object) {
285273
options.response.body = result;
286274
} else {
287275
options.response.body = result;
288276
}
289-
290-
return options.next();
291277
}
278+
279+
// apply http headers
280+
Object.keys(action.headers).forEach(name => {
281+
options.response.set(name, action.headers[name]);
282+
});
283+
284+
return options.next();
292285
}
293286

294287
/**

test/functional/action-params.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -412,7 +412,7 @@ describe("action parameters", () => {
412412
});
413413
assertRequest([3002], "get", "state", response => {
414414
expect(response).to.be.status(200);
415-
expect(response).to.have.header("content-type", "application/json; charset=utf-8");
415+
expect(response).to.have.header("content-type", "application/json");
416416
expect(response.body.username).to.be.equal("pleerock");
417417
});
418418
assertRequest([3002], "get", "state/username", response => {
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import "reflect-metadata";
2+
3+
import {createReadStream} from "fs";
4+
import * as path from "path";
5+
import {createExpressServer, createKoaServer, getMetadataArgsStorage} from "../../src/index";
6+
import {assertRequest} from "./test-utils";
7+
import {InterceptorInterface} from "../../src/InterceptorInterface";
8+
import {Interceptor} from "../../src/decorator/Interceptor";
9+
import {UseInterceptor} from "../../src/decorator/UseInterceptor";
10+
import {JsonController} from "../../src/decorator/JsonController";
11+
import {Get} from "../../src/decorator/Get";
12+
import {Action} from "../../src/Action";
13+
import {ContentType} from "../../src/decorator/ContentType";
14+
const chakram = require("chakram");
15+
const expect = chakram.expect;
16+
17+
describe("special result value treatment", () => {
18+
19+
const rawData = [0xFF, 0x66, 0xAA, 0xCC];
20+
21+
before(() => {
22+
23+
// reset metadata args storage
24+
getMetadataArgsStorage().reset();
25+
26+
@JsonController()
27+
class HandledController {
28+
29+
@Get("/stream")
30+
@ContentType("text/plain")
31+
getStream() {
32+
return createReadStream(path.resolve(__dirname, "../../../../test/resources/sample-text-file.txt"));
33+
}
34+
35+
@Get("/buffer")
36+
@ContentType("application/octet-stream")
37+
getBuffer() {
38+
return new Buffer(rawData);
39+
}
40+
41+
@Get("/array")
42+
@ContentType("application/octet-stream")
43+
getUIntArray() {
44+
return new Uint8Array(rawData);
45+
}
46+
47+
}
48+
49+
});
50+
51+
let expressApp: any, koaApp: any;
52+
before(done => expressApp = createExpressServer().listen(3001, done));
53+
after(done => expressApp.close(done));
54+
before(done => koaApp = createKoaServer().listen(3002, done));
55+
after(done => koaApp.close(done));
56+
57+
describe("should pipe stream to response", () => {
58+
assertRequest([3001, 3002], "get", "stream", response => {
59+
expect(response).to.be.status(200);
60+
expect(response).to.have.header("content-type", (contentType: string) => {
61+
expect(contentType).to.match(/text\/plain/);
62+
});
63+
expect(response.body).to.be.equal("Hello World!");
64+
});
65+
});
66+
67+
describe("should send raw binary data from Buffer", () => {
68+
assertRequest([3001, 3002], "get", "buffer", response => {
69+
expect(response).to.be.status(200);
70+
expect(response).to.have.header("content-type", "application/octet-stream");
71+
expect(response.body).to.be.equal(new Buffer(rawData).toString());
72+
});
73+
});
74+
75+
describe("should send raw binary data from UIntArray", () => {
76+
assertRequest([3001, 3002], "get", "array", response => {
77+
expect(response).to.be.status(200);
78+
expect(response).to.have.header("content-type", "application/octet-stream");
79+
expect(response.body).to.be.equal(Buffer.from(rawData).toString());
80+
});
81+
});
82+
83+
});

test/resources/sample-text-file.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Hello World!

0 commit comments

Comments
 (0)