> ## Knowledge Base Index
> Fetch the complete knowledge base index at: https://helpdesk.ggcircuit.com/sitemap.xml
> Use this file to discover available pages before exploring further.
> Pure-Markdown content can be obtained by appending a '.md' suffix to the content URLs listed in the sitemap (without the trailing slash).

# ggRock client fails to connect to the server (SignalR negotiate returns 405)

# ggRock client fails to connect to the server (SignalR negotiate returns 405)
**A redirect in the ggRock server's Nginx config can stop the ggRock client from communicating with the server, which appears as the client failing to start and looping. This article explains why it happens, how to confirm it, and how to fix it.**

## Symptoms
* The ggRock client fails to start and immediately shuts down, looping on every boot.
* The client cannot communicate with the ggRock server, so any function that depends on that connection stops working.
* The client log shows a SignalR negotiation failure against the hub, for example:
```
[ERR] Failed to start connection. Error getting negotiation response from "https://<SERVER>/hubs/clientHub".
System.Net.Http.HttpRequestException: Response status code does not indicate success: 405 (Method Not Allowed).
...
The HostOptions.BackgroundServiceExceptionBehavior is configured to StopHost ... the IHost instance is stopping.
[INF] Application is shutting down...

```
The `405` on the negotiate request is the key signal. Because the host is set to **StopHost** on a background-service exception, the whole client process exits and keeps retrying.

## Why it happens
SignalR's `negotiate` endpoint only accepts **POST**. When a client follows an HTTP **301/302** redirect, it downgrades the request from POST to **GET** (only 307/308 preserve the method). The result:
1. The client POSTs to `/hubs/clientHub/negotiate`.
2. Nginx returns **301** to the canonical hostname.
3. The client follows the redirect, downgrading POST to GET.
4. The GET hits the negotiate endpoint, which rejects non-POST with **405**.
5. The client crashes and restarts.
This is triggered when the client connects to the server **by IP address**. An IP connection sends no SNI and a `Host:` header of the IP, so Nginx does not match the named server block and falls through to a **catch-all / `default_server`** block. If that block performs a redirect (commonly an "IP/alt-host → canonical hostname" redirect), every IP-connecting client is affected.
|| The canonical hostname works because it matches `server_name` and SNI, so Nginx routes it straight to the backend instead of the redirect block.

## Confirm the cause
Run these from an affected client. Use the server's IP for the first test and the canonical hostname for the second.
```
# By IP (no SNI) - reproduces what the client does
curl.exe -k -i -X POST "https://<SERVER_IP>/hubs/clientHub/negotiate?negotiateVersion=1"

# By hostname - what a healthy path looks like
curl.exe -i -X POST "https://ggrock.example.com/hubs/clientHub/negotiate?negotiateVersion=1"

```
Interpret the responses:
| Request | Response | Meaning |
| ---- |
| POST via IP | `301 Moved Permanently` + `Location:` | Hitting the catch-all redirect — this is the problem |
| POST via IP, followed (`-L`) | `405 Method Not Allowed` | Redirect downgraded POST to GET |
| POST via hostname | `401 Unauthorized` + `WWW-Authenticate: Bearer` | **Healthy** — reached the backend; auth is simply missing |
| POST via hostname | `301` or `405` | Still misrouted; recheck the config |
A healthy negotiate endpoint answers with **401** (or a JSON payload), never **301** or **405**.
| `-k` is required when testing by IP because the certificate is issued for the hostname, not the IP. Use the hostname (and drop `-k`) to also validate the certificate.

## Fix
Make the real ggRock server block the **`default_server`** so IP / no-SNI connections are proxied to the backend instead of redirected, and remove the catch-all redirect.
**Before** — a catch-all redirect block (the cause):
```
# Catch-all: any connection whose SNI matches no server_name lands here.
# IP / no-SNI clients get redirected, which breaks the SignalR POST.
server {
    listen 443 ssl default_server;
    listen [::]:443 ssl default_server;
    server_name _;
    include snippets/ggrock-cert.conf;
    return 301 https://ggrock.example.com$request_uri;
}

```
**After** — move `default_server` onto the canonical server block:
```
server {
    listen      443 ssl http2 default_server;
    listen [::]:443 ssl http2 default_server;
    server_name ggrock.example.com;
    include snippets/ggrock-cert.conf;

    # ... existing ssl_*, gzip, client_max_body_size, etc. ...

    location / {
        proxy_pass http://localhost:5000;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Host $http_host;
    }

    location /hubs {
        proxy_pass http://localhost:5000/hubs;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $http_connection;
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
    }
}

```
Then **delete** the `server_name _;` catch-all block.
||| Only one server block per port can be `default_server`. Two `default_server` entries on 443 will stop Nginx from starting.
If you still want an alternate hostname to redirect to the canonical one, don't keep it as the catch-all — convert it to a **named** block so it only fires for that specific name:
```
server {
    listen      443 ssl;
    listen [::]:443 ssl;
    server_name ggrock-alt.example.com;
    include snippets/ggrock-cert.conf;
    return 301 https://ggrock.example.com$request_uri;
}

```
|| Over HTTPS, the certificate is presented during the TLS handshake **before** the 301 can be sent. If the cert is not valid for the alternate name, that redirect will throw a certificate warning first. The redirect is clean over plain HTTP (port 80).

## Apply and verify
```
nginx -t && systemctl reload nginx

```
Re-test the negotiate endpoint by IP — it should now return **401**, not 301/405:
```
curl.exe -k -i -X POST "https://<SERVER_IP>/hubs/clientHub/negotiate?negotiateVersion=1"

```
Restart the ggRock client (or reboot the machine). The shutdown loop should stop and the client should connect and resume communicating with the server.

## Notes and prevention
* **This is a connectivity failure at its root.** Any operation that depends on the client communicating with the ggRock server (machine management, status reporting, domain join, and other server-mediated functions) will fail until the connection is restored. Fixing the client's connection restores those functions.
* **The most durable fix is to point clients at the hostname the certificate is valid for.** When clients use the hostname, the redirect never applies. Making the canonical block the `default_server` covers IP / no-SNI clients regardless.
||| A package update can overwrite the bundled Nginx config and re-introduce a catch-all redirect. Keep the corrected config saved so you can re-apply it, or place your changes where updates won't overwrite them, and re-test client connectivity after any ggRock update.