diff --git a/lib/fetch-common.ts b/lib/fetch-common.ts index 03662f8..7b862ec 100644 --- a/lib/fetch-common.ts +++ b/lib/fetch-common.ts @@ -16,6 +16,7 @@ const { HTTP2_HEADER_METHOD, HTTP2_HEADER_SCHEME, HTTP2_HEADER_PATH, + HTTP2_HEADER_AUTHORITY, // Methods HTTP2_METHOD_GET, @@ -124,6 +125,7 @@ export async function setupFetch( const { origin, protocol, + host, pathname, search, hash, } = new URL( url ); const path = pathname + search + hash; @@ -143,6 +145,9 @@ export async function setupFetch( if ( headers.has( HTTP2_HEADER_COOKIE ) ) cookies.push( ...arrayify( headers.get( HTTP2_HEADER_COOKIE ) ) ); + if ( !headers.has( "host" ) ) + headers.set( "host", host ); + const headersToSend: RawHeaders = { // Set required headers ...( session.protocol === "http1" ? { } : { @@ -165,7 +170,7 @@ export async function setupFetch( if ( key === "host" && session.protocol === "http2" ) // Convert to :authority like curl does: // https://github.com/grantila/fetch-h2/issues/9 - headersToSend[ ":authority" ] = val; + headersToSend[ HTTP2_HEADER_AUTHORITY ] = val; else if ( key !== HTTP2_HEADER_COOKIE ) headersToSend[ key ] = val; } diff --git a/test/fetch-h2/context.ts b/test/fetch-h2/context.ts index 1412800..b2ec749 100644 --- a/test/fetch-h2/context.ts +++ b/test/fetch-h2/context.ts @@ -1,3 +1,4 @@ +import { map } from "already"; import { TestData } from "../lib/server-common"; import { makeMakeServer } from "../lib/server-helpers"; @@ -270,7 +271,7 @@ describe( `context (${version} over ${proto.replace( ":", "" )})`, ( ) => { const { server } = await makeServer( ); - const { disconnectAll, fetch } = context( ); + const { disconnectAll, fetch } = context( { ...cycleOpts } ); const awaitFetch = fetch( "${proto}//localhost:0" ); @@ -305,5 +306,82 @@ describe( `context (${version} over ${proto.replace( ":", "" )})`, ( ) => await server.shutdown( ); } ); } ); + + describe( "session sharing", ( ) => + { + jest.setTimeout( 2500 ); + + it( "should re-use session for same host", async ( ) => + { + const { disconnectAll, fetch } = context( { ...cycleOpts } ); + + const urls = [ + [ "https://en.wikipedia.org/wiki/33", "33" ], + [ "https://en.wikipedia.org/wiki/44", "44" ], + [ "https://en.wikipedia.org/wiki/42", "42" ], + ]; + + const resps = await map( + urls, + { concurrency: Infinity }, + async ( [ url, title ] ) => + { + const resp = await fetch( url ); + const text = await resp.text( ); + const m = text.match( /]*>(.*)<\/h1>/ ); + return { expected: title, got: m?.[ 1 ] }; + } + ); + + resps.forEach( ( { expected, got } ) => + { + expect( expected ).toBe( got ); + } ); + + await disconnectAll( ); + } ); + + it( "should re-use session for same SAN but different host", + async ( ) => + { + const { disconnectAll, fetch } = context( { ...cycleOpts } ); + + const urls = [ + { lang: "en", title: "33" }, + { lang: "en", title: "44" }, + { lang: "sv", title: "33" }, + { lang: "sv", title: "44" }, + ] as const; + + const resps = await map( + urls, + { concurrency: Infinity }, + async ( { lang, title } ) => + { + const url = `https://${lang}.wikipedia.org/wiki/${title}`; + const resp = await fetch( url ); + const text = await resp.text( ); + const mLang = text.match( /]* lang="([^"]+)"/ ); + const mTitle = text.match( /]*>([^<]+)<\/h1>/ ); + return { + expectedLang: lang, + gotLang: mLang?.[ 1 ], + expectedTitle: title, + gotTitle: mTitle?.[ 1 ], + }; + } + ); + + resps.forEach( + ( { expectedLang, gotLang, expectedTitle, gotTitle } ) => + { + expect( expectedLang ).toBe( gotLang ); + expect( expectedTitle ).toBe( gotTitle ); + } + ); + + await disconnectAll( ); + } ); + } ); } ); } );