@@ -32,7 +32,8 @@ import {
3232 RuleEvent ,
3333 RawTrailers ,
3434 RawPassthroughEvent ,
35- RawPassthroughDataEvent
35+ RawPassthroughDataEvent ,
36+ RawHeaders
3637} from "../types" ;
3738import { DestroyableServer } from "destroyable-server" ;
3839import {
@@ -596,141 +597,170 @@ export class MockttpServer extends AbstractMockttp implements Mockttp {
596597 * For both normal requests & websockets, we do some standard preprocessing to ensure we have the absolute
597598 * URL destination in place, and timing, tags & id metadata all ready for an OngoingRequest.
598599 */
599- private preprocessRequest ( req : ExtendedRawRequest , type : 'request' | 'websocket' ) : OngoingRequest {
600- parseRequestBody ( req , { maxSize : this . maxBodySize } ) ;
601-
602- let rawHeaders = pairFlatRawHeaders ( req . rawHeaders ) ;
603- let socketMetadata : SocketMetadata | undefined = req . socket [ SocketMetadata ] ;
604-
605- // Make req.url always absolute, if it isn't already, using the host header.
606- // It might not be if this is a direct request, or if it's being transparently proxied.
607- if ( ! isAbsoluteUrl ( req . url ! ) ) {
608- req . protocol = getHeaderValue ( rawHeaders , ':scheme' ) ||
609- ( req . socket [ LastHopEncrypted ] ? 'https' : 'http' ) ;
610- req . path = req . url ;
611-
612- const tunnelDestination = req . socket [ LastTunnelAddress ]
613- ? getDestination ( req . protocol , req . socket [ LastTunnelAddress ] )
614- : undefined ;
615-
616- const isTunnelToIp = ! ! tunnelDestination && isIP ( tunnelDestination . hostname ) ;
617-
618- const urlDestination = getDestination ( req . protocol ,
619- ( ! isTunnelToIp
620- ? (
621- req . socket [ LastTunnelAddress ] ?? // Tunnel domain name is preferred if available
622- getHeaderValue ( rawHeaders , ':authority' ) ??
623- getHeaderValue ( rawHeaders , 'host' ) ??
624- req . socket [ TlsMetadata ] ?. sniHostname
625- )
626- : (
627- getHeaderValue ( rawHeaders , ':authority' ) ??
628- getHeaderValue ( rawHeaders , 'host' ) ??
629- req . socket [ TlsMetadata ] ?. sniHostname ??
630- req . socket [ LastTunnelAddress ] // We use the IP iff we have no hostname available at all
631- ) )
632- ?? `localhost:${ this . port } ` // If you specify literally nothing, it's a direct request
633- ) ;
600+ private preprocessRequest ( req : ExtendedRawRequest , type : 'request' | 'websocket' ) : OngoingRequest | null {
601+ try {
602+ parseRequestBody ( req , { maxSize : this . maxBodySize } ) ;
603+
604+ let rawHeaders = pairFlatRawHeaders ( req . rawHeaders ) ;
605+ let socketMetadata : SocketMetadata | undefined = req . socket [ SocketMetadata ] ;
606+
607+ // Make req.url always absolute, if it isn't already, using the host header.
608+ // It might not be if this is a direct request, or if it's being transparently proxied.
609+ if ( ! isAbsoluteUrl ( req . url ! ) ) {
610+ req . protocol = getHeaderValue ( rawHeaders , ':scheme' ) ||
611+ ( req . socket [ LastHopEncrypted ] ? 'https' : 'http' ) ;
612+ req . path = req . url ;
613+
614+ const tunnelDestination = req . socket [ LastTunnelAddress ]
615+ ? getDestination ( req . protocol , req . socket [ LastTunnelAddress ] )
616+ : undefined ;
617+
618+ const isTunnelToIp = ! ! tunnelDestination && isIP ( tunnelDestination . hostname ) ;
619+
620+ const urlDestination = getDestination ( req . protocol ,
621+ ( ! isTunnelToIp
622+ ? (
623+ req . socket [ LastTunnelAddress ] ?? // Tunnel domain name is preferred if available
624+ getHeaderValue ( rawHeaders , ':authority' ) ??
625+ getHeaderValue ( rawHeaders , 'host' ) ??
626+ req . socket [ TlsMetadata ] ?. sniHostname
627+ )
628+ : (
629+ getHeaderValue ( rawHeaders , ':authority' ) ??
630+ getHeaderValue ( rawHeaders , 'host' ) ??
631+ req . socket [ TlsMetadata ] ?. sniHostname ??
632+ req . socket [ LastTunnelAddress ] // We use the IP iff we have no hostname available at all
633+ ) )
634+ ?? `localhost:${ this . port } ` // If you specify literally nothing, it's a direct request
635+ ) ;
634636
635- // Actual destination always follows the tunnel - even if it's an IP
636- req . destination = tunnelDestination
637- ?? urlDestination ;
637+ // Actual destination always follows the tunnel - even if it's an IP
638+ req . destination = tunnelDestination
639+ ?? urlDestination ;
638640
639- // URL port should always match the real port - even if (e.g) the Host header is lying.
640- urlDestination . port = req . destination . port ;
641+ // URL port should always match the real port - even if (e.g) the Host header is lying.
642+ urlDestination . port = req . destination . port ;
641643
642- const absoluteUrl = `${ req . protocol } ://${
643- normalizeHost ( req . protocol , `${ urlDestination . hostname } :${ urlDestination . port } ` )
644- } ${ req . path } `;
644+ const absoluteUrl = `${ req . protocol } ://${
645+ normalizeHost ( req . protocol , `${ urlDestination . hostname } :${ urlDestination . port } ` )
646+ } ${ req . path } `;
647+
648+ let effectiveUrl : string ;
649+ try {
650+ effectiveUrl = new URL ( absoluteUrl ) . toString ( ) ;
651+ } catch ( e : any ) {
652+ req . url = absoluteUrl ;
653+ throw e ;
654+ }
645655
646- if ( ! getHeaderValue ( rawHeaders , ':path' ) ) {
647- ( req as Mutable < ExtendedRawRequest > ) . url = new url . URL ( absoluteUrl ) . toString ( ) ;
656+ if ( ! getHeaderValue ( rawHeaders , ':path' ) ) {
657+ ( req as Mutable < ExtendedRawRequest > ) . url = effectiveUrl ;
658+ } else {
659+ // Node's HTTP/2 compat logic maps .url to headers[':path']. We want them to
660+ // diverge: .url should always be absolute, while :path may stay relative,
661+ // so we override the built-in getter & setter:
662+ Object . defineProperty ( req , 'url' , {
663+ value : effectiveUrl
664+ } ) ;
665+ }
648666 } else {
649- // Node's HTTP/2 compat logic maps .url to headers[':path']. We want them to
650- // diverge: .url should always be absolute, while :path may stay relative,
651- // so we override the built-in getter & setter:
667+ // We have an absolute request. This is effectively a combined tunnel + end-server request,
668+ // so we need to handle both of those, and hide the proxy-specific bits from later logic.
669+ req . protocol = req . url ! . split ( '://' , 1 ) [ 0 ] ;
670+ req . path = getPathFromAbsoluteUrl ( req . url ! ) ;
671+ req . destination = getDestination (
672+ req . protocol ,
673+ req . socket [ LastTunnelAddress ] ?? getHostFromAbsoluteUrl ( req . url ! )
674+ ) ;
675+
676+ const proxyAuthHeader = getHeaderValue ( rawHeaders , 'proxy-authorization' ) ;
677+ if ( proxyAuthHeader ) {
678+ // Use this metadata for this request, but _only_ this request - it's not relevant
679+ // to other requests on the same socket so we don't add it to req.socket.
680+ socketMetadata = getSocketMetadataFromProxyAuth ( req . socket , proxyAuthHeader ) ;
681+ }
682+
683+ rawHeaders = rawHeaders . filter ( ( [ key ] ) => {
684+ const lcKey = key . toLowerCase ( ) ;
685+ return lcKey !== 'proxy-connection' &&
686+ lcKey !== 'proxy-authorization' ;
687+ } )
688+ }
689+
690+ if ( type === 'websocket' ) {
691+ req . protocol = req . protocol === 'https'
692+ ? 'wss'
693+ : 'ws' ;
694+
695+ // Transform the protocol in req.url too:
652696 Object . defineProperty ( req , 'url' , {
653- value : new url . URL ( absoluteUrl ) . toString ( )
697+ value : req . url ! . replace ( / ^ h t t p / , 'ws' )
654698 } ) ;
655699 }
656- } else {
657- // We have an absolute request. This is effectively a combined tunnel + end-server request,
658- // so we need to handle both of those, and hide the proxy-specific bits from later logic.
659- req . protocol = req . url ! . split ( '://' , 1 ) [ 0 ] ;
660- req . path = getPathFromAbsoluteUrl ( req . url ! ) ;
661- req . destination = getDestination (
662- req . protocol ,
663- req . socket [ LastTunnelAddress ] ?? getHostFromAbsoluteUrl ( req . url ! )
664- ) ;
665700
666- const proxyAuthHeader = getHeaderValue ( rawHeaders , 'proxy-authorization' ) ;
667- if ( proxyAuthHeader ) {
668- // Use this metadata for this request, but _only_ this request - it's not relevant
669- // to other requests on the same socket so we don't add it to req.socket.
670- socketMetadata = getSocketMetadataFromProxyAuth ( req . socket , proxyAuthHeader ) ;
671- }
701+ const id = crypto . randomUUID ( ) ;
672702
673- rawHeaders = rawHeaders . filter ( ( [ key ] ) => {
674- const lcKey = key . toLowerCase ( ) ;
675- return lcKey !== 'proxy-connection' &&
676- lcKey !== 'proxy-authorization' ;
677- } )
678- }
703+ const tags : string [ ] = getSocketMetadataTags ( socketMetadata ) ;
679704
680- if ( type === 'websocket' ) {
681- req . protocol = req . protocol === 'https'
682- ? 'wss'
683- : 'ws' ;
705+ const timingEvents : TimingEvents = {
706+ startTime : Date . now ( ) ,
707+ startTimestamp : now ( )
708+ } ;
684709
685- // Transform the protocol in req.url too:
686- Object . defineProperty ( req , 'url' , {
687- value : req . url ! . replace ( / ^ h t t p / , 'ws' )
710+ req . on ( 'end' , ( ) => {
711+ timingEvents . bodyReceivedTimestamp ||= now ( ) ;
688712 } ) ;
689- }
690-
691- const id = crypto . randomUUID ( ) ;
692713
693- const tags : string [ ] = getSocketMetadataTags ( socketMetadata ) ;
714+ const headers = rawHeadersToObject ( rawHeaders ) ;
694715
695- const timingEvents : TimingEvents = {
696- startTime : Date . now ( ) ,
697- startTimestamp : now ( )
698- } ;
716+ // Not writable for HTTP/2:
717+ makePropertyWritable ( req , 'headers' ) ;
718+ makePropertyWritable ( req , 'rawHeaders' ) ;
699719
700- req . on ( 'end' , ( ) => {
701- timingEvents . bodyReceivedTimestamp ||= now ( ) ;
702- } ) ;
720+ let rawTrailers : RawTrailers | undefined ;
721+ Object . defineProperty ( req , 'rawTrailers' , {
722+ get : ( ) => rawTrailers ,
723+ set : ( flatRawTrailers ) => {
724+ rawTrailers = flatRawTrailers
725+ ? pairFlatRawHeaders ( flatRawTrailers )
726+ : undefined ;
727+ }
728+ } ) ;
703729
704- const headers = rawHeadersToObject ( rawHeaders ) ;
730+ return Object . assign ( req , {
731+ id,
732+ headers,
733+ rawHeaders,
734+ rawTrailers, // Just makes the type happy - really managed by property above
735+ remoteIpAddress : req . socket . remoteAddress ,
736+ remotePort : req . socket . remotePort ,
737+ timingEvents,
738+ tags
739+ } ) as OngoingRequest ;
740+ } catch ( e : any ) {
741+ const error : Error = Object . assign ( e , {
742+ code : e . code ?? 'PREPROCESSING_FAILED' ,
743+ badRequest : req
744+ } ) ;
705745
706- // Not writable for HTTP/2:
707- makePropertyWritable ( req , 'headers' ) ;
708- makePropertyWritable ( req , 'rawHeaders' ) ;
746+ const h2Session = req . httpVersionMajor > 1 &&
747+ ( req as any ) . stream ?. session ;
709748
710- let rawTrailers : RawTrailers | undefined ;
711- Object . defineProperty ( req , 'rawTrailers' , {
712- get : ( ) => rawTrailers ,
713- set : ( flatRawTrailers ) => {
714- rawTrailers = flatRawTrailers
715- ? pairFlatRawHeaders ( flatRawTrailers )
716- : undefined ;
749+ if ( h2Session ) {
750+ this . handleInvalidHttp2Request ( error , h2Session ) ;
751+ } else {
752+ this . handleInvalidHttp1Request ( error , req . socket )
717753 }
718- } ) ;
719754
720- return Object . assign ( req , {
721- id,
722- headers,
723- rawHeaders,
724- rawTrailers, // Just makes the type happy - really managed by property above
725- remoteIpAddress : req . socket . remoteAddress ,
726- remotePort : req . socket . remotePort ,
727- timingEvents,
728- tags
729- } ) as OngoingRequest ;
755+ return null ; // Null -> preprocessing failed, error already handled here
756+ }
757+
730758 }
731759
732760 private async handleRequest ( rawRequest : ExtendedRawRequest , rawResponse : http . ServerResponse ) {
733761 const request = this . preprocessRequest ( rawRequest , 'request' ) ;
762+ if ( request === null ) return ; // Preprocessing failed - don't handle this
763+
734764 if ( this . debug ) console . log ( `Handling request for ${ rawRequest . url } ` ) ;
735765
736766 let result : 'responded' | 'aborted' | null = null ;
@@ -824,9 +854,10 @@ export class MockttpServer extends AbstractMockttp implements Mockttp {
824854 }
825855
826856 private async handleWebSocket ( rawRequest : ExtendedRawRequest , socket : net . Socket , head : Buffer ) {
827- if ( this . debug ) console . log ( `Handling websocket for ${ rawRequest . url } ` ) ;
828-
829857 const request = this . preprocessRequest ( rawRequest , 'websocket' ) ;
858+ if ( request === null ) return ; // Preprocessing failed - don't handle this
859+
860+ if ( this . debug ) console . log ( `Handling websocket for ${ rawRequest . url } ` ) ;
830861
831862 socket . on ( 'error' , ( error ) => {
832863 console . log ( 'Response error:' , this . debug ? error : error . message ) ;
@@ -1008,7 +1039,7 @@ ${await this.suggestRule(request)}`
10081039 // Called on server clientError, e.g. if the client disconnects during initial
10091040 // request data, or sends totally invalid gibberish. Only called for HTTP/1.1 errors.
10101041 private handleInvalidHttp1Request (
1011- error : Error & { code ?: string , rawPacket ?: Buffer } ,
1042+ error : Error & { code ?: string , rawPacket ?: Buffer , badRequest ?: ExtendedRawRequest } ,
10121043 socket : net . Socket
10131044 ) {
10141045 if ( socket [ ClientErrorInProgress ] ) {
@@ -1065,12 +1096,18 @@ ${await this.suggestRule(request)}`
10651096 ?? Buffer . from ( [ ] ) ;
10661097
10671098 // For packets where we get more than just httpolyglot-peeked data, guess-parse them:
1068- const parsedRequest = rawPacket . byteLength > 1
1069- ? tryToParseHttpRequest ( rawPacket , socket )
1070- : { } ;
1099+ const parsedRequest = error . badRequest ??
1100+ ( rawPacket . byteLength > 1
1101+ ? tryToParseHttpRequest ( rawPacket , socket )
1102+ : { }
1103+ ) ;
10711104
10721105 if ( isHeaderOverflow ) commonParams . tags . push ( 'header-overflow' ) ;
10731106
1107+ const rawHeaders = parsedRequest . rawHeaders ?. [ 0 ] && typeof parsedRequest . rawHeaders [ 0 ] === 'string'
1108+ ? pairFlatRawHeaders ( parsedRequest . rawHeaders as string [ ] )
1109+ : parsedRequest . rawHeaders as RawHeaders | undefined ;
1110+
10741111 const request : ClientError [ 'request' ] = {
10751112 ...commonParams ,
10761113 httpVersion : parsedRequest . httpVersion || '1.1' ,
@@ -1079,7 +1116,7 @@ ${await this.suggestRule(request)}`
10791116 url : parsedRequest . url ,
10801117 path : parsedRequest . path ,
10811118 headers : parsedRequest . headers || { } ,
1082- rawHeaders : parsedRequest . rawHeaders || [ ] ,
1119+ rawHeaders : rawHeaders || [ ] ,
10831120 remoteIpAddress : socket . remoteAddress ,
10841121 remotePort : socket . remotePort ,
10851122 destination : parsedRequest . destination
@@ -1131,7 +1168,7 @@ ${await this.suggestRule(request)}`
11311168 // Handle HTTP/2 client errors. This is a work in progress, but usefully reports
11321169 // some of the most obvious cases.
11331170 private handleInvalidHttp2Request (
1134- error : Error & { code ?: string , errno ?: number } ,
1171+ error : Error & { code ?: string , errno ?: number , badRequest ?: ExtendedRawRequest } ,
11351172 session : http2 . Http2Session
11361173 ) {
11371174 // Unlike with HTTP/1.1, we have no control of the actual handling of
@@ -1142,6 +1179,10 @@ ${await this.suggestRule(request)}`
11421179
11431180 const isBadPreface = ( error . errno === - 903 ) ;
11441181
1182+ const rawHeaders = error . badRequest ?. rawHeaders ?. [ 0 ] && typeof error . badRequest ?. rawHeaders [ 0 ] === 'string'
1183+ ? pairFlatRawHeaders ( error . badRequest ?. rawHeaders as string [ ] )
1184+ : error . badRequest ?. rawHeaders as RawHeaders | undefined ;
1185+
11451186 this . announceClientErrorAsync ( session . initialSocket , {
11461187 errorCode : error . code ,
11471188 request : {
@@ -1151,19 +1192,18 @@ ${await this.suggestRule(request)}`
11511192 ...( isBadPreface ? [ 'client-error:bad-preface' ] : [ ] ) ,
11521193 ...getSocketMetadataTags ( socket ?. [ SocketMetadata ] )
11531194 ] ,
1154- httpVersion : '2' ,
1195+ httpVersion : error . badRequest ?. httpVersion ?? '2' ,
11551196
11561197 // Best guesses:
11571198 timingEvents : { startTime : Date . now ( ) , startTimestamp : now ( ) } ,
1158- protocol : isTLS ? "https" : "http" ,
1159- url : isTLS ? `https://${
1160- ( socket as tls . TLSSocket ) . servername // Use the hostname from SNI
1161- } /` : undefined ,
1162-
1163- // Unknowable:
1164- path : undefined ,
1165- headers : { } ,
1166- rawHeaders : [ ]
1199+ protocol : error . badRequest ?. protocol || ( isTLS ? "https" : "http" ) ,
1200+ url : error . badRequest ?. url ||
1201+ ( isTLS ? `https://${ ( socket as tls . TLSSocket ) . servername } /` : undefined ) ,
1202+
1203+ path : error . badRequest ?. path ,
1204+ headers : error . badRequest ?. headers || { } ,
1205+ rawHeaders : rawHeaders || [ ] ,
1206+ destination : error . badRequest ?. destination
11671207 } ,
11681208 response : 'aborted' // These h2 errors get no app-level response, just a shutdown.
11691209 } ) ;
0 commit comments