In production (learn.spikersoft.com calling api.spikersoft.com), starting a video call from the menu picker immediately failed with the snack:
Couldn't invite that user. Please try again.
Browser console:
WebSocket connection to 'wss://api.spikersoft.com/hubs/video-call?access_token=...' failed.
Error: Failed to start the connection: Error: WebSocket failed to connect. The connection could
not be found on the server, either the endpoint may not be a SignalR endpoint, the connection ID
is not present on the server, or there is a proxy blocking WebSockets. If you have multiple
servers check that sticky sessions are enabled.
Repro
Deploy backend with the new VideoCallHub mapped at /hubs/video-call (Phase 1 of the bulletproof-video plan).
From any logged-in browser session, open Start a Video Call from the user menu.
Pick another online user.
WebSocket upgrade to /hubs/video-call?access_token=... closes immediately, picker shows the snack above.
Root cause
SpikerSoft.Api/Infrastructure/Authentication.cs -> IsSignalRHubPath(PathString) is the allowlist that tells the JWT bearer middleware to promote ?access_token=... (the only way SignalR's WebSocket transport can pass a bearer token — you can't set Authorization headers during a WebSocket upgrade) into context.Token.
The new /hubs/video-call endpoint was never added to that list. So at runtime:
JWT middleware ignores the query param because the path isn't on the allowlist.
WebSocket arrives at the hub anonymous.
[Authorize] on VideoCallHub rejects it.
Connection closes instantly. SignalR surfaces its standard misleading "endpoint may not be a SignalR endpoint / proxy blocking WebSockets" error — which is what tripped the picker snack.
Same latent bug existed for /hubs/pre-registration, /hubs/dashboard, /hubs/cluster — they were mapped in WebApplicationExtensions.ConfigureSignalRHubs but also missing from the allowlist.
Impact
Severity: P1 — feature is 100% broken in production for all users.
Users affected: anyone trying to start a video call.
Workaround: none.
How we'll prevent regressions
Left a comment in IsSignalRHubPath explaining that every authenticated hub MUST be added here and what the failure mode looks like, so the next person mapping a hub doesn't have to rediscover this from misleading SignalR errors.
## Symptom
In production (`learn.spikersoft.com` calling `api.spikersoft.com`), starting a video call from the menu picker immediately failed with the snack:
> Couldn't invite that user. Please try again.
Browser console:
```
WebSocket connection to 'wss://api.spikersoft.com/hubs/video-call?access_token=...' failed.
Error: Failed to start the connection: Error: WebSocket failed to connect. The connection could
not be found on the server, either the endpoint may not be a SignalR endpoint, the connection ID
is not present on the server, or there is a proxy blocking WebSockets. If you have multiple
servers check that sticky sessions are enabled.
```
## Repro
1. Deploy backend with the new `VideoCallHub` mapped at `/hubs/video-call` (Phase 1 of the bulletproof-video plan).
2. From any logged-in browser session, open `Start a Video Call` from the user menu.
3. Pick another online user.
4. WebSocket upgrade to `/hubs/video-call?access_token=...` closes immediately, picker shows the snack above.
## Root cause
`SpikerSoft.Api/Infrastructure/Authentication.cs` -> `IsSignalRHubPath(PathString)` is the allowlist that tells the JWT bearer middleware to promote `?access_token=...` (the only way SignalR's WebSocket transport can pass a bearer token — you can't set `Authorization` headers during a WebSocket upgrade) into `context.Token`.
The new `/hubs/video-call` endpoint was never added to that list. So at runtime:
1. Browser opens `wss://.../hubs/video-call?access_token=<jwt>`.
2. JWT middleware ignores the query param because the path isn't on the allowlist.
3. WebSocket arrives at the hub **anonymous**.
4. `[Authorize]` on `VideoCallHub` rejects it.
5. Connection closes instantly. SignalR surfaces its standard misleading "endpoint may not be a SignalR endpoint / proxy blocking WebSockets" error — which is what tripped the picker snack.
Same latent bug existed for `/hubs/pre-registration`, `/hubs/dashboard`, `/hubs/cluster` — they were mapped in `WebApplicationExtensions.ConfigureSignalRHubs` but also missing from the allowlist.
## Impact
- **Severity:** P1 — feature is 100% broken in production for all users.
- **Users affected:** anyone trying to start a video call.
- **Workaround:** none.
## How we'll prevent regressions
Left a comment in `IsSignalRHubPath` explaining that **every authenticated hub MUST be added here** and what the failure mode looks like, so the next person mapping a hub doesn't have to rediscover this from misleading SignalR errors.
spikerj
added the bug label 2026-05-10 23:42:44 +00:00
Fixed in SpikerSoft.Api/Infrastructure/Authentication.cs by adding /hubs/video-call (plus three other already-mapped hubs that were silently broken for the same reason: /hubs/pre-registration, /hubs/dashboard, /hubs/cluster) to the IsSignalRHubPath allowlist:
privatestaticboolIsSignalRHubPath(PathStringpath){// SignalR's WebSocket transport can't set Authorization headers, so// the client passes the bearer token via `?access_token=...` and we// promote it to `context.Token` here. Every authenticated hub MUST// be listed below — if it isn't, the WebSocket upgrade comes in// anonymous, the hub's [Authorize] kicks it, and the client sees// a misleading "WebSocket failed to connect / endpoint may not be// a SignalR endpoint" error (which is exactly what the// bulletproof-video Phase 1 hub hit in production).returnpath.StartsWithSegments("/hubs/chat")||path.StartsWithSegments("/hubs/game")||path.StartsWithSegments("/hubs/notifications")||path.StartsWithSegments("/hubs/calendar-notifications")||path.StartsWithSegments("/hubs/quiz")||path.StartsWithSegments("/hubs/dungeon-crawler")||path.StartsWithSegments("/hubs/chess")||path.StartsWithSegments("/hubs/video-call")||path.StartsWithSegments("/hubs/pre-registration")||path.StartsWithSegments("/hubs/dashboard")||path.StartsWithSegments("/hubs/cluster")||path.StartsWithSegments("/codeExecutionHub");}
No frontend changes required. After the next backend deploy, the WebSocket upgrade carries the bearer token, [Authorize] on VideoCallHub is satisfied, and the picker's invite goes through.
Verification
SpikerSoft.Tests.Unit/SignalR/VideoCallHubTests.cs already covers the auth-required paths (anonymous invite is rejected, etc.) so any future regression where the hub becomes anonymous-callable would fail there.
Manual: wss://api.spikersoft.com/hubs/video-call?access_token=<jwt> should now complete its upgrade and the hub's OnConnectedAsync should log VideoCallHub: user <uid> connected.
Closing as fixed.
## Resolution
Fixed in `SpikerSoft.Api/Infrastructure/Authentication.cs` by adding `/hubs/video-call` (plus three other already-mapped hubs that were silently broken for the same reason: `/hubs/pre-registration`, `/hubs/dashboard`, `/hubs/cluster`) to the `IsSignalRHubPath` allowlist:
```csharp
private static bool IsSignalRHubPath(PathString path)
{
// SignalR's WebSocket transport can't set Authorization headers, so
// the client passes the bearer token via `?access_token=...` and we
// promote it to `context.Token` here. Every authenticated hub MUST
// be listed below — if it isn't, the WebSocket upgrade comes in
// anonymous, the hub's [Authorize] kicks it, and the client sees
// a misleading "WebSocket failed to connect / endpoint may not be
// a SignalR endpoint" error (which is exactly what the
// bulletproof-video Phase 1 hub hit in production).
return path.StartsWithSegments("/hubs/chat") ||
path.StartsWithSegments("/hubs/game") ||
path.StartsWithSegments("/hubs/notifications") ||
path.StartsWithSegments("/hubs/calendar-notifications") ||
path.StartsWithSegments("/hubs/quiz") ||
path.StartsWithSegments("/hubs/dungeon-crawler") ||
path.StartsWithSegments("/hubs/chess") ||
path.StartsWithSegments("/hubs/video-call") ||
path.StartsWithSegments("/hubs/pre-registration") ||
path.StartsWithSegments("/hubs/dashboard") ||
path.StartsWithSegments("/hubs/cluster") ||
path.StartsWithSegments("/codeExecutionHub");
}
```
No frontend changes required. After the next backend deploy, the WebSocket upgrade carries the bearer token, `[Authorize]` on `VideoCallHub` is satisfied, and the picker's invite goes through.
### Verification
- `SpikerSoft.Tests.Unit/SignalR/VideoCallHubTests.cs` already covers the auth-required paths (anonymous invite is rejected, etc.) so any future regression where the hub becomes anonymous-callable would fail there.
- Manual: `wss://api.spikersoft.com/hubs/video-call?access_token=<jwt>` should now complete its upgrade and the hub's `OnConnectedAsync` should log `VideoCallHub: user <uid> connected`.
Closing as fixed.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Symptom
In production (
learn.spikersoft.comcallingapi.spikersoft.com), starting a video call from the menu picker immediately failed with the snack:Browser console:
Repro
VideoCallHubmapped at/hubs/video-call(Phase 1 of the bulletproof-video plan).Start a Video Callfrom the user menu./hubs/video-call?access_token=...closes immediately, picker shows the snack above.Root cause
SpikerSoft.Api/Infrastructure/Authentication.cs->IsSignalRHubPath(PathString)is the allowlist that tells the JWT bearer middleware to promote?access_token=...(the only way SignalR's WebSocket transport can pass a bearer token — you can't setAuthorizationheaders during a WebSocket upgrade) intocontext.Token.The new
/hubs/video-callendpoint was never added to that list. So at runtime:wss://.../hubs/video-call?access_token=<jwt>.[Authorize]onVideoCallHubrejects it.Same latent bug existed for
/hubs/pre-registration,/hubs/dashboard,/hubs/cluster— they were mapped inWebApplicationExtensions.ConfigureSignalRHubsbut also missing from the allowlist.Impact
How we'll prevent regressions
Left a comment in
IsSignalRHubPathexplaining that every authenticated hub MUST be added here and what the failure mode looks like, so the next person mapping a hub doesn't have to rediscover this from misleading SignalR errors.Resolution
Fixed in
SpikerSoft.Api/Infrastructure/Authentication.csby adding/hubs/video-call(plus three other already-mapped hubs that were silently broken for the same reason:/hubs/pre-registration,/hubs/dashboard,/hubs/cluster) to theIsSignalRHubPathallowlist:No frontend changes required. After the next backend deploy, the WebSocket upgrade carries the bearer token,
[Authorize]onVideoCallHubis satisfied, and the picker's invite goes through.Verification
SpikerSoft.Tests.Unit/SignalR/VideoCallHubTests.csalready covers the auth-required paths (anonymous invite is rejected, etc.) so any future regression where the hub becomes anonymous-callable would fail there.wss://api.spikersoft.com/hubs/video-call?access_token=<jwt>should now complete its upgrade and the hub'sOnConnectedAsyncshould logVideoCallHub: user <uid> connected.Closing as fixed.