@@ -11,7 +11,7 @@ open System.Text
1111open System.Threading .Tasks
1212open Microsoft.AspNetCore .Builder
1313open Microsoft.AspNetCore .Http
14- open System.Net .NetworkInformation
14+ open System.Net .Sockets
1515
1616type ITestHttpServer =
1717 inherit IDisposable
@@ -47,14 +47,14 @@ let startHttpLocalServer() =
4747 } |> Async.StartAsTask :> Task
4848 )) |> ignore
4949
50+ // Use TcpListener(0) to ask the OS for a free port, then release it.
51+ // The TOCTOU window (between Stop and Kestrel's bind) is microseconds,
52+ // far more reliable than the previous random-port-then-check approach.
5053 let freePort =
51- let random = new System.Random()
52- let mutable port = random.Next( 10000 , 65000 ) // Use a random high port instead of a fixed port
53- while
54- IPGlobalProperties.GetIPGlobalProperties() .GetActiveTcpListeners()
55- |> Array.map ( fun x -> x.Port)
56- |> Array.contains port do
57- port <- random.Next( 10000 , 65000 )
54+ let listener = new TcpListener( System.Net.IPAddress.Loopback, 0 )
55+ listener.Start()
56+ let port = ( listener.LocalEndpoint :?> System.Net.IPEndPoint) .Port
57+ listener.Stop()
5858 port
5959
6060 let baseAddress = $" http://127.0.0.1:{freePort}"
@@ -64,7 +64,7 @@ let startHttpLocalServer() =
6464
6565 { new ITestHttpServer with
6666 member this.Dispose () =
67- app.StopAsync() |> Async.AwaitTask |> ignore
67+ app.StopAsync() |> Async.AwaitTask |> Async.RunSynchronously
6868 printfn $" Stopped local http server with address {baseAddress}"
6969 member this.WorkerTask = workerTask
7070 member this.BaseAddress = baseAddress }
@@ -294,22 +294,12 @@ let testFormDataBodySize (size: int) =
294294
295295[<Test; TestCaseSource( " testFormDataSizesInBytes" ) >]
296296let testMultipartFormDataBodySize ( size : int ) =
297- // Skip this test on Windows when running in CI because of flaky port binding behavior on some Windows CI agents.
298- let isWindows = System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform( System.Runtime.InteropServices.OSPlatform.Windows)
299- let inCi =
300- let env v = Environment.GetEnvironmentVariable v
301- [ " CI" ; " GITHUB_ACTIONS" ; " TF_BUILD" ; " APPVEYOR" ; " GITLAB_CI" ; " JENKINS_URL" ]
302- |> List.exists ( fun e -> not ( String.IsNullOrEmpty ( env e)))
303-
304- if isWindows && inCi then
305- Assert.Ignore( " Skipping test on Windows in CI" )
306- else
307- use localServer = startHttpLocalServer()
308- let bodyString = seq { for _ i in 0 .. size -> " x\n " } |> String.concat " "
309- let multipartItem = [ MultipartItem( " input" , " input.txt" , new MemoryStream( Encoding.UTF8.GetBytes( bodyString)) :> Stream) ]
310- let body = Multipart( Guid.NewGuid() .ToString(), multipartItem)
297+ use localServer = startHttpLocalServer()
298+ let bodyString = seq { for _ i in 0 .. size -> " x\n " } |> String.concat " "
299+ let multipartItem = [ MultipartItem( " input" , " input.txt" , new MemoryStream( Encoding.UTF8.GetBytes( bodyString)) :> Stream) ]
300+ let body = Multipart( Guid.NewGuid() .ToString(), multipartItem)
311301
312- Assert.DoesNotThrowAsync( fun () -> Http.AsyncRequest ( url= localServer.BaseAddress + " /200" , httpMethod= " POST" , body= body, timeout = 10000 ) |> Async.Ignore |> Async.StartAsTask :> _)
302+ Assert.DoesNotThrowAsync( fun () -> Http.AsyncRequest ( url= localServer.BaseAddress + " /200" , httpMethod= " POST" , body= body, timeout = 10000 ) |> Async.Ignore |> Async.StartAsTask :> _)
313303
314304[<Test>]
315305let ``escaping of url parameters`` () =
0 commit comments