1
- import type { Readable } from 'node:stream' ;
2
- import {
3
- decompressedResponseMap ,
4
- getSupportedEncodings ,
5
- isAsyncIterable ,
6
- isReadable ,
7
- } from '../utils.js' ;
1
+ import { decompressedResponseMap , getSupportedEncodings } from '../utils.js' ;
8
2
import type { ServerAdapterPlugin } from './types.js' ;
9
3
4
+ const emptyEncodings = [ 'none' , 'identity' ] ;
5
+
10
6
export function useContentEncoding < TServerContext > ( ) : ServerAdapterPlugin < TServerContext > {
11
- const encodingMap = new WeakMap < Request , string [ ] > ( ) ;
12
7
return {
13
8
onRequest ( { request, setRequest, fetchAPI, endResponse } ) {
14
- if ( request . body ) {
15
- const contentEncodingHeader = request . headers . get ( 'content-encoding' ) ;
16
- if ( contentEncodingHeader && contentEncodingHeader !== 'none' ) {
17
- const contentEncodings = contentEncodingHeader ?. split ( ',' ) ;
9
+ const contentEncodingHeader = request . headers . get ( 'content-encoding' ) ;
10
+ if (
11
+ contentEncodingHeader &&
12
+ contentEncodingHeader !== 'none' &&
13
+ contentEncodingHeader !== 'identity' &&
14
+ request . body
15
+ ) {
16
+ const contentEncodings = contentEncodingHeader
17
+ . split ( ',' )
18
+ . filter ( encoding => ! emptyEncodings . includes ( encoding ) ) as CompressionFormat [ ] ;
19
+ if ( contentEncodings . length ) {
18
20
if (
19
- ! contentEncodings . every ( encoding =>
20
- getSupportedEncodings ( fetchAPI ) . includes ( encoding as CompressionFormat ) ,
21
- )
21
+ ! contentEncodings . every ( encoding => getSupportedEncodings ( fetchAPI ) . includes ( encoding ) )
22
22
) {
23
23
endResponse (
24
24
new fetchAPI . Response ( `Unsupported 'Content-Encoding': ${ contentEncodingHeader } ` , {
@@ -30,83 +30,46 @@ export function useContentEncoding<TServerContext>(): ServerAdapterPlugin<TServe
30
30
}
31
31
let newBody = request . body ;
32
32
for ( const contentEncoding of contentEncodings ) {
33
- newBody = newBody . pipeThrough (
34
- new fetchAPI . DecompressionStream ( contentEncoding as CompressionFormat ) ,
35
- ) ;
33
+ newBody = request . body . pipeThrough ( new fetchAPI . DecompressionStream ( contentEncoding ) ) ;
36
34
}
37
- request = new fetchAPI . Request ( request . url , {
38
- body : newBody ,
39
- cache : request . cache ,
40
- credentials : request . credentials ,
41
- headers : request . headers ,
42
- integrity : request . integrity ,
43
- keepalive : request . keepalive ,
44
- method : request . method ,
45
- mode : request . mode ,
46
- redirect : request . redirect ,
47
- referrer : request . referrer ,
48
- referrerPolicy : request . referrerPolicy ,
49
- signal : request . signal ,
50
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
51
- // @ts -ignore - not in the TS types yet
52
- duplex : 'half' ,
53
- } ) ;
54
- setRequest ( request ) ;
35
+ setRequest (
36
+ new fetchAPI . Request ( request . url , {
37
+ body : newBody ,
38
+ cache : request . cache ,
39
+ credentials : request . credentials ,
40
+ headers : request . headers ,
41
+ integrity : request . integrity ,
42
+ keepalive : request . keepalive ,
43
+ method : request . method ,
44
+ mode : request . mode ,
45
+ redirect : request . redirect ,
46
+ referrer : request . referrer ,
47
+ referrerPolicy : request . referrerPolicy ,
48
+ signal : request . signal ,
49
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
50
+ // @ts -ignore - not in the TS types yet
51
+ duplex : 'half' ,
52
+ } ) ,
53
+ ) ;
55
54
}
56
55
}
56
+ } ,
57
+ onResponse ( { request, response, setResponse, fetchAPI } ) {
57
58
const acceptEncoding = request . headers . get ( 'accept-encoding' ) ;
58
59
if ( acceptEncoding ) {
59
- encodingMap . set ( request , acceptEncoding . split ( ',' ) ) ;
60
- }
61
- } ,
62
- onResponse ( { request, response, setResponse, fetchAPI, serverContext } ) {
63
- // Hack for avoiding to create whatwg-node to create a readable stream until it's needed
64
- if ( ( response as any ) [ 'bodyInit' ] || response . body ) {
65
- const encodings = encodingMap . get ( request ) ;
66
- if ( encodings ) {
60
+ const encodings = acceptEncoding . split ( ',' ) as CompressionFormat [ ] ;
61
+ if ( encodings . length && response . body ) {
67
62
const supportedEncoding = encodings . find ( encoding =>
68
- getSupportedEncodings ( fetchAPI ) . includes ( encoding as CompressionFormat ) ,
63
+ getSupportedEncodings ( fetchAPI ) . includes ( encoding ) ,
69
64
) ;
70
65
if ( supportedEncoding ) {
71
66
const compressionStream = new fetchAPI . CompressionStream (
72
67
supportedEncoding as CompressionFormat ,
73
68
) ;
74
- // To calculate final content-length
75
- const contentLength = response . headers . get ( 'content-length' ) ;
76
- if ( contentLength ) {
77
- const bufOfRes = ( response as any ) . _buffer ;
78
- if ( bufOfRes ) {
79
- const writer = compressionStream . writable . getWriter ( ) ;
80
- const write$ = writer . write ( bufOfRes ) ;
81
- serverContext . waitUntil ?.( write$ ) ;
82
- const close$ = writer . close ( ) ;
83
- serverContext . waitUntil ?.( close$ ) ;
84
- const uint8Arrays$ = isReadable ( ( compressionStream . readable as any ) [ 'readable' ] )
85
- ? collectReadableValues ( ( compressionStream . readable as any ) [ 'readable' ] )
86
- : isAsyncIterable ( compressionStream . readable )
87
- ? collectAsyncIterableValues ( compressionStream . readable )
88
- : collectReadableStreamValues ( compressionStream . readable ) ;
89
- return uint8Arrays$ . then ( uint8Arrays => {
90
- const chunks = uint8Arrays . flatMap ( uint8Array => [ ...uint8Array ] ) ;
91
- const uint8Array = new Uint8Array ( chunks ) ;
92
- const newHeaders = new fetchAPI . Headers ( response . headers ) ;
93
- newHeaders . set ( 'content-encoding' , supportedEncoding ) ;
94
- newHeaders . set ( 'content-length' , uint8Array . byteLength . toString ( ) ) ;
95
- const compressedResponse = new fetchAPI . Response ( uint8Array , {
96
- ...response ,
97
- headers : newHeaders ,
98
- } ) ;
99
- decompressedResponseMap . set ( compressedResponse , response ) ;
100
- setResponse ( compressedResponse ) ;
101
- const close$ = compressionStream . writable . close ( ) ;
102
- serverContext . waitUntil ?.( close$ ) ;
103
- } ) ;
104
- }
105
- }
106
69
const newHeaders = new fetchAPI . Headers ( response . headers ) ;
107
70
newHeaders . set ( 'content-encoding' , supportedEncoding ) ;
108
71
newHeaders . delete ( 'content-length' ) ;
109
- const compressedBody = response . body ! . pipeThrough ( compressionStream ) ;
72
+ const compressedBody = response . body . pipeThrough ( compressionStream ) ;
110
73
const compressedResponse = new fetchAPI . Response ( compressedBody , {
111
74
status : response . status ,
112
75
statusText : response . statusText ,
@@ -120,35 +83,3 @@ export function useContentEncoding<TServerContext>(): ServerAdapterPlugin<TServe
120
83
} ,
121
84
} ;
122
85
}
123
-
124
- function collectReadableValues < T > ( readable : Readable ) : Promise < T [ ] > {
125
- const values : T [ ] = [ ] ;
126
- readable . on ( 'data' , value => values . push ( value ) ) ;
127
- return new Promise ( ( resolve , reject ) => {
128
- readable . once ( 'end' , ( ) => resolve ( values ) ) ;
129
- readable . once ( 'error' , reject ) ;
130
- } ) ;
131
- }
132
-
133
- async function collectAsyncIterableValues < T > ( asyncIterable : AsyncIterable < T > ) : Promise < T [ ] > {
134
- const values : T [ ] = [ ] ;
135
- for await ( const value of asyncIterable ) {
136
- values . push ( value ) ;
137
- }
138
- return values ;
139
- }
140
-
141
- async function collectReadableStreamValues < T > ( readableStream : ReadableStream < T > ) : Promise < T [ ] > {
142
- const reader = readableStream . getReader ( ) ;
143
- const values : T [ ] = [ ] ;
144
- while ( true ) {
145
- const { done, value } = await reader . read ( ) ;
146
- if ( done ) {
147
- reader . releaseLock ( ) ;
148
- break ;
149
- } else if ( value ) {
150
- values . push ( value ) ;
151
- }
152
- }
153
- return values ;
154
- }
0 commit comments