Stuck on the "Path appears valid" error

Hello,

I am trying to get Hops working with Rhino 8. I have been reading similar topics here on the forums and have tried a fix related to the *.runtimeconfig.json files of rhino.compute and compute.geometry but unfortunately this didn’t work.

The fix I applied was based on this topic: Grasshopper Hops doesn’t work on RH8 SR17 - Rhino Developer / compute.rhino3d - McNeel Forum

Based on this I altered both

%AppData%\McNeel\Rhinoceros\packages\8.0\Hops\0.16.18\compute.geometry\computeg.geometry.runtimeconfig.json

{
    "runtimeOptions": {
        "tfm": "net8.0",
        "rollForward": "Major",
        "frameworks": [
            {
                "name": "Microsoft.NETCore.App",
                "version": "8.0.25"
            },
            {
                "name": "Microsoft.WindowsDesktop.App",
                "version": "8.0.25"
            },
            {
                "name": "Microsoft.AspNetCore.App",
                "version": "8.0.25"
            }
        ],
        "configProperties": {
"System.Reflection.Metadata.MetadataUpdater.IsSupported": false,
"System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": true,
"CSWINRT_USE_WINDOWS_UI_XAML_PROJECTIONS": false
        }
    }
}

%AppData%\McNeel\Rhinoceros\packages\8.0\Hops\0.16.28\rhino.compute\rhino.compute.runtimeconfig.json

{
    "runtimeOptions": {
        "tfm": "net8.0",
        "includedFrameworks": [
            {
                "name": "Microsoft.NETCore.App",
                "version": "8.0.25"
            },
            {

                "name": "Microsoft.AspNetCore.App",
                "version": "8.0.25"
            }
        ],
        "configProperties": {
            "System.GC.Server": true,
            "System.Reflection.Metadata.MetadataUpdater.IsSupported": false,            "System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": false
        }
    }
}

My installed dotnet runtimes are:

Given this setup, I keep getting the error: Path appears valid, but to something that is not Hops related.

When I start Rhino and open my GH definition, Hops seems to work fine for a while. However, I see that when the rhino.compute instance (the one bundled with Rhino locally) goes to sleep afeter some period of inactivity or when locking my laptop, I consistently get this error.

Based on this topic Error: Path appears valid, but to something that is not Hops related - Grasshopper / Hops - McNeel Forum I see that the rollForward setting might do something

I do believe modifying the *.runtimeconfig.json files can be the fix since Hops is at least working to some extent (it didn’t at all before). However, I am still missing something.

My latest insight is that the runtimeconfig of compute.geometry looks a bit different than the rhino.compute one. I specifically notice the frameworksattribute for compute.geometry vs. the includedFrameworks attribute for rhino.compute. Could this be something I need to look into?

I have tried to investigate some more why Hops stops working after the child proces goes idle. I had a look at the Rhino compute code on Github and had AI help me write a report. Maybe it helps us look in the right direction for a fix.

It mentioned the following:

After an idle period, compute.geometry (child process on port 6001) shuts itself down as expected via the idle span mechanism. However, when a new Hops request comes in afterwards, rhino.compute (parent on port 6500) fails with a 500 error / HttpRequestException: Connection refused (localhost:6001) instead of spawning a new child process.

Expected behavior: rhino.compute should detect the dead child, remove it from the process pool, spawn a replacement, and fulfill the request.

Actual behavior: The request fails immediately with connection refused. Subsequent requests also fail. Recovery requires manually restarting the service.

Root cause (from source code analysis of compute.rhino3d):

There are two issues working together:

  1. Race condition in ComputeChildren.GetComputeServerBaseUrl() (ComputeChildren.cs):
    The round-robin scheduler checks Process.HasExited to determine if a child is alive. However, when compute.geometry calls app.StopAsync() during idle shutdown, the TCP port is closed first (socket stops listening), but the OS process may still be running briefly during its shutdown sequence. This creates a window where HasExited returns false (process still alive) but port 6001 is already closed — so the request gets forwarded to a dead socket.

  2. Missing error handling in ReverseProxyHandler() (ReverseProxy.cs):
    The proxy handler only catches ConnectionResetException and OperationCanceledException. It does not catch HttpRequestException (which is what you get on connection refused). So instead of removing the dead child from the queue and retrying with a fresh spawn, the exception propagates as a 500 error to the caller.

How the idle shutdown triggers it:

In Shutdown.cs, the child process periodically asks the parent GET /idlespan — “how long since your last proxied request?” If the parent’s idle span (plus a 10% buffer) exceeds the configured limit, the child shuts down. This is by design. The problem is that nothing in the parent notices or reacts to the child being gone until a request actually comes in, and at that point the race condition above prevents proper recovery.

Suggested fixes (any one of these would resolve it):

  1. Catch HttpRequestException in ReverseProxyHandler — on connection refused, evict the child from the queue and retry via GetComputeServerBaseUrl() (which would then see HasExited == true and trigger LaunchCompute()).

  2. Add a port-liveness check in GetComputeServerBaseUrl() — check IsPortOpen(port) in addition to Process.HasExited before considering a child alive. The IsPortOpen helper already exists in the codebase.

  3. Add a retry loop in the proxy handler with a configurable retry count (e.g., 1-2 retries) that on connection failure forces a re-evaluation of the child pool.

Reproduction steps:

  1. Start rhino.compute with --spawn-on-startup and an idle span configured (e.g., --idlespan 3600)

  2. Let the system sit idle longer than the configured idle span

  3. Observe compute.geometry logging “Idle span limit reached” and shutting down

  4. Send a Hops request from Grasshopper

  5. Result: 500 error with “connection refused” on localhost:6001

After investigating some more. I believe the %AppData%\McNeel\Rhinoceros\packages\8.0\Hops\0.16.28\rhino.compute\rhino.compute.runtimeconfig.json should just be:

{
"runtimeOptions": {
"tfm": "net8.0",
"includedFrameworks": [
{
"name": "Microsoft.NETCore.App",
"version": "8.0.0"
},
{

"name": "Microsoft.AspNetCore.App",
"version": "8.0.0"
}
],
"configProperties": {
"System.GC.Server": true,
"System.Reflection.Metadata.MetadataUpdater.IsSupported": false, "System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": false
}
}
}

This file shouldn’t have been edited since this concerns a bundled .NET runtime with the rhino.compute application.

For %AppData%\McNeel\Rhinoceros\packages\8.0\Hops\0.16.18\compute.geometry\computeg.geometry.runtimeconfig.jsoni have tried setting the rollForward to LatestMinor to make sure it always uses the latest .NET 8 version available.

Unfortunately, these fixes still didn’t resolve the issue. After an hour when the idlespan time limit is reached, hops starts giving the error: “Path appears valid, but to something that is not Hops related” again. In the Rhino.compute console I see:

CG [09:59:44 DBG] The elapsed time has exceeded the idle span limit
CG [09:59:44 DBG] Checking with parent process at ``http://localhost:6500/idlespan``.
RC [09:59:44 DBG] Request received to /idlespan endpoint
RC [09:59:44 INF] HTTP GET /idlespan responded 200 in 3.7735 ms
CG [09:59:44 DBG] Response received. Parent process idle span is 3594 seconds.
CG [09:59:44 DBG] Idle span limit reached
CG [09:59:44 DBG] Shutting down child process
CG [09:59:44 INF] Total elapsed time for child process is 00 days, 01 hrs, 00 mins, 01 secs
CG [09:59:44 DBG] Hosting stopping
CG [09:59:44 INF] Application is shutting down…
CG [09:59:44 DBG] Hosting stopping
CG [09:59:44 DBG] Hosting stopped
CG [09:59:44 DBG] Hosting stopped
RC [10:00:38 INF] Max concurrent requests = 0
RC [10:00:50 ERR] HTTP POST /grasshopper responded 500 in 4096.8813 ms
System.Net.Http.HttpRequestException: Kan geen verbinding maken omdat de doelcomputer de verbinding actief heeft geweigerd. (localhost:6001)
—> System.Net.Sockets.SocketException (10061): Kan geen verbinding maken omdat de doelcomputer de verbinding actief heeft geweigerd.
at System.Net.Sockets.Socket.AwaitableSocketAsyncEventArgs.ThrowException(SocketError error, CancellationToken cancellationToken)
at System.Net.Sockets.Socket.AwaitableSocketAsyncEventArgs.System.Threading.Tasks.Sources.IValueTaskSource.GetResult(Int16 token)
at System.Net.Sockets.Socket.g__WaitForConnectWithCancellation|285_0(AwaitableSocketAsyncEventArgs saea, ValueTask connectTask, CancellationToken cancellationToken)
at System.Net.Http.HttpConnectionPool.ConnectToTcpHostAsync(String host, Int32 port, HttpRequestMessage initialRequest, Boolean async, CancellationToken cancellationToken)
— End of inner exception stack trace —
at System.Net.Http.HttpConnectionPool.ConnectToTcpHostAsync(String host, Int32 port, HttpRequestMessage initialRequest, Boolean async, CancellationToken cancellationToken)
at System.Net.Http.HttpConnectionPool.ConnectAsync(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken)
at System.Net.Http.HttpConnectionPool.CreateHttp11ConnectionAsync(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken)
at System.Net.Http.HttpConnectionPool.AddHttp11ConnectionAsync(QueueItem queueItem)
at System.Threading.Tasks.TaskCompletionSourceWithCancellation`1.WaitWithCancellationAsync(CancellationToken cancellationToken)
at System.Net.Http.HttpConnectionPool.SendWithVersionDetectionAndRetryAsync(HttpRequestMessage request, Boolean async, Boolean doRequestAuth, CancellationToken cancellationToken)
at System.Net.Http.DiagnosticsHandler.SendAsyncCore(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken)
at System.Net.Http.HttpClient.g__Core|83_0(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationTokenSource cts, Boolean disposeCts, CancellationTokenSource pendingRequestsCts, CancellationToken originalCancellationToken)
at rhino.compute.ReverseProxyModule.SendProxyRequest(HttpRequest initialRequest, HttpMethod method, String baseurl) in D:\BuildAgent\work\4852f584398a78e4\src\rhino.compute\ReverseProxy.cs:line 145
at rhino.compute.ReverseProxyModule.ReverseProxyGrasshopper(HttpRequest req, HttpResponse res) in D:\BuildAgent\work\4852f584398a78e4\src\rhino.compute\ReverseProxy.cs:line 201
at Serilog.AspNetCore.RequestLoggingMiddleware.Invoke(HttpContext httpContext)

This for me also points to a bug with restarting the the compute.geometry service after the idlespan time limit is reached.