@@ -58,9 +58,11 @@ const memoizedParse = memorize((url) => {
5858
5959const UP_PATH_REGEXP = / (?: ^ | [ \\ / ] ) \. \. (?: [ \\ / ] | $ ) / ;
6060
61+ /** @typedef {import("fs").Stats } FSStats */
62+
6163/**
6264 * @typedef {object } Extra
63- * @property {import("fs").Stats } stats stats
65+ * @property {FSStats } stats stats
6466 * @property {boolean= } immutable true when immutable, otherwise false
6567 * @property {OutputFileSystem } outputFileSystem outputFileSystem
6668 */
@@ -81,13 +83,27 @@ class FilenameError extends Error {
8183 constructor ( message , code ) {
8284 super ( message ) ;
8385 this . name = "FilenameError" ;
84- this . code = code ;
86+ this . statusCode = code ;
8587 }
8688}
8789
8890/** @typedef {{ filename: string, extra: Extra } } FilenameWithExtra */
8991
90- // TODO fix redirect logic when `/` at the end, like https://github.com/pillarjs/send/blob/master/index.js#L586
92+ /**
93+ * @param {unknown } error error
94+ * @returns {boolean } true when error is like not found, otherwise false
95+ */
96+ function isNotFoundError ( error ) {
97+ switch ( /** @type {NodeJS.ErrnoException } */ ( error ) . code ) {
98+ case "ENAMETOOLONG" :
99+ case "ENOENT" :
100+ case "ENOTDIR" :
101+ return true ;
102+ default :
103+ return false ;
104+ }
105+ }
106+
91107/**
92108 * @template {IncomingMessage} Request
93109 * @template {ServerResponse} Response
@@ -99,9 +115,6 @@ function getFilenameFromUrl(context, url) {
99115 /** @type {URL } */
100116 let urlObject ;
101117
102- /** @type {string | undefined } */
103- let foundFilename ;
104-
105118 try {
106119 // The `url` property of the `request` is contains only `pathname`, `search` and `hash`
107120 urlObject = memoizedParse ( url ) ;
@@ -110,14 +123,19 @@ function getFilenameFromUrl(context, url) {
110123 }
111124
112125 const { options, stats } = context ;
113- /** @type {Extra } */
114- const extra = { } ;
115126
116127 /** @type {Stats[] } */
117128 const allStats =
118129 /** @type {MultiStats } */
119130 ( stats ) . stats || [ /** @type {Stats } */ ( stats ) ] ;
120131
132+ const index =
133+ options . index === false
134+ ? /** @type {string[] } */ ( [ ] )
135+ : typeof options . index === "undefined" || options . index === true
136+ ? [ "index.html" ]
137+ : [ options . index ] ;
138+
121139 for ( const { compilation } of allStats ) {
122140 if ( compilation . options . devServer === false ) {
123141 continue ;
@@ -158,6 +176,7 @@ function getFilenameFromUrl(context, url) {
158176 throw new FilenameError ( "Forbidden" , 403 ) ;
159177 }
160178
179+ // send file logic
161180 // The `output.path` is always present and always absolute
162181 const outputPath = compilation . getPath (
163182 compilation . outputOptions . path || "" ,
@@ -172,61 +191,100 @@ function getFilenameFromUrl(context, url) {
172191 pathname . slice ( publicPathPathname . length ) ,
173192 ) ;
174193
194+ const { assetsInfo } = compilation ;
175195 const { outputFileSystem } =
176196 /** @type {Compiler & { outputFileSystem: OutputFileSystem } } */
177197 ( compilation . compiler ) ;
178198
179- try {
180- extra . stats = outputFileSystem . statSync ( filename ) ;
181- } catch {
182- continue ;
183- }
184-
185- const { assetsInfo } = compilation ;
199+ /**
200+ * @param {string } filename filename
201+ * @returns {FilenameWithExtra | undefined } filename when found, otherwise undefined
202+ */
203+ const resolveIndex = ( filename ) => {
204+ filename = path . join ( filename , index [ 0 ] ) ;
186205
187- if ( extra . stats . isFile ( ) ) {
188- foundFilename = filename ;
206+ let stats ;
189207
190- // Rspack does not yet support `assetsInfo`, so we need to check if `assetsInfo` exists here
191- extra . immutable = assetsInfo
192- ? assetsInfo . get ( pathname . slice ( publicPathPathname . length ) ) ?. immutable
193- : false ;
194- extra . outputFileSystem = outputFileSystem ;
208+ try {
209+ stats = outputFileSystem . statSync ( filename ) ;
210+ } catch ( err ) {
211+ if ( isNotFoundError ( err ) ) return ;
212+ throw err ;
213+ }
195214
196- break ;
197- } else if (
198- extra . stats . isDirectory ( ) &&
199- ( typeof options . index === "undefined" || options . index )
200- ) {
201- const indexValue =
202- typeof options . index === "undefined" ||
203- typeof options . index === "boolean"
204- ? "index.html"
205- : options . index ;
215+ if ( /** @type {FSStats } */ ( stats ) . isDirectory ( ) ) {
216+ return resolveIndex ( filename ) ;
217+ }
206218
207- filename = path . join ( filename , indexValue ) ;
219+ const extra = {
220+ immutable : assetsInfo
221+ ? assetsInfo . get ( pathname . slice ( publicPathPathname . length ) )
222+ ?. immutable
223+ : false ,
224+ outputFileSystem,
225+ stats : /** @type {FSStats } */ ( stats ) ,
226+ } ;
227+
228+ return { filename, extra } ;
229+ } ;
230+
231+ /**
232+ * @param {string } filename filename
233+ * @returns {FilenameWithExtra | undefined } filename when found, otherwise undefined
234+ */
235+ const resolveFile = ( filename ) => {
236+ let stats ;
208237
209238 try {
210- extra . stats = outputFileSystem . statSync ( filename ) ;
211- } catch {
212- continue ;
239+ stats = outputFileSystem . statSync ( filename ) ;
240+ } catch ( err ) {
241+ if ( isNotFoundError ( err ) ) return ;
242+ throw err ;
213243 }
214244
215- if ( extra . stats . isFile ( ) ) {
216- foundFilename = filename ;
217- extra . outputFileSystem = outputFileSystem ;
245+ if ( /** @type {FSStats } */ ( stats ) . isDirectory ( ) ) {
246+ // Different between `send` and our logic is here, `send` makes a redirect, we just return a file.
247+ return resolveIndex ( filename ) ;
248+ }
218249
219- break ;
250+ if ( filename . endsWith ( path . sep ) ) {
251+ return ;
220252 }
253+
254+ /** @type {Extra } */
255+ const extra = {
256+ immutable : assetsInfo
257+ ? assetsInfo . get ( pathname . slice ( publicPathPathname . length ) )
258+ ?. immutable
259+ : false ,
260+ outputFileSystem,
261+ stats : /** @type {FSStats } */ ( stats ) ,
262+ } ;
263+
264+ return { filename, extra } ;
265+ } ;
266+
267+ // send index logic
268+ if ( index . length > 0 && pathname . endsWith ( "/" ) ) {
269+ const result = resolveIndex ( filename ) ;
270+
271+ if ( ! result ) {
272+ continue ;
273+ }
274+
275+ return result ;
221276 }
222- }
223- }
224277
225- if ( ! foundFilename ) {
226- return ;
227- }
278+ // send file logic
279+ const result = resolveFile ( filename ) ;
280+
281+ if ( ! result ) {
282+ continue ;
283+ }
228284
229- return { filename : foundFilename , extra } ;
285+ return result ;
286+ }
287+ }
230288}
231289
232290/**
@@ -448,9 +506,11 @@ function wrapper(context) {
448506
449507 /**
450508 * @param {NodeJS.ErrnoException } error error
509+ * @param {string= } message override message
510+ * @param {number= } code override code
451511 * @returns {Promise<void> }
452512 */
453- async function errorHandler ( error ) {
513+ async function errorHandler ( error , message , code ) {
454514 switch ( error . code ) {
455515 case "ENAMETOOLONG" :
456516 case "ENOENT" :
@@ -460,7 +520,7 @@ function wrapper(context) {
460520 } ) ;
461521 break ;
462522 default :
463- await sendError ( error . message , 500 , {
523+ await sendError ( message || error . message , code || 500 , {
464524 modifyResponseData : context . options . modifyResponseData ,
465525 } ) ;
466526 break ;
@@ -713,20 +773,22 @@ function wrapper(context) {
713773 const errorCode =
714774 typeof err === "object" &&
715775 err !== null &&
716- typeof ( /** @type {FilenameError } */ ( err ) . code ) !== "undefined"
717- ? /** @type {FilenameError } */ ( err ) . code
718- : 403 ;
776+ typeof ( /** @type {FilenameError } */ ( err ) . statusCode ) !== "undefined"
777+ ? /** @type {FilenameError } */ ( err ) . statusCode
778+ : undefined ;
719779
720780 if ( errorCode === 403 ) {
721781 context . logger . error ( `Malicious path "${ requestUrl } ".` ) ;
722782 }
723783
724- await sendError (
725- errorCode === 400 ? "Bad Request" : "Forbidden" ,
784+ await errorHandler (
785+ /** @type {NodeJS.ErrnoException } */ ( err ) ,
786+ errorCode === 400
787+ ? "Bad Request"
788+ : errorCode === 403
789+ ? "Forbidden"
790+ : undefined ,
726791 errorCode ,
727- {
728- modifyResponseData : context . options . modifyResponseData ,
729- } ,
730792 ) ;
731793 return ;
732794 }
0 commit comments