Skip to content

Commit 848be9d

Browse files
climba03003Uzlopak
andauthored
feat: replace into-stream to Readable.from (#290)
* feat: replace into-stream to Readable.from * test: regression test of issue 288 * fixup * feat: support more types * chore: fix deps * fixup * fixup * fixup * fixup * refactor: update checking condition * fixup * chore: apply suggestion Co-authored-by: Aras Abbasi <[email protected]> Signed-off-by: KaKa <[email protected]> * fixup * fixup --------- Signed-off-by: KaKa <[email protected]> Co-authored-by: Aras Abbasi <[email protected]>
1 parent 593e2d8 commit 848be9d

File tree

6 files changed

+156
-17
lines changed

6 files changed

+156
-17
lines changed

index.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@ const fp = require('fastify-plugin')
77
const encodingNegotiator = require('@fastify/accept-negotiator')
88
const pump = require('pump')
99
const mimedb = require('mime-db')
10-
const intoStream = require('into-stream')
1110
const peek = require('peek-stream')
1211
const { Minipass } = require('minipass')
1312
const pumpify = require('pumpify')
13+
const { Readable } = require('readable-stream')
1414

15-
const { isStream, isGzip, isDeflate } = require('./lib/utils')
15+
const { isStream, isGzip, isDeflate, intoAsyncIterator } = require('./lib/utils')
1616

1717
const InvalidRequestEncodingError = createError('FST_CP_ERR_INVALID_CONTENT_ENCODING', 'Unsupported Content-Encoding: %s', 415)
1818
const InvalidRequestCompressedPayloadError = createError('FST_CP_ERR_INVALID_CONTENT', 'Could not decompress the request payload using the provided encoding', 400)
@@ -276,7 +276,7 @@ function buildRouteCompress (fastify, params, routeOptions, decorateOnly) {
276276
if (Buffer.byteLength(payload) < params.threshold) {
277277
return next()
278278
}
279-
payload = intoStream(payload)
279+
payload = Readable.from(intoAsyncIterator(payload))
280280
}
281281

282282
setVaryHeader(reply)
@@ -400,7 +400,7 @@ function compress (params) {
400400
if (Buffer.byteLength(payload) < params.threshold) {
401401
return this.send(payload)
402402
}
403-
payload = intoStream(payload)
403+
payload = Readable.from(intoAsyncIterator(payload))
404404
}
405405

406406
setVaryHeader(this)
@@ -509,7 +509,7 @@ function maybeUnzip (payload, serialize) {
509509
// handle case where serialize doesn't return a string or Buffer
510510
if (!Buffer.isBuffer(buf)) return result
511511
if (isCompressed(buf) === 0) return result
512-
return intoStream(result)
512+
return Readable.from(intoAsyncIterator(result))
513513
}
514514

515515
function zipStream (deflate, encoding) {

lib/utils.js

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,45 @@ function isStream (stream) {
3535
return stream !== null && typeof stream === 'object' && typeof stream.pipe === 'function'
3636
}
3737

38-
module.exports = { isGzip, isDeflate, isStream }
38+
/**
39+
* Provide a async iteratable for Readable.from
40+
*/
41+
async function * intoAsyncIterator (payload) {
42+
if (typeof payload === 'object') {
43+
if (Buffer.isBuffer(payload)) {
44+
yield payload
45+
return
46+
}
47+
48+
if (
49+
// ArrayBuffer
50+
payload instanceof ArrayBuffer ||
51+
// NodeJS.TypedArray
52+
ArrayBuffer.isView(payload)
53+
) {
54+
yield Buffer.from(payload)
55+
return
56+
}
57+
58+
// Iterator
59+
if (Symbol.iterator in payload) {
60+
for (const chunk of payload) {
61+
yield chunk
62+
}
63+
return
64+
}
65+
66+
// Async Iterator
67+
if (Symbol.asyncIterator in payload) {
68+
for await (const chunk of payload) {
69+
yield chunk
70+
}
71+
return
72+
}
73+
}
74+
75+
// string
76+
yield payload
77+
}
78+
79+
module.exports = { isGzip, isDeflate, isStream, intoAsyncIterator }

package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@
88
"dependencies": {
99
"@fastify/accept-negotiator": "^1.1.0",
1010
"fastify-plugin": "^4.5.0",
11-
"into-stream": "^6.0.0",
1211
"mime-db": "^1.52.0",
1312
"minipass": "^7.0.2",
1413
"peek-stream": "^1.1.3",
1514
"pump": "^3.0.0",
16-
"pumpify": "^2.0.1"
15+
"pumpify": "^2.0.1",
16+
"readable-stream": "^4.5.2"
1717
},
1818
"devDependencies": {
1919
"@fastify/pre-commit": "^2.0.2",
@@ -26,7 +26,8 @@
2626
"standard": "^17.1.0",
2727
"tap": "^16.3.7",
2828
"tsd": "^0.30.0",
29-
"typescript": "^5.1.6"
29+
"typescript": "^5.1.6",
30+
"undici": "^5.28.3"
3031
},
3132
"scripts": {
3233
"coverage": "npm run test:unit -- --coverage-report=html",

test/issue-288.test.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
'use strict'
2+
3+
const { test } = require('tap')
4+
const Fastify = require('fastify')
5+
const fastifyCompress = require('..')
6+
const { request, setGlobalDispatcher, Agent } = require('undici')
7+
8+
setGlobalDispatcher(new Agent({
9+
keepAliveTimeout: 10,
10+
keepAliveMaxTimeout: 10
11+
}))
12+
13+
test('should not corrupt the file content', async (t) => {
14+
// provide 2 byte unicode content
15+
const twoByteUnicodeContent = new Array(5_000)
16+
.fill('0')
17+
.map(() => {
18+
const random = new Array(10).fill('A').join('🍃')
19+
return random + '- FASTIFY COMPRESS,🍃 FASTIFY COMPRESS'
20+
})
21+
.join('\n')
22+
const fastify = new Fastify()
23+
t.teardown(() => fastify.close())
24+
25+
fastify.register(async (instance, opts) => {
26+
await fastify.register(fastifyCompress)
27+
// compression
28+
instance.get('/issue', async (req, reply) => {
29+
return twoByteUnicodeContent
30+
})
31+
})
32+
33+
// no compression
34+
fastify.get('/good', async (req, reply) => {
35+
return twoByteUnicodeContent
36+
})
37+
38+
await fastify.listen({ port: 0 })
39+
40+
const { port } = fastify.server.address()
41+
const url = `http://localhost:${port}`
42+
43+
const response = await request(`${url}/issue`)
44+
const response2 = await request(`${url}/good`)
45+
const body = await response.body.text()
46+
const body2 = await response2.body.text()
47+
t.equal(body, body2)
48+
})

test/utils.test.js

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ const { createReadStream } = require('node:fs')
44
const { Socket } = require('node:net')
55
const { Duplex, PassThrough, Readable, Stream, Transform, Writable } = require('node:stream')
66
const { test } = require('tap')
7-
const { isStream, isDeflate, isGzip } = require('../lib/utils')
7+
const { isStream, isDeflate, isGzip, intoAsyncIterator } = require('../lib/utils')
88

99
test('isStream() utility should be able to detect Streams', async (t) => {
1010
t.plan(12)
@@ -61,3 +61,45 @@ test('isGzip() utility should be able to detect gzip compressed Buffer', async (
6161
t.equal(isGzip(undefined), false)
6262
t.equal(isGzip(''), false)
6363
})
64+
65+
test('intoAsyncIterator() utility should handle different data', async (t) => {
66+
t.plan(8)
67+
68+
const buf = Buffer.from('foo')
69+
const str = 'foo'
70+
const arr = [str, str]
71+
const arrayBuffer = new ArrayBuffer(8)
72+
const typedArray = new Int32Array(arrayBuffer)
73+
const asyncIterator = (async function * () {
74+
yield str
75+
})()
76+
const obj = {}
77+
78+
for await (const buffer of intoAsyncIterator(buf)) {
79+
t.equal(buffer, buf)
80+
}
81+
82+
for await (const string of intoAsyncIterator(str)) {
83+
t.equal(string, str)
84+
}
85+
86+
for await (const chunk of intoAsyncIterator(arr)) {
87+
t.equal(chunk, str)
88+
}
89+
90+
for await (const chunk of intoAsyncIterator(arrayBuffer)) {
91+
t.equal(chunk.toString(), Buffer.from(arrayBuffer).toString())
92+
}
93+
94+
for await (const chunk of intoAsyncIterator(typedArray)) {
95+
t.equal(chunk.toString(), Buffer.from(typedArray).toString())
96+
}
97+
98+
for await (const chunk of intoAsyncIterator(asyncIterator)) {
99+
t.equal(chunk, str)
100+
}
101+
102+
for await (const chunk of intoAsyncIterator(obj)) {
103+
t.equal(chunk, obj)
104+
}
105+
})

types/index.d.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,12 @@ import {
22
FastifyPluginCallback,
33
FastifyReply,
44
FastifyRequest,
5+
RouteOptions as FastifyRouteOptions,
56
RawServerBase,
6-
RawServerDefault,
7-
RouteOptions as FastifyRouteOptions
8-
} from 'fastify'
9-
import { Input, InputObject } from 'into-stream'
10-
import { Stream } from 'stream'
11-
import { BrotliOptions, ZlibOptions } from 'zlib'
7+
RawServerDefault
8+
} from 'fastify';
9+
import { Stream } from 'stream';
10+
import { BrotliOptions, ZlibOptions } from 'zlib';
1211

1312
declare module 'fastify' {
1413
export interface FastifyContextConfig {
@@ -26,7 +25,7 @@ declare module 'fastify' {
2625
}
2726

2827
interface FastifyReply {
29-
compress(input: Stream | Input | InputObject): void;
28+
compress(input: Stream | Input): void;
3029
}
3130

3231
export interface RouteOptions {
@@ -61,6 +60,14 @@ type EncodingToken = 'br' | 'deflate' | 'gzip' | 'identity';
6160

6261
type CompressibleContentTypeFunction = (contentType: string) => boolean;
6362

63+
type Input =
64+
| Buffer
65+
| NodeJS.TypedArray
66+
| ArrayBuffer
67+
| string
68+
| Iterable<Buffer | string>
69+
| AsyncIterable<Buffer | string>;
70+
6471
declare namespace fastifyCompress {
6572

6673
export interface FastifyCompressOptions {

0 commit comments

Comments
 (0)