Skip to content

llmessage and llcorehttp: fix the five bugs their tests' follow-up found - #458

Merged
RyeMutt merged 5 commits into
rye/tm19-test-fixesfrom
rye/tm19-prod-fixes
Oct 12, 2026
Merged

RyeMutt merged 5 commits into
rye/tm19-test-fixesfrom
rye/tm19-prod-fixes

Conversation

@RyeMutt

@RyeMutt RyeMutt commented Oct 12, 2026

Copy link
Copy Markdown
Member

Description

Fixes the five bugs in llmessage's and llcorehttp's own code that #456 found while fixing their tests and left alone, since that change was to tests only. Each fix comes with a test that fails without it.

It stacks on #456 (rye/tm19-test-fixes), whose Catch2 tests these extend.

  • 6173726, LLBufferArray::splitAfter.
    • The bug: asked to split after a segment's last byte, it should leave the segment whole. It tested for the address one past the last byte instead, which getSegment never returns. So it split anyway and inserted the zero-length segment its comment says it avoids.
    • LLBufferStreamBuf::sync had come to rely on that. It splits after the last byte written and erases the segment after the split, which is the unused rest of the put area. When a write had filled its segment, what it erased was the empty segment. With the split fixed alone, it would erase whatever came next, another channel's data included, on every flush and every stream destruction. It now trims only a put area that has something left over.
  • a685bc1, BufferArrayStreamBuf::seekoff.
    • The bugs:
      • a seek to the end stopped on the last byte, so the next read returned that byte instead of EOF;
      • an offset from the end was subtracted rather than added;
      • a seek before the start wrapped round as a size_t;
      • over an empty BufferArray every seek failed;
      • a seek from the current position moved it even when the seek then failed.
    • The fix: both directions now share one computation. A seek before the start fails and changes nothing; past the end is the end.
    • Effect: nothing in the viewer seeks these streams. The test's seek to the end now puts back the '!', as the original TUT message said it should.
  • 35218b7, HTTP_RETRY_BACKOFF_MAX_DEFAULT.
    • The bug: it is commented "5 sec" and was 5E6L when MAINT-7495 set it. A 2024 change that rewrote these constants as integers typed 50000006UL, fifty seconds.
    • Effect: a request that sets no ceiling of its own has backed off since as if it had none. With the default five retries, it waited 1, 2, 4, 8 and 16 seconds rather than 1, 2, 4, 5 and 5.
    • The fix: the value is 5 seconds again. Two static_asserts keep the defaults in order and within the 20-second limit; the second fails on the old value.
  • ef08613, the ascii data packers' binary unpacks.
    • The bug: unpackBinaryData took the size a line gave and read that many bytes from the 512-byte copy of the line, wherever the line ended. It reported success with the size the line claimed. A line whose size did not parse left out_size as it was, which LLTextureAnim leaves uninitialised. unpackBinaryDataFixed read past a short line the same way.
    • The fix: both packers share one unpack that stops at the end of the line. A short line fails, and out_size says how much it gave.
  • 30e3faf, LLHTTPRegistrar::buildAllServices.
    • The bug: addNode refuses a node for a path that already has one and leaves the node with its caller, but it said nothing. buildAllServices forgot the node it had just built, leaking one for each path already present.
    • The fix: addNode returns whether it took the node, and buildAllServices deletes a node it refused.

Related Issues

  • Please link to a relevant GitHub issue for additional context.

Issue Link: none. These were found by #456 and listed in its notes.


Checklist

  • I have provided a clear title and detailed description for this pull request.
  • If useful, I have included media such as screenshots and video to show off my changes.
  • I have tested the changes locally and verified they work as intended.
  • All new and existing tests pass.
  • Code follows the project's style guidelines.
  • Documentation has been updated if needed.
  • Any dependent changes have been merged and published in downstream modules.
  • I have reviewed the contributing guidelines.

Additional Notes

  • Tested on macOS arm64, RelWithDebInfo:
    • the full build, viewer included, is clean;
    • all 447 tests of ctest -LE manual pass, and so do the 8 pending combined runs;
    • llmessage_tests and llcorehttp_tests each pass 10 random orders, with no state guard report.
  • Each new test fails without its fix. The data packer's tests were run against the old lldatapacker.cpp, and failed. Each other fix was undone in part: the split, sync's guard, each part of seekoff, the backoff value, the delete in buildAllServices, and addNode's refusal. Each time the test meant for it failed.
  • Merge order: this should follow Test framework, stage 3: llmessage's and llcorehttp's tests in Catch2 #455 and Test framework: fix the old test bugs in llmessage's and llcorehttp's tests #456. Once they merge, its base becomes develop.
  • Behaviour change in the viewer: only the backoff. Requests that set no ceiling of their own retry sooner, as MAINT-7495 intended.
  • Not changed, found on the way: addNode given a second wildcard under one parent, named differently from the first, replaces the first and leaks it rather than refusing it. Refusing it would change which node answers such paths, so it wants its own change.

🤖 Generated with Claude Code

RyeMutt and others added 5 commits October 12, 2026 14:42
…t byte

splitAfter meant to leave a segment whole when asked to split it after
its last byte, but tested for the address one past it, which getSegment
never returns. So it split anyway and inserted the zero-length segment
its comment says it avoids.

LLBufferStreamBuf::sync had come to rely on that. It splits after the
last byte written and erases the segment after the split, the part of
the put area the stream did not use. When a write had filled its
segment, the segment it erased was the empty one, so it erased nothing.
With the split fixed it would erase whatever came next, another
channel's data included, on every flush and on the stream's
destruction. It now trims only a put area with something left over.

llbuffer's tests check that a split after the last byte adds nothing,
and io's that a flush after filling the put segment keeps the segment
after it. Each fails without its half of the fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
seekoff clamped a read position to the last byte, so a seek to the end
left the next read returning that byte rather than EOF, and a putback
there putting back the byte before it. It also took an offset from the
end the wrong way, so seekg(-2, end) went two bytes past the end and
was clamped back, and it let a seek before the start wrap round as a
size_t. Over an empty BufferArray every seek failed, the end being one
byte before the start. A seek from the current position moved the read
position even when the seek then failed.

Both directions now work out where a seek lands the same way: from the
start, the current position or the end, adding the offset; before the
start fails and changes nothing, past the end is the end. A read
position at the end lies just past the last byte, in that byte's block.

Nothing in the viewer seeks one of these streams, so this changes no
behaviour there. bufferstream's tests check the end, an offset from it,
a write from it, a seek before the start, and an empty BufferArray. The
seek to the end now puts back the '!' that the TUT test's message said
it would, where the check had expected EOF.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
HTTP_RETRY_BACKOFF_MAX_DEFAULT is commented "5 sec" and was 5E6L when
MAINT-7495 set it, to stop the viewer retrying for too long. A 2024
change that wrote these constants as integers gave it as 50000006UL,
fifty seconds. A request that sets no ceiling of its own has backed off
since as if it had none: with the default five retries, after 1, 2, 4, 8
and 16 seconds instead of 1, 2, 4, 5 and 5. HttpOptions takes the same
default, clamped on its way into a request to the 20-second limit, which
is still above every one of those delays. It is 5 seconds again.

Two static_asserts hold the defaults in order and within that limit;
the second fails on the old value.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LLDataPackerAsciiBuffer's and LLDataPackerAsciiFile's unpackBinaryData
took the size a line gave, capped only by the caller's buffer, and read
that many bytes three characters at a time from the line, a 512-byte
copy on the stack, with no regard to where the line ended. A line that
claimed more bytes than it held was read on past its terminator and
past the copy, and the unpack reported success with the size it
claimed. A line whose size did not parse left out_size as it was, which
LLTextureAnim, among others, leaves uninitialised. unpackBinaryDataFixed
read the caller's count from its line the same way.

Both packers now share one unpack that stops where the line does. A line
with no size fails with out_size 0; one that holds fewer bytes than it
claims gives what it holds, fails, and says how many bytes it gave in
out_size. unpackBinaryDataFixed fails likewise when its line is short.
What a well-formed line unpacks to is unchanged.

lldatapacker's tests give each a line that claims more than it holds,
and one with no size. They fail on the old code, which reported success
for every one of them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
addNode refuses a node for a path that already has one and leaves it
with its caller, but said nothing, and buildAllServices passed it a node
fresh from a factory and forgot it. Building the services into a root
that has some of them already, as building twice does, leaked a node a
path.

addNode now returns whether it took the node, and buildAllServices
deletes one it refused. llhttpnode's tests check the refusal through the
return value, and that a second buildAllServices into the same root
leaves as many nodes alive as the first: it registers a node type that
counts itself. Both fail without the fix.

Not changed: a second wildcard under one parent, named differently from
the first, replaces it rather than being refused, and the first is
leaked. Refusing it would change which node answers such a path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 27 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 18b0ec99-4b9d-4958-9b07-7abc5475d080

📥 Commits

Reviewing files that changed from the base of the PR and between a4623f5 and 30e3faf.


📒 Files selected for processing (13)
  • indra/llcorehttp/_httpinternal.h
  • indra/llcorehttp/bufferstream.cpp
  • indra/llcorehttp/tests/bufferstream_test.cpp
  • indra/llmessage/llbuffer.cpp
  • indra/llmessage/llbufferstream.cpp
  • indra/llmessage/lldatapacker.cpp
  • indra/llmessage/llhttpnode.cpp
  • indra/llmessage/llhttpnode.h
  • indra/llmessage/tests/io_test.cpp
  • indra/llmessage/tests/llbuffer_test.cpp
  • indra/llmessage/tests/lldatapacker_test.cpp
  • indra/llmessage/tests/llhttpnode_stub.cpp
  • indra/llmessage/tests/llhttpnode_test.cpp

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@RyeMutt
RyeMutt added this pull request to stack #457 October 12, 2026 21:12
@RyeMutt
RyeMutt merged commit 8337ab0 into develop Oct 12, 2026
21 checks passed
@RyeMutt
RyeMutt deleted the rye/tm19-prod-fixes branch October 12, 2026 23:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant