Skip to content

Commit ff918cc

Browse files
committed
Force Long Polling for SignalR when custom headers set
Browsers can't send custom headers over WebSocket or SSE, so the plugin now forces Long Polling transport when API key headers are configured to ensure headers are sent with each request. Added a Playwright test to verify Long Polling is used and headers are present in polling requests.
1 parent c632276 commit ff918cc

2 files changed

Lines changed: 72 additions & 0 deletions

File tree

src/SignalR.OpenApi.SwaggerUi/Resources/signalr-openapi-plugin.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,15 @@ var SignalROpenApiPlugin = function (system) {
258258
});
259259
}
260260

261+
// Browsers cannot send custom HTTP headers on WebSocket or
262+
// Server-Sent Events connections. When custom headers are
263+
// configured, fall back to Long Polling which includes headers
264+
// with every HTTP request so the server can read them on each
265+
// hub invocation.
266+
if (options.headers && Object.keys(options.headers).length > 0) {
267+
options.transport = signalR.HttpTransportType.LongPolling;
268+
}
269+
261270
var connection = new signalR.HubConnectionBuilder()
262271
.withUrl(hubPath, options)
263272
.withAutomaticReconnect()

test/SignalR.OpenApi.Tests/SwaggerUiApiKeyPlaywrightTests.cs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,69 @@ public async Task SwaggerUi_ApiKeyAuthorizedThenInvokeSucceeds()
197197
$"Should not get connection error. Body: {bodyText}\nConsole logs:\n{allLogs}");
198198
}
199199

200+
/// <summary>
201+
/// Verifies that when apiKey headers are authorized, the plugin uses
202+
/// Long Polling transport so that custom headers are sent with every
203+
/// HTTP request (browsers cannot send custom headers on WebSocket).
204+
/// </summary>
205+
/// <returns>A <see cref="Task"/> representing the asynchronous test.</returns>
206+
[TestMethod]
207+
public async Task SwaggerUi_ApiKeyHeadersForceLongPollingTransport()
208+
{
209+
const string headerValue = "test-session-lp";
210+
211+
await Page.GotoAsync($"{baseUrl}/signalr-swagger/index.html");
212+
213+
var operationBlock = Page.Locator(".opblock");
214+
await operationBlock.First.WaitForAsync(new() { Timeout = 15000 });
215+
216+
// Authorize with the apiKey value
217+
await AuthorizeApiKeyAsync(headerValue);
218+
219+
// Track all requests to the hub endpoint to detect transport type.
220+
// Long Polling sends repeated GET requests to the hub URL with the
221+
// custom header. WebSocket would show a single upgrade request without it.
222+
var pollRequestsWithHeader = 0;
223+
var webSocketUpgradeDetected = false;
224+
225+
Page.Request += (_, request) =>
226+
{
227+
var url = request.Url;
228+
229+
// Detect WebSocket upgrade (SignalR WebSocket transport)
230+
if (url.Contains("/hubs/basic") && request.IsNavigationRequest is false
231+
&& request.ResourceType == "websocket")
232+
{
233+
webSocketUpgradeDetected = true;
234+
}
235+
236+
// Detect Long Polling GET requests carrying the custom header
237+
if (url.Contains("/hubs/basic") && request.Method == "GET"
238+
&& !url.Contains("/negotiate"))
239+
{
240+
var header = request.Headers.GetValueOrDefault(TestHeaderName.ToLowerInvariant());
241+
if (header == headerValue)
242+
{
243+
Interlocked.Increment(ref pollRequestsWithHeader);
244+
}
245+
}
246+
};
247+
248+
// Execute a hub method to trigger connection + invocation
249+
await ExecuteSendMessageAsync();
250+
251+
// Wait for response and a few poll cycles
252+
await Page.WaitForTimeoutAsync(5000);
253+
254+
Assert.IsFalse(
255+
webSocketUpgradeDetected,
256+
"Should not use WebSocket transport when apiKey headers are configured.");
257+
258+
Assert.IsTrue(
259+
pollRequestsWithHeader > 0,
260+
"Long Polling requests should include the custom header.");
261+
}
262+
200263
private static int GetAvailablePort()
201264
{
202265
using var listener = new TcpListener(IPAddress.Loopback, 0);

0 commit comments

Comments
 (0)