Skip to content

Possible verbatim string confusion? #3032

Description

@mgravell

I've seen the following repeatedly today only against CI servers, on the main branch (mentioning to rule out any V3/V2 stuff):

[xUnit.net 00:00:20.13]     ConfigTests.GetInfo (RESP3) [FAIL]

Error: Expected CPU, got miscellaneous
txt=# CPU
used_cpu_sys=1.937500
used_cpu_user=0.859375
used_cpu_sys_children=0.015625
used_cpu_user_children=0.000000
used_cpu_sys_main_thread=1.937500
used_cpu_user_main_thread=0.859375

This is interesting because a RESP3 verbatim string would be:

={count}\r\n{kind}:{payload}\r\n, where kind is always exactly 3 characters, for example =16\r\ntxt:blahblahblah\r\n.

The server should not be issuing txt=... it should be txt:... - so... either the server has started responding differently, or we're borking something on the inbound.

Relevant CI is using Redis version=8.6.1, in particular in the Windows Server 2022 build.

CI starts this server in WSL with:

https://github.com/StackExchange/StackExchange.Redis/actions/runs/22959665638/workflow#L67-L75

  run: |
        apt-get update
        apt-get install curl gpg lsb-release libgomp1 jq -y
        curl -fsSL https://packages.redis.io/gpg | gpg --dearmor -o /usr/share/keyrings/redis-archive-keyring.gpg
        chmod 644 /usr/share/keyrings/redis-archive-keyring.gpg
        echo "deb [signed-by=/usr/share/keyrings/redis-archive-keyring.gpg] https://packages.redis.io/deb $(lsb_release -cs) main" | tee /etc/apt/sources.list.d/redis.list
        apt-get update
        apt-get install -y redis
        mkdir redis

I'm not sure there's anything to do on this right now other than keep an eye on it.

Activity

  1. mgravell commented on Sep 16, 2026

    @mgravell
    CollaboratorAuthor

    Found it, and the server was innocent.

    The txt= in that log is the test's own formatting, not the wire — ConfigTests.cs builds the failure message
    with sb.Append(setting.Key).Append('='). So the real observation is a parsed pair of ("txt", "# CPU"),
    which means the line the INFO parser saw was txt:# CPU: the verbatim kind prefix survived into the payload.
    That line then fails the StartsWith("# ") section test, gets split on the colon into a key/value pair, and
    with no section header ever recognised every field lands under miscellaneous. Hence "expected CPU, got
    miscellaneous" with all the data present.

    The cause was in RawResult.GetString(out verbatimPrefix):

    if (Payload.IsSingleSegment)
    {
        s = Format.GetString(Payload.First.Span);
        return Resp3Type == ResultType.VerbatimString ? GetVerbatimString(s, out verbatimPrefix) : s;
    }
    #if NET6_0_OR_GREATER
        // use system-provided sequence decoder
        return Encoding.UTF8.GetString(in _payload);      // <-- never calls GetVerbatimString
    #else
        ...
        return Resp3Type == ResultType.VerbatimString ? GetVerbatimString(s, out verbatimPrefix) : s;
    #endif

    The multi-segment path on .NET 6+ skipped the strip entirely. Single-segment replies were fine, and so was the
    down-level branch — so it fired only when the INFO reply happened to straddle a buffer boundary, which is why
    it looked flaky, machine-specific and server-shaped. It also left verbatimPrefix empty on that path.

    Believed fixed in v3: RESPite's RespReader.ReadString linearises via Buffer(...) before inspecting the
    kind prefix, so single- and multi-segment payloads take the same path and the segment-dependence is gone.
    Confirmed locally with a throwaway [Theory, Resp(...)] over a verbatim INFO frame — the attribute generates
    right-sized, oversized, every two-chunk split point (including inside txt:) and chunk-per-byte, 43 variants,
    all green; and it does fail if the strip is disabled, so it wasn't passing vacuously. Not keeping the test:
    the defect lived in RawResult, which no longer exists, so it would guard a different code path than the one
    that broke.

    Worth noting for next time: any single-segment test of verbatim handling would have passed throughout this.
    The exhaustive splitting in the Resp attribute is what makes this class of bug findable.

    Closing as fixed in v3.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions