SignalR/SignalR · error · FormatException
Invalid cursor.
Error message
Invalid cursor.
What it means
Thrown by Cursor.GetCursors when the cursor string is null or empty. Cursors are opaque tokens SignalR uses to track how many messages a client has consumed from each topic/stream; the client echoes them back on reconnect. A null/empty cursor means the client sent nothing or the server-side caller bypassed normal flow. This is treated as a hard format error (unlike a wrong-prefix cursor, which returns null gracefully).
Source
Thrown at src/Microsoft.AspNet.SignalR.Core/Messaging/Cursor.cs:135
return sb.ToString();
}
public static List<Cursor> GetCursors(string cursor, string prefix)
{
return GetCursors(cursor, prefix, s => s);
}
public static List<Cursor> GetCursors(string cursor, string prefix, Func<string, string> keyMaximizer)
{
return GetCursors(cursor, prefix, (key, state) => ((Func<string, string>)state).Invoke(key), keyMaximizer);
}
public static List<Cursor> GetCursors(string cursor, string prefix, Func<string, object, string> keyMaximizer, object state)
{
// Technically GetCursors should never be called with a null value, so this is extra cautious
if (String.IsNullOrEmpty(cursor))
{
throw new FormatException(Resources.Error_InvalidCursorFormat);
}
// If the cursor does not begin with the prefix stream, it isn't necessarily a formatting problem.
// The cursor with a different prefix might have had different, but also valid, formatting.
// Null should be returned so new cursors will be generated
if (!cursor.StartsWith(prefix, StringComparison.Ordinal))
{
return null;
}
var signals = new HashSet<string>();
var cursors = new List<Cursor>();
string currentKey = null;
string currentEscapedKey = null;
ulong currentId;
bool escape = false;
bool consumingKey = true;
var sb = new StringBuilder();View on GitHub (pinned to 693053b89a)
Solutions
- Verify the client sends the cursor (messageId) query parameter on every reconnect/poll request after the initial negotiate.
- If you are calling GetCursors directly, guard against null/empty before calling and pass a valid cursor or skip the call.
- Check for proxy/load-balancer URL rewriting that strips query parameters.
- Update the client library to a version compatible with the server.
Example fix
// before — caller passes null cursor directly
var cursors = Cursor.GetCursors(null, prefix);
// after — guard before calling
if (String.IsNullOrEmpty(cursor)) {
cursors = null; // let the framework generate new cursors
} else {
cursors = Cursor.GetCursors(cursor, prefix);
} Defensive patterns
Strategy: validation
Validate before calling
// Check cursor before calling GetCursors
if (string.IsNullOrEmpty(cursor)) {
// Do not call GetCursors; return null to let framework generate new cursors
return null;
}
var cursors = Cursor.GetCursors(cursor, prefix); Try / catch
try {
var cursors = Cursor.GetCursors(cursor, prefix);
} catch (FormatException ex) when (ex.Message.Contains("Invalid cursor")) {
// treat as a fresh connection — generate new cursors
logger.Warn("Client sent empty/invalid cursor, regenerating", ex);
cursors = null;
} Prevention
- Guard against null/empty cursor before calling GetCursors.
- Ensure clients always send the cursor parameter after the initial negotiate handshake.
- Log missing-cursor occurrences to identify misbehaving clients.
When it happens
Trigger: GetCursors is invoked with a null or zero-length cursor argument. This can happen if a client reconnect request omits the cursor (messageId) query parameter entirely, or if internal code passes null instead of letting the framework generate a fresh cursor set.
Common situations: A misbehaving or very old client that does not send a message cursor on poll/reconnect; a custom host that strips the cursor parameter; a proxy that drops query-string parameters; manual testing with a hand-crafted URL missing the cursor.
Related errors
- SignalR: Invalid transport(s) specified, aborting start.
- Query string property must be either a string or object.
- No username specified!
- That's already your username...
- Username '{0}' is already taken!
AI-assisted analysis of SignalR/SignalR@693053b89a (2026-08-13).
Data as JSON: /api/errors/82893b710dc44162.
Report an issue: GitHub.