@@ -417,6 +417,7 @@ private static bool ValidateAddressFamily(ref AddressFamily addressFamily, strin
417417 }
418418
419419 private const string Localhost = "localhost" ;
420+ private const string LocalhostWithTrailingDot = Localhost + "." ;
420421 private const string InvalidDomain = "invalid" ;
421422
422423 // Some systems (e.g. Android, some Linux distros) map ::1 to "ip6-localhost" instead of
@@ -451,19 +452,25 @@ private static bool IsReservedName(string hostName, string reservedName)
451452 }
452453
453454 /// <summary>
454- /// Checks if the given host name is a subdomain of localhost (e.g., "foo. localhost") .
455- /// Plain "localhost" or "localhost. " returns false.
455+ /// Checks if the given host name should fall back to " localhost" when resolution fails or returns no addresses .
456+ /// Fully qualified "localhost." and localhost subdomains return true; plain "localhost" returns false.
456457 /// </summary>
457- private static bool IsLocalhostSubdomain ( string hostName )
458+ private static bool ShouldFallbackToLocalhost ( string hostName )
458459 {
460+ // Plain "localhost" is the fallback target, so only its fully qualified form should enter this path.
461+ if ( hostName . Equals ( LocalhostWithTrailingDot , StringComparison . OrdinalIgnoreCase ) )
462+ {
463+ return true ;
464+ }
465+
459466 // Strip trailing dot for length comparison
460467 int length = hostName . Length ;
461468 if ( hostName . EndsWith ( '.' ) )
462469 {
463470 length -- ;
464471 }
465472
466- // Must be longer than "localhost" (not just equal with trailing dot)
473+ // Subdomains must be longer than "localhost".
467474 return length > Localhost . Length && IsReservedName ( hostName , Localhost ) ;
468475 }
469476
@@ -513,10 +520,10 @@ private static object GetHostEntryOrAddressesCore(string hostName, bool justAddr
513520
514521 if ( errorCode != SocketError . Success )
515522 {
516- // RFC 6761 Section 6.3: If localhost subdomain fails, fall back to resolving plain "localhost".
517- if ( IsLocalhostSubdomain ( hostName ) )
523+ // RFC 6761 Section 6.3: If a localhost name fails, fall back to resolving plain "localhost".
524+ if ( ShouldFallbackToLocalhost ( hostName ) )
518525 {
519- if ( NetEventSource . Log . IsEnabled ( ) ) NetEventSource . Info ( hostName , "RFC 6761: Localhost subdomain resolution failed, falling back to 'localhost'" ) ;
526+ if ( NetEventSource . Log . IsEnabled ( ) ) NetEventSource . Info ( hostName , "RFC 6761: Localhost name resolution failed, falling back to 'localhost'" ) ;
520527 NameResolutionTelemetry . Log . AfterResolution ( hostName , activity , answer : null , exception : CreateException ( errorCode , nativeErrorCode ) ) ;
521528 fallbackToLocalhost = true ;
522529 }
@@ -526,10 +533,10 @@ private static object GetHostEntryOrAddressesCore(string hostName, bool justAddr
526533 throw CreateException ( errorCode , nativeErrorCode ) ;
527534 }
528535 }
529- else if ( addresses . Length == 0 && IsLocalhostSubdomain ( hostName ) )
536+ else if ( addresses . Length == 0 && ShouldFallbackToLocalhost ( hostName ) )
530537 {
531- // RFC 6761 Section 6.3: If localhost subdomain returns empty addresses, fall back to plain "localhost".
532- if ( NetEventSource . Log . IsEnabled ( ) ) NetEventSource . Info ( hostName , "RFC 6761: Localhost subdomain returned empty, falling back to 'localhost'" ) ;
538+ // RFC 6761 Section 6.3: If a localhost name returns empty addresses, fall back to plain "localhost".
539+ if ( NetEventSource . Log . IsEnabled ( ) ) NetEventSource . Info ( hostName , "RFC 6761: Localhost name returned empty, falling back to 'localhost'" ) ;
533540 NameResolutionTelemetry . Log . AfterResolution ( hostName , activity , answer : justAddresses ? addresses : ( object ) new IPHostEntry { AddressList = addresses , HostName = newHostName ! , Aliases = aliases } , exception : null ) ;
534541 fallbackToLocalhost = true ;
535542 }
@@ -713,7 +720,7 @@ private static Task GetHostEntryOrAddressesCoreAsync(string hostName, bool justR
713720 Task . FromException < IPHostEntry > ( invalidDomainException ! ) ;
714721 }
715722
716- // For localhost subdomains (RFC 6761 Section 6.3), we try the OS resolver first.
723+ // For localhost subdomains and fully qualified "localhost." (RFC 6761 Section 6.3), we try the OS resolver first.
717724 // If it fails or returns empty, we fall back to resolving plain "localhost".
718725 // This fallback logic is handled in GetHostEntryOrAddressesCore and GetAddrInfoWithTelemetryAsync.
719726
@@ -725,11 +732,11 @@ private static Task GetHostEntryOrAddressesCoreAsync(string hostName, bool justR
725732 // instead of calling the synchronous version in the ThreadPool.
726733 // If it fails, we will fall back to ThreadPool as well.
727734
728- // Always use the telemetry-enabled path for localhost subdomains to ensure fallback handling.
735+ // Always use the telemetry-enabled path for names requiring localhost fallback handling.
729736 // For other hostnames, use the non-telemetry path if diagnostics are disabled.
730- bool isLocalhostSubdomain = IsLocalhostSubdomain ( hostName ) ;
737+ bool shouldFallbackToLocalhost = ShouldFallbackToLocalhost ( hostName ) ;
731738 Task ? t ;
732- if ( NameResolutionTelemetry . AnyDiagnosticsEnabled ( ) || isLocalhostSubdomain )
739+ if ( NameResolutionTelemetry . AnyDiagnosticsEnabled ( ) || shouldFallbackToLocalhost )
733740 {
734741 t = justAddresses
735742 ? GetAddrInfoWithTelemetryAsync < IPAddress [ ] > ( hostName , justAddresses , family , cancellationToken )
@@ -783,15 +790,15 @@ private static Task GetHostEntryOrAddressesCoreAsync(string hostName, bool justR
783790
784791 if ( task != null )
785792 {
786- bool isLocalhostSubdomain = IsLocalhostSubdomain ( hostName ) ;
787- return CompleteAsync ( task , hostName , justAddresses , addressFamily , isLocalhostSubdomain , startingTimestamp , cancellationToken ) ;
793+ bool shouldFallbackToLocalhost = ShouldFallbackToLocalhost ( hostName ) ;
794+ return CompleteAsync ( task , hostName , justAddresses , addressFamily , shouldFallbackToLocalhost , startingTimestamp , cancellationToken ) ;
788795 }
789796
790797 // If resolution even did not start don't bother with telemetry.
791798 // We will retry on thread-pool.
792799 return null ;
793800
794- static async Task < T > CompleteAsync ( Task task , string hostName , bool justAddresses , AddressFamily addressFamily , bool isLocalhostSubdomain , long startingTimeStamp , CancellationToken cancellationToken )
801+ static async Task < T > CompleteAsync ( Task task , string hostName , bool justAddresses , AddressFamily addressFamily , bool shouldFallbackToLocalhost , long startingTimeStamp , CancellationToken cancellationToken )
795802 {
796803 NameResolutionActivity activity = NameResolutionTelemetry . Log . BeforeResolution ( hostName , startingTimeStamp ) ;
797804 Exception ? exception = null ;
@@ -801,19 +808,19 @@ static async Task<T> CompleteAsync(Task task, string hostName, bool justAddresse
801808 {
802809 result = await ( ( Task < T > ) task ) . ConfigureAwait ( false ) ;
803810
804- // RFC 6761 Section 6.3: If localhost subdomain returns empty addresses, fall back to plain "localhost".
805- if ( isLocalhostSubdomain && result is IPAddress [ ] addresses && addresses . Length == 0 )
811+ // RFC 6761 Section 6.3: If a localhost name returns empty addresses, fall back to plain "localhost".
812+ if ( shouldFallbackToLocalhost && result is IPAddress [ ] addresses && addresses . Length == 0 )
806813 {
807- if ( NetEventSource . Log . IsEnabled ( ) ) NetEventSource . Info ( hostName , "RFC 6761: Localhost subdomain returned empty, falling back to 'localhost'" ) ;
814+ if ( NetEventSource . Log . IsEnabled ( ) ) NetEventSource . Info ( hostName , "RFC 6761: Localhost name returned empty, falling back to 'localhost'" ) ;
808815 NameResolutionTelemetry . Log . AfterResolution ( hostName , activity , answer : result , exception : null ) ;
809816 fallbackOccurred = true ;
810817
811818 return await GetLocalhostAddressesAsync ( addressFamily , cancellationToken ) . ConfigureAwait ( false ) ;
812819 }
813820
814- if ( isLocalhostSubdomain && result is IPHostEntry entry && entry . AddressList . Length == 0 )
821+ if ( shouldFallbackToLocalhost && result is IPHostEntry entry && entry . AddressList . Length == 0 )
815822 {
816- if ( NetEventSource . Log . IsEnabled ( ) ) NetEventSource . Info ( hostName , "RFC 6761: Localhost subdomain returned empty, falling back to 'localhost'" ) ;
823+ if ( NetEventSource . Log . IsEnabled ( ) ) NetEventSource . Info ( hostName , "RFC 6761: Localhost name returned empty, falling back to 'localhost'" ) ;
817824 NameResolutionTelemetry . Log . AfterResolution ( hostName , activity , answer : result , exception : null ) ;
818825 fallbackOccurred = true ;
819826
@@ -822,10 +829,10 @@ static async Task<T> CompleteAsync(Task task, string hostName, bool justAddresse
822829
823830 return result ;
824831 }
825- catch ( SocketException ex ) when ( isLocalhostSubdomain && ! fallbackOccurred )
832+ catch ( SocketException ex ) when ( shouldFallbackToLocalhost && ! fallbackOccurred )
826833 {
827- // RFC 6761 Section 6.3: If localhost subdomain fails, fall back to resolving plain "localhost".
828- if ( NetEventSource . Log . IsEnabled ( ) ) NetEventSource . Info ( hostName , "RFC 6761: Localhost subdomain resolution failed, falling back to 'localhost'" ) ;
834+ // RFC 6761 Section 6.3: If a localhost name fails, fall back to resolving plain "localhost".
835+ if ( NetEventSource . Log . IsEnabled ( ) ) NetEventSource . Info ( hostName , "RFC 6761: Localhost name resolution failed, falling back to 'localhost'" ) ;
829836 NameResolutionTelemetry . Log . AfterResolution ( hostName , activity , answer : null , exception : ex ) ;
830837 fallbackOccurred = true ;
831838
0 commit comments