@@ -1957,71 +1957,83 @@ function createMiddlewareHandler(routeKey: string, handler: StacksHandler): Rout
19571957 const reqId = enhancedReq . _requestId as string | undefined
19581958 const startNs = enhancedReq . _startNs as bigint | undefined
19591959 const durMs = startNs != null ? Number ( process . hrtime . bigint ( ) - startNs ) / 1_000_000 : null
1960+ const after = enhancedReq . _afterResponse
1961+ const requested = enhancedReq . _responseHeaders
1962+ const frameworkMetadataApplied = response
1963+ && ( response as unknown as Record < symbol , unknown > ) [ FRAMEWORK_RESPONSE_METADATA_APPLIED ] === true
1964+ // Framework JSON responses already carry their request id and security
1965+ // defaults from `formatJsonResult`. When no middleware requested later
1966+ // work, leave the native Headers object untouched and avoid allocating
1967+ // the post-processing closure on the dominant success path.
1968+ const needsResponseMetadata = response && (
1969+ response . status >= 400
1970+ || Array . isArray ( after )
1971+ || ( requested && typeof requested === 'object' )
1972+ || durMs != null
1973+ || ! frameworkMetadataApplied
1974+ )
19601975
1961- const setHeaders = ( h : Headers ) => {
1962- /*
1963- * Headers a middleware asked to have on the response.
1964- *
1965- * The middleware pipeline is pre-action only, so a middleware that has
1966- * something to say *about the answer* - a rate limit's remaining count,
1967- * a cache verdict, a deprecation notice - had nowhere to put it.
1968- * Compression got a hard-coded post-action wrapper keyed on a
1969- * `_compress` marker; everything else got nothing, and the workaround
1970- * in an app is to wrap every action.
1971- *
1972- * So: a middleware writes `request._responseHeaders`, and they land
1973- * here, before the router's own. The router's win a collision on
1974- * purpose - `X-Request-ID` and `Server-Timing` are this layer's to
1975- * state, and a middleware overwriting them breaks correlation.
1976- */
1977- /*
1978- * Callbacks a middleware asked to run once the answer is known.
1979- *
1980- * The header seam covers "put this on the response"; this covers
1981- * "record that this happened", which is the other half of what a
1982- * pre-action pipeline cannot do. Metrics are the obvious case: a
1983- * middleware can time the start of a request and has no way to learn
1984- * the status or the duration without one of these.
1985- *
1986- * Failures are swallowed on purpose. A metrics callback that throws
1987- * must not turn a served request into a 500 - the observation is worth
1988- * less than the thing being observed.
1989- */
1990- const after = ( enhancedReq ) . _afterResponse
1991- if ( Array . isArray ( after ) ) {
1992- for ( const callback of after ) {
1993- try {
1994- if ( typeof callback === 'function' )
1995- callback ( { status : response ?. status ?? 0 , durationMs : durMs ?? 0 } )
1976+ if ( needsResponseMetadata && typeof response . headers ?. set === 'function' ) {
1977+ const setHeaders = ( h : Headers ) => {
1978+ /*
1979+ * Headers a middleware asked to have on the response.
1980+ *
1981+ * The middleware pipeline is pre-action only, so a middleware that has
1982+ * something to say *about the answer* - a rate limit's remaining count,
1983+ * a cache verdict, a deprecation notice - had nowhere to put it.
1984+ * Compression got a hard-coded post-action wrapper keyed on a
1985+ * `_compress` marker; everything else got nothing, and the workaround
1986+ * in an app is to wrap every action.
1987+ *
1988+ * So: a middleware writes `request._responseHeaders`, and they land
1989+ * here, before the router's own. The router's win a collision on
1990+ * purpose - `X-Request-ID` and `Server-Timing` are this layer's to
1991+ * state, and a middleware overwriting them breaks correlation.
1992+ */
1993+ /*
1994+ * Callbacks a middleware asked to run once the answer is known.
1995+ *
1996+ * The header seam covers "put this on the response"; this covers
1997+ * "record that this happened", which is the other half of what a
1998+ * pre-action pipeline cannot do. Metrics are the obvious case: a
1999+ * middleware can time the start of a request and has no way to learn
2000+ * the status or the duration without one of these.
2001+ *
2002+ * Failures are swallowed on purpose. A metrics callback that throws
2003+ * must not turn a served request into a 500 - the observation is worth
2004+ * less than the thing being observed.
2005+ */
2006+ if ( Array . isArray ( after ) ) {
2007+ for ( const callback of after ) {
2008+ try {
2009+ if ( typeof callback === 'function' )
2010+ callback ( { status : response . status , durationMs : durMs ?? 0 } )
2011+ }
2012+ catch { /* an observation is worth less than the request it observes */ }
19962013 }
1997- catch { /* an observation is worth less than the request it observes */ }
19982014 }
1999- }
20002015
2001- const requested = ( enhancedReq ) . _responseHeaders
2002- if ( requested && typeof requested === 'object' ) {
2003- for ( const [ name , value ] of Object . entries ( requested as Record < string , unknown > ) ) {
2004- if ( typeof value === 'string' )
2005- h . set ( name , value )
2016+ if ( requested && typeof requested === 'object' ) {
2017+ for ( const [ name , value ] of Object . entries ( requested as Record < string , unknown > ) ) {
2018+ if ( typeof value === 'string' )
2019+ h . set ( name , value )
2020+ }
20062021 }
2007- }
20082022
2009- const frameworkMetadataApplied = ( response as unknown as Record < symbol , unknown > ) [ FRAMEWORK_RESPONSE_METADATA_APPLIED ] === true
2010- if ( reqId && ! frameworkMetadataApplied ) h . set ( 'X-Request-ID' , reqId )
2011- if ( durMs != null ) {
2012- let timing = `total;dur=${ durMs . toFixed ( 1 ) } `
2013- // Append per-middleware timing entries. Chrome's network
2014- // panel shows these as a stacked timeline under the response.
2015- for ( const t of middlewareTimings ) {
2016- timing += `, mw_${ t . name } ;dur=${ t . ms . toFixed ( 1 ) } `
2023+ if ( reqId && ! frameworkMetadataApplied ) h . set ( 'X-Request-ID' , reqId )
2024+ if ( durMs != null ) {
2025+ let timing = `total;dur=${ durMs . toFixed ( 1 ) } `
2026+ // Append per-middleware timing entries. Chrome's network
2027+ // panel shows these as a stacked timeline under the response.
2028+ for ( const t of middlewareTimings ) {
2029+ timing += `, mw_${ t . name } ;dur=${ t . ms . toFixed ( 1 ) } `
2030+ }
2031+ h . set ( 'Server-Timing' , timing )
20172032 }
2018- h . set ( 'Server-Timing' , timing )
2033+ if ( ! frameworkMetadataApplied )
2034+ applySecurityHeaders ( h )
20192035 }
2020- if ( ! frameworkMetadataApplied )
2021- applySecurityHeaders ( h )
2022- }
20232036
2024- if ( response && typeof ( response ) . headers ?. set === 'function' ) {
20252037 const isErrorJson = response . status >= 400
20262038 && ( response . headers . get ( 'content-type' ) || '' ) . includes ( 'json' )
20272039
0 commit comments