@@ -60,18 +60,32 @@ public async Task<CheckResult> PingAsync(Guid endpointId, string host, int timeo
6060
6161 try
6262 {
63- // Use overload that accepts cancellation token
64- PingReply reply = await ping . SendPingAsync ( host , TimeSpan . FromMilliseconds ( timeoutMs ) , cancellationToken : combinedCts . Token ) ;
65- stopwatch . Stop ( ) ;
63+ PingReply reply ;
6664
67- if ( reply . Status == IPStatus . Success )
65+ // Handle IPv6 link-local with scope
66+ if ( IPAddress . TryParse ( host , out var ip ) )
6867 {
69- return CheckResult . Success ( endpointId , timestamp , reply . RoundtripTime ) ;
68+ if ( ip . AddressFamily == AddressFamily . InterNetworkV6 && host . Contains ( '%' ) )
69+ {
70+ var parts = host . Split ( '%' ) ;
71+ ip = IPAddress . Parse ( parts [ 0 ] ) ;
72+ ip . ScopeId = long . Parse ( parts [ 1 ] ) ;
73+ }
74+
75+ reply = await ping . SendPingAsync ( ip , timeoutMs ) ;
7076 }
7177 else
7278 {
73- return CheckResult . Failure ( endpointId , timestamp , $ "Ping failed: { reply . Status } ") ;
79+ // Hostname fallback
80+ reply = await ping . SendPingAsync ( host , timeoutMs ) ;
7481 }
82+
83+ stopwatch . Stop ( ) ;
84+
85+ if ( reply . Status == IPStatus . Success )
86+ return CheckResult . Success ( endpointId , timestamp , reply . RoundtripTime ) ;
87+ else
88+ return CheckResult . Failure ( endpointId , timestamp , $ "Ping failed: { reply . Status } ") ;
7589 }
7690 catch ( OperationCanceledException ) when ( timeoutCts . Token . IsCancellationRequested && ! cancellationToken . IsCancellationRequested )
7791 {
@@ -105,12 +119,25 @@ public async Task<CheckResult> TcpConnectAsync(Guid endpointId, string host, int
105119 using var timeoutCts = new CancellationTokenSource ( timeoutMs ) ;
106120 using var combinedCts = CancellationTokenSource . CreateLinkedTokenSource ( cancellationToken , timeoutCts . Token ) ;
107121
108- // Use the combined cancellation token for the connection
109- Task connectTask = tcpClient . ConnectAsync ( host , port , combinedCts . Token ) . AsTask ( ) ;
110-
111122 try
112123 {
113- await connectTask ;
124+ // Handle IPv6 link-local with scope
125+ if ( IPAddress . TryParse ( host , out var ip ) )
126+ {
127+ if ( ip . AddressFamily == AddressFamily . InterNetworkV6 && host . Contains ( '%' ) )
128+ {
129+ var parts = host . Split ( '%' ) ;
130+ ip = IPAddress . Parse ( parts [ 0 ] ) ;
131+ ip . ScopeId = long . Parse ( parts [ 1 ] ) ;
132+ }
133+
134+ await tcpClient . ConnectAsync ( ip , port , combinedCts . Token ) ;
135+ }
136+ else
137+ {
138+ await tcpClient . ConnectAsync ( host , port , combinedCts . Token ) ;
139+ }
140+
114141 stopwatch . Stop ( ) ;
115142 return CheckResult . Success ( endpointId , timestamp , stopwatch . ElapsedMilliseconds ) ;
116143 }
@@ -196,10 +223,28 @@ public async Task<CheckResult> HttpCheckAsync(Guid endpointId, string host, int
196223
197224 /// <summary>
198225 /// Builds HTTP URL with proper protocol detection and validation.
199- /// Handles cases where host already contains protocol or needs port-based detection .
226+ /// Handles IPv6 addresses, scope IDs, existing protocols, ports, and optional path .
200227 /// </summary>
201228 private static string BuildHttpUrl ( string host , int port , string ? path )
202229 {
230+ // Wrap IPv6 in brackets and escape scope IDs for URLs
231+ if ( IPAddress . TryParse ( host , out var ip ) && ip . AddressFamily == AddressFamily . InterNetworkV6 )
232+ {
233+ // Handle scope index (zone)
234+ if ( host . Contains ( '%' ) )
235+ {
236+ var parts = host . Split ( '%' ) ;
237+ string baseHost = parts [ 0 ] ;
238+ string scope = parts [ 1 ] ;
239+ // Per RFC 6874: must escape "%" as "%25" in URLs
240+ host = $ "[{ baseHost } %25{ scope } ]";
241+ }
242+ else
243+ {
244+ host = $ "[{ host } ]";
245+ }
246+ }
247+
203248 string url ;
204249
205250 // Check if host already contains a protocol
@@ -229,49 +274,32 @@ private static string BuildHttpUrl(string host, int port, string? path)
229274 else
230275 {
231276 // Host doesn't contain protocol, determine from port and context
232- string scheme ;
233-
234277 // Use smart defaults: 443 and common HTTPS ports default to HTTPS, others to HTTP
235- if ( port == 443 || IsCommonHttpsPort ( port ) )
236- {
237- scheme = "https" ;
238- }
239- else
240- {
241- scheme = "http" ;
242- }
243-
244- url = $ "{ scheme } ://{ host } ";
278+ string scheme = ( port == 443 || IsCommonHttpsPort ( port ) ) ? "https" : "http" ;
245279
246280 // Add port if not standard for the chosen protocol
247281 int standardPort = scheme == "https" ? 443 : 80 ;
282+
283+ url = $ "{ scheme } ://{ host } ";
248284 if ( port != standardPort )
249- {
250285 url += $ ":{ port } ";
251- }
252286 }
253-
254287 // Add path if specified
255288 if ( ! string . IsNullOrEmpty ( path ) )
256289 {
257290 // Ensure URL ends with host/port and path starts with /
258291 if ( ! url . EndsWith ( "/" ) && ! path . StartsWith ( "/" ) )
259- {
260292 url += "/" ;
261- }
262293 else if ( url . EndsWith ( "/" ) && path . StartsWith ( "/" ) )
263- {
264- // Remove duplicate slash
265- path = path . Substring ( 1 ) ;
266- }
294+ path = path . Substring ( 1 ) ; // Remove duplicate slash
267295
268296 url += path ;
269297 }
270298
271299 // Validate the final URL
272300 try
273301 {
274- var validationUri = new Uri ( url ) ;
302+ _ = new Uri ( url ) ;
275303 return url ;
276304 }
277305 catch ( UriFormatException ex )
0 commit comments