8000 Store outbox errors that contain null characters or exceed the column in bytes by Semih702 · Pull Request #54320 · keycloak/keycloak · GitHub
[go: up one dir, main page]

Skip to content

Store outbox errors that contain null characters or exceed the column in bytes - #54320

Merged
ssilvert merged 3 commits into
keycloak:mainfrom
Semih702:k5-outbox-last-error
Oct 27, 2026
Merged

ssilvert merged 3 commits into
keycloak:mainfrom
Semih702:k5-outbox-last-error

Conversation

@Semih702
@Semih702 Semih702 commented Oct 25, 2026 •
Copy link
Copy Markdown
Contributor

Closes #54319

OutboxStore.truncateError now makes sure LAST_ERROR can always be stored:

  • it removes U+0000, which PostgreSQL cannot store in text;
  • it limits the message to 2048 UTF-8 bytes instead of 2048 characters, because Oracle (byte semantics) and SQL Server with a UTF-8 collation measure VARCHAR(2048) in bytes. It cuts at a code point boundary and keeps the ... marker.

Messages without U+0000 that fit in 2048 bytes are stored unchanged. No schema change.

SsfPushDeliveryHandler.truncateDetail no longer splits a surrogate pair, and its Javadoc now says the final cap is the store's, in UTF-8 bytes.

Tests:

OutboxStoreTests#recordFailure* dev-mem postgres mssql
without the change fails (assertion only; H2 stores the value) fails (invalid byte sequence for encoding "UTF8": 0x00) fails (string truncation)
with the change pass pass pass

End to end, before the change, a receiver answering 500 with such a body made the healthy receiver of the same event get the push 10 extra times in 5 s (PostgreSQL with U+0000, SQL Server with 1,100 × é). I checked that locally with SSF tests that are not part of this PR, as ssf/tests/base runs only on dev-mem in CI.

This also applies on top of #54139, which keeps truncateError as it is.

AI disclosure: Claude Code was used to assist with analysis, reproduction and implementation. I understand every change and can revise it.

… in bytes

A receiver's error body with U+0000 (PostgreSQL) or more than 2048 UTF-8
bytes (Oracle, SQL Server with a UTF-8 collation) could not be stored in
LAST_ERROR. That rolled back the whole drainer tick, so other rows were
pushed again on every tick and the failing row was never dead-lettered.

Closes keycloak#54319

Signed-off-by: Semih702 <semih702do@gmail.com>
@Semih702
Semih702 requested a review from a team as a code owner October 25, 2026 13:35
Copilot AI balanced review requested due to automatic review settings October 25, 2026 13:35
Copilot AI left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The focused implementation addresses the persistence constraints with regression coverage and no identified blocking issues.

No open issues 🎉

What changed in this PR

Fixes #54319 by making outbox error messages safe to persist across supported databases, preventing failed error writes from stalling SSF delivery. No schema changes are required.

Changes:

  • Removes U+0000 and caps errors at 2048 UTF-8 bytes, preserving code point boundaries and the truncation marker.
  • Adds persistence coverage for null characters and multibyte messages.
  • Adds SSF regression tests verifying that problematic receiver responses do not cause duplicate pushes to healthy receivers.
File Description
tests/​base/​src/​test/​java/​org/​keycloak/​tests/​events/​outbox/​OutboxStoreTests.java Tests sanitized, byte-limited error persistence.
ssf/​tests/​base/​src/​test/​java/​org/​keycloak/​tests/​ssf/​transmitter/​SsfTransmitterPushFailureTests.java Tests healthy delivery alongside receivers returning problematic errors.
model/​jpa/​src/​main/​java/​org/​keycloak/​events/​outbox/​OutboxStore.java Sanitizes null characters and truncates errors by UTF-8 byte length.

Review effort: Balanced

@ssilvert ssilvert left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the fix. Stripping null characters and truncating by UTF-8 bytes without splitting a code point does what #54319 asks. A few things to address before merging:

  1. Literal NUL bytes in the test sources. See the inline comment.

  2. The new tests can't catch a regression in CI. See the earlier comment.

  3. An existing test still checks characters against what is now a byte limit. See the inline comment.

  4. SsfPushDeliveryHandler.truncateDetail is now out of step with the store. That file isn't in this PR, so the comment is here instead of inline. truncateDetail still caps the detail at 1024 UTF-16 chars with substring:

    • Its Javadoc (line 178) says the VARCHAR(2048) column "comfortably absorbs the prefix + url + detail". Now that the limit is in bytes, that's no longer true: 1024 three-byte characters are about 3 KB, so OutboxStore.truncateError always ends up doing the real truncation for non-ASCII bodies.
    • substring(0, 1024) can split a surrogate pair and leave a lone high surrogate, which is stored as ? or U+FFFD.

    At minimum the Javadoc should be updated. Ideally truncateDetail would also avoid splitting a surrogate pair (for example, back off one char when Character.isHighSurrogate(detail.charAt(1023))).

…rom splitting surrogates

Closes keycloak#54319

Signed-off-by: Semih702 <semih702do@gmail.com>
Copilot AI balanced review requested due to automatic review settings October 26, 2026 19:53
Copilot AI left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The advertised SSF end-to-end regression tests are missing, leaving healthy-receiver duplicate prevention unverified.

1 open issue

Review effort: Balanced

@Semih702
Semih702 commented Oct 26, 2026 •
Copy link
Copy Markdown
Contributor Author

Thanks for the review! All four points are addressed in 676f724 and 6fb508a.

truncateDetail no longer splits a surrogate pair, its Javadoc now says the final cap is the store's (in UTF-8 bytes), and it has a small unit test. The constant is renamed to MAX_LAST_ERROR_BYTES.

The cap is in UTF-8 bytes since the previous commits.

Closes keycloak#54319

Signed-off-by: Semih702 <semih702do@gmail.com>
Copilot AI balanced review requested due to automatic review settings October 26, 2026 21:11
Copilot AI left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The focused changes address the reported persistence failures with regression coverage and no identified blocking issues; tests were not executed during this review.

No open issues 🎉

1 resolved since last review

Review effort: Balanced

@sguilhen sguilhen left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR @Semih702 , your latest changes look good to me. Asking @ssilvert to re-review.

@sguilhen
sguilhen requested a review from ssilvert October 27, 2026 13:25
@ssilvert ssilvert left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, all four points are addressed. I checked the Store IT logs: recordFailure_storesErrorWithNullCharacterAndMultiByteCharacters runs on postgres, mssql, mysql and mariadb, and is skipped on oracle as intended.

@ssilvert
ssilvert merged commit df9c635 into keycloak:main Oct 27, 2026
98 checks passed
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.

SSF push outbox stalls when a receiver's error response cannot be stored in LAST_ERROR

4 participants

0