1
- import { Readable } from "node:stream" ;
2
-
1
+ import Stream from "node:stream" ;
3
2
import type { NextConfig } from "next" ;
4
3
import { NodeNextRequest , NodeNextResponse } from "next/dist/server/base-http/node" ;
5
- import { createRequestResponseMocks } from "next/dist/server/lib/mock-request" ;
4
+ import { MockedResponse } from "next/dist/server/lib/mock-request" ;
6
5
import NextNodeServer , { NodeRequestHandler } from "next/dist/server/next-server" ;
6
+ import type { IncomingMessage } from "node:http" ;
7
+
8
+ const NON_BODY_RESPONSES = new Set ( [ 101 , 204 , 205 , 304 ] ) ;
7
9
8
- /**
9
- * Injected at build time
10
- * (we practically follow what Next.js does here:
11
- https://github.com/vercel/next.js/blob/68a7128/packages/next/src/build/utils.ts#L2137-L2139)
12
- */
10
+ // Injected at build time
13
11
const nextConfig : NextConfig = JSON . parse ( process . env . __NEXT_PRIVATE_STANDALONE_CONFIG ?? "{}" ) ;
14
12
15
13
let requestHandler : NodeRequestHandler | null = null ;
@@ -23,6 +21,7 @@ export default {
23
21
customServer : false ,
24
22
dev : false ,
25
23
dir : "" ,
24
+ minimalMode : false ,
26
25
} ) . getRequestHandler ( ) ;
27
26
}
28
27
@@ -32,50 +31,98 @@ export default {
32
31
let imageUrl =
33
32
url . searchParams . get ( "url" ) ?? "https://developers.cloudflare.com/_astro/logo.BU9hiExz.svg" ;
34
33
if ( imageUrl . startsWith ( "/" ) ) {
35
- imageUrl = new URL ( imageUrl , request . url ) . href ;
34
+ return env . ASSETS . fetch ( new URL ( imageUrl , request . url ) ) ;
36
35
}
37
36
return fetch ( imageUrl , { cf : { cacheEverything : true } } as any ) ;
38
37
}
39
38
40
- const resBody = new TransformStream ( ) ;
41
- const writer = resBody . writable . getWriter ( ) ;
42
-
43
- const reqBodyNodeStream = request . body ? Readable . fromWeb ( request . body as any ) : undefined ;
44
-
45
- const { req, res } = createRequestResponseMocks ( {
46
- method : request . method ,
47
- url : url . href . slice ( url . origin . length ) ,
48
- headers : Object . fromEntries ( [ ...request . headers ] ) ,
49
- bodyReadable : reqBodyNodeStream ,
50
- resWriter : ( chunk ) => {
51
- writer . write ( chunk ) . catch ( console . error ) ;
52
- return true ;
53
- } ,
54
- } ) ;
55
-
56
- let headPromiseResolve : any = null ;
57
- const headPromise = new Promise < void > ( ( resolve ) => {
58
- headPromiseResolve = resolve ;
59
- } ) ;
60
- res . flushHeaders = ( ) => headPromiseResolve ?.( ) ;
61
-
62
- if ( reqBodyNodeStream != null ) {
63
- const origPush = reqBodyNodeStream . push ;
64
- reqBodyNodeStream . push = ( chunk : any ) => {
65
- req . push ( chunk ) ;
66
- return origPush . call ( reqBodyNodeStream , chunk ) ;
67
- } ;
68
- }
69
-
70
- ctx . waitUntil ( ( res as any ) . hasStreamed . then ( ( ) => writer . close ( ) ) ) ;
39
+ const { req, res, webResponse } = getWrappedStreams ( request , ctx ) ;
71
40
72
41
ctx . waitUntil ( requestHandler ( new NodeNextRequest ( req ) , new NodeNextResponse ( res ) ) ) ;
73
42
74
- await Promise . race ( [ res . headPromise , headPromise ] ) ;
75
-
76
- return new Response ( resBody . readable , {
77
- status : res . statusCode ,
78
- headers : ( res as any ) . headers ,
79
- } ) ;
43
+ return await webResponse ( ) ;
80
44
} ,
81
45
} ;
46
+
47
+ function getWrappedStreams ( request : Request , ctx : any ) {
48
+ const url = new URL ( request . url ) ;
49
+
50
+ const req = (
51
+ request . body ? Stream . Readable . fromWeb ( request . body as any ) : Stream . Readable . from ( [ ] )
52
+ ) as IncomingMessage ;
53
+ req . httpVersion = "1.0" ;
54
+ req . httpVersionMajor = 1 ;
55
+ req . httpVersionMinor = 0 ;
56
+ req . url = url . href . slice ( url . origin . length ) ;
57
+ req . headers = Object . fromEntries ( [ ...request . headers ] ) ;
58
+ req . method = request . method ;
59
+ Object . defineProperty ( req , "__node_stream__" , {
60
+ value : true ,
61
+ writable : false ,
62
+ } ) ;
63
+ Object . defineProperty ( req , "headersDistinct" , {
64
+ get ( ) {
65
+ const headers : Record < string , string [ ] > = { } ;
66
+ for ( const [ key , value ] of Object . entries ( req . headers ) ) {
67
+ if ( ! value ) continue ;
68
+ headers [ key ] = Array . isArray ( value ) ? value : [ value ] ;
69
+ }
70
+ return headers ;
71
+ } ,
72
+ } ) ;
73
+
74
+ const { readable, writable } = new IdentityTransformStream ( ) ;
75
+ const resBodyWriter = writable . getWriter ( ) ;
76
+
77
+ const res = new MockedResponse ( {
78
+ resWriter : ( chunk ) => {
79
+ resBodyWriter . write ( typeof chunk === "string" ? Buffer . from ( chunk ) : chunk ) . catch ( ( err : any ) => {
80
+ if (
81
+ err . message . includes ( "WritableStream has been closed" ) ||
82
+ err . message . includes ( "Network connection lost" )
83
+ ) {
84
+ // safe to ignore
85
+ return ;
86
+ }
87
+ console . error ( "Error in resBodyWriter.write" ) ;
88
+ console . error ( err ) ;
89
+ } ) ;
90
+ return true ;
91
+ } ,
92
+ } ) ;
93
+
94
+ // It's implemented as a no-op, but really it should mark the headers as done
95
+ res . flushHeaders = ( ) => ( res as any ) . headPromiseResolve ( ) ;
96
+
97
+ // Only allow statusCode to be modified if not sent
98
+ let { statusCode } = res ;
99
+ Object . defineProperty ( res , "statusCode" , {
100
+ get : function ( ) {
101
+ return statusCode ;
102
+ } ,
103
+ set : function ( val ) {
104
+ if ( this . finished || this . headersSent ) {
105
+ console . error ( "headers already sent" ) ;
106
+ return ;
107
+ }
108
+ statusCode = val ;
109
+ } ,
110
+ } ) ;
111
+
112
+ // Make sure the writer is eventually closed
113
+ ctx . waitUntil ( ( res as any ) . hasStreamed . finally ( ( ) => resBodyWriter . close ( ) . catch ( ( ) => { } ) ) ) ;
114
+
115
+ return {
116
+ res,
117
+ req,
118
+ webResponse : async ( ) => {
119
+ await res . headPromise ;
120
+ // TODO: remove this once streaming with compression is working nicely
121
+ res . setHeader ( "content-encoding" , "identity" ) ;
122
+ return new Response ( NON_BODY_RESPONSES . has ( res . statusCode ) ? null : readable , {
123
+ status : res . statusCode ,
124
+ headers : ( res as any ) . headers ,
125
+ } ) ;
126
+ } ,
127
+ } ;
128
+ }
0 commit comments