Skip to content

Commit c8ff569

Browse files
committed
feat(common): extend streamable-file header support
Provide option to specify additional headers when using StreamableFile. No need to access the native response option. BREAKING CHANGE: not specifying content-disposition header and StreamableFile option disposition would send header value 'null' instead of not sending the header at all. This changes to not sending the header if no value is specified. This commit closes issue nestjs#9229.
1 parent 4a45709 commit c8ff569

8 files changed

Lines changed: 134 additions & 11 deletions

File tree

integration/send-files/e2e/express.spec.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ import { join } from 'path';
99
import * as request from 'supertest';
1010
import { AppModule } from '../src/app.module';
1111

12-
const readmeString = readFileSync(join(process.cwd(), 'Readme.md')).toString();
12+
const readme = readFileSync(join(process.cwd(), 'Readme.md'));
13+
const readmeString = readme.toString();
1314

1415
describe('Express FileSend', () => {
1516
let app: NestExpressApplication;
@@ -53,12 +54,18 @@ describe('Express FileSend', () => {
5354
expect(res.body.toString()).to.be.eq(readmeString);
5455
});
5556
});
56-
it('should return a file with correct content type and disposition', async () => {
57+
it('should return a file with correct headers', async () => {
5758
return request(app.getHttpServer())
5859
.get('/file/with/headers')
5960
.expect(200)
6061
.expect('Content-Type', 'text/markdown')
6162
.expect('Content-Disposition', 'attachment; filename="Readme.md"')
63+
.expect('Content-Length', readme.byteLength.toString())
64+
.expect('Accept-Ranges', 'bytes')
65+
.expect(
66+
'Content-Range',
67+
`bytes 0-${readme.byteLength - 1}/${readme.byteLength}`,
68+
)
6269
.expect(res => {
6370
expect(res.text).to.be.eq(readmeString);
6471
});

integration/send-files/e2e/fastify.spec.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@ import {
44
} from '@nestjs/platform-fastify';
55
import { Test } from '@nestjs/testing';
66
import { expect } from 'chai';
7-
import { readFileSync } from 'fs';
7+
import { read, readFileSync } from 'fs';
88
import { join } from 'path';
99
import { AppModule } from '../src/app.module';
1010

11-
const readmeString = readFileSync(join(process.cwd(), 'Readme.md')).toString();
11+
const readme = readFileSync(join(process.cwd(), 'Readme.md'));
12+
const readmeString = readme.toString();
1213

1314
describe('Fastify FileSend', () => {
1415
let app: NestFastifyApplication;
@@ -67,4 +68,21 @@ describe('Fastify FileSend', () => {
6768
expect(payload.toString()).to.be.eq(readmeString);
6869
});
6970
});
71+
it('should return a file with correct headers', async () => {
72+
return app
73+
.inject({ url: '/file/with/headers', method: 'get' })
74+
.then(({ statusCode, headers, payload }) => {
75+
expect(statusCode).to.equal(200);
76+
expect(headers['content-type']).to.equal('text/markdown');
77+
expect(headers['content-disposition']).to.equal(
78+
'attachment; filename="Readme.md"',
79+
);
80+
expect(headers['content-length']).to.equal(readme.byteLength);
81+
expect(headers['accept-ranges']).to.equal('bytes');
82+
expect(headers['content-range']).to.equal(
83+
`bytes 0-${readme.byteLength - 1}/${readme.byteLength}`,
84+
);
85+
expect(payload).to.equal(readmeString);
86+
});
87+
});
7088
});

integration/send-files/src/app.service.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,15 @@ export class AppService {
2525
}
2626

2727
getFileWithHeaders(): StreamableFile {
28+
const file = readFileSync(join(process.cwd(), 'Readme.md'));
2829
return new StreamableFile(
2930
createReadStream(join(process.cwd(), 'Readme.md')),
3031
{
3132
type: 'text/markdown',
3233
disposition: 'attachment; filename="Readme.md"',
34+
length: file.byteLength,
35+
acceptRanges: 'bytes',
36+
range: `bytes 0-${file.byteLength - 1}/${file.byteLength}`,
3337
},
3438
);
3539
}

packages/common/file-stream/streamable-file.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,19 @@ export class StreamableFile {
2626
}
2727

2828
getHeaders() {
29-
const { type = 'application/octet-stream', disposition = null } =
30-
this.options;
31-
return { type, disposition };
29+
const {
30+
type = 'application/octet-stream',
31+
disposition = undefined,
32+
acceptRanges = undefined,
33+
length = undefined,
34+
range = undefined,
35+
} = this.options;
36+
return {
37+
type,
38+
disposition,
39+
acceptRanges,
40+
length,
41+
range,
42+
};
3243
}
3344
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
export interface StreamableFileOptions {
22
type?: string;
33
disposition?: string;
4+
length?: number;
5+
range?: string;
6+
acceptRanges?: string;
47
}

packages/common/test/file-stream/streamable-file.spec.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,38 @@ describe('StreamableFile', () => {
1717
expect(streamableFile.getStream()).to.equal(stream);
1818
});
1919
});
20+
describe('when options is empty', () => {
21+
it('should return application/octet-stream for type and undefined for others', () => {
22+
const stream = new Readable();
23+
const streamableFile = new StreamableFile(stream);
24+
expect(streamableFile.getHeaders()).to.deep.equal({
25+
type: 'application/octet-stream',
26+
disposition: undefined,
27+
acceptRanges: undefined,
28+
length: undefined,
29+
range: undefined,
30+
});
31+
});
32+
});
33+
describe('when options is defined', () => {
34+
it('should pass provided headers', () => {
35+
const stream = new Readable();
36+
const streamableFile = new StreamableFile(stream, {
37+
type: 'application/pdf',
38+
acceptRanges: '123',
39+
disposition: 'inline',
40+
length: 100,
41+
range: '456',
42+
});
43+
expect(streamableFile.getHeaders()).to.deep.equal({
44+
type: 'application/pdf',
45+
acceptRanges: '123',
46+
disposition: 'inline',
47+
length: 100,
48+
range: '456',
49+
});
50+
});
51+
});
2052
describe('otherwise', () => {
2153
describe('when input is a Buffer instance', () => {
2254
it('should create a readable stream and push the input buffer', () => {

packages/platform-express/adapters/express-adapter.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,36 @@ export class ExpressAdapter extends AbstractHttpAdapter {
4848
}
4949
if (body instanceof StreamableFile) {
5050
const streamHeaders = body.getHeaders();
51-
if (response.getHeader('Content-Type') === undefined) {
51+
if (
52+
response.getHeader('Content-Type') === undefined &&
53+
streamHeaders.type
54+
) {
5255
response.setHeader('Content-Type', streamHeaders.type);
5356
}
54-
if (response.getHeader('Content-Disposition') === undefined) {
57+
if (
58+
response.getHeader('Content-Disposition') === undefined &&
59+
streamHeaders.disposition
60+
) {
5561
response.setHeader('Content-Disposition', streamHeaders.disposition);
5662
}
63+
if (
64+
response.getHeader('Content-Length') === undefined &&
65+
streamHeaders.length
66+
) {
67+
response.setHeader('Content-Length', streamHeaders.length);
68+
}
69+
if (
70+
response.getHeader('Content-Range') === undefined &&
71+
streamHeaders.range
72+
) {
73+
response.setHeader('Content-Range', streamHeaders.range);
74+
}
75+
if (
76+
response.getHeader('Accept-Ranges') === undefined &&
77+
streamHeaders.acceptRanges
78+
) {
79+
response.setHeader('Accept-Ranges', streamHeaders.acceptRanges);
80+
}
5781
return body.getStream().pipe(response);
5882
}
5983
return isObject(body) ? response.json(body) : response.send(String(body));

packages/platform-fastify/adapters/fastify-adapter.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -282,12 +282,36 @@ export class FastifyAdapter<
282282
}
283283
if (body instanceof StreamableFile) {
284284
const streamHeaders = body.getHeaders();
285-
if (fastifyReply.getHeader('Content-Type') === undefined) {
285+
if (
286+
fastifyReply.getHeader('Content-Type') === undefined &&
287+
streamHeaders.type
288+
) {
286289
fastifyReply.header('Content-Type', streamHeaders.type);
287290
}
288-
if (fastifyReply.getHeader('Content-Disposition') === undefined) {
291+
if (
292+
fastifyReply.getHeader('Content-Disposition') === undefined &&
293+
streamHeaders.disposition
294+
) {
289295
fastifyReply.header('Content-Disposition', streamHeaders.disposition);
290296
}
297+
if (
298+
fastifyReply.getHeader('Content-Length') === undefined &&
299+
streamHeaders.length
300+
) {
301+
fastifyReply.header('Content-Length', streamHeaders.length);
302+
}
303+
if (
304+
fastifyReply.getHeader('Content-Range') === undefined &&
305+
streamHeaders.range
306+
) {
307+
fastifyReply.header('Content-Range', streamHeaders.range);
308+
}
309+
if (
310+
fastifyReply.getHeader('Accept-Ranges') === undefined &&
311+
streamHeaders.acceptRanges
312+
) {
313+
fastifyReply.header('Accept-Ranges', streamHeaders.acceptRanges);
314+
}
291315
body = body.getStream();
292316
}
293317
return fastifyReply.send(body);

0 commit comments

Comments
 (0)