Skip to content

Validate and encode subjects in the bytes domain - #983

Draft
caspervonb wants to merge 2 commits into
mainfrom
subject-bytes-canonical
Draft

Validate and encode subjects in the bytes domain#983
caspervonb wants to merge 2 commits into
mainfrom
subject-bytes-canonical

Conversation

@caspervonb

Copy link
Copy Markdown
Collaborator

_validate_subject accepted str | bytes and returned str, so publish() decoded a bytes subject and re-encoded it for the wire — the zero-copy bytes form only survived when validation was skipped, and encode_pub/encode_hpub (bytes) and encode_sub (str) disagreed on the canonical form.

Validate in the bytes domain: callers convert to bytes inline, the validators check UTF-8 (via decode) and CRLF/whitespace (a bytes regex) and return the original bytes object, and encode_sub takes bytes like the PUB encoders. subscribe() decodes once for the public Subscription.subject str. request() is widened to accept bytes subjects and Headers, matching publish().

Behavior is unchanged — subjects are still rejected for non-UTF-8, whitespace, and CRLF.

_validate_subject accepted str|bytes and returned str, so publish() decoded
a bytes subject and re-encoded it for the wire -- the zero-copy bytes form
only held when validation was skipped, and the PUB encoders (bytes)
disagreed with encode_sub (str) on the canonical form. Validate on bytes
instead: callers convert inline, the validators check UTF-8 (via decode)
and CRLF/whitespace (a bytes regex) and return the original bytes, and
encode_sub takes bytes. subscribe() decodes once for the public
Subscription.subject. Also widen request() to accept bytes subjects and
Headers, matching publish().
@synadia-claude-reviewer

synadia-claude-reviewer Bot commented Jun 22, 2026

Copy link
Copy Markdown

Claude finished @caspervonb's task in 2m 43s —— View job


PR Review

  • Gather context
  • Read PR diff and changed files
  • Review for correctness, security, and performance
  • Post findings

The design is correct and the motivation is sound: keeping subjects in the bytes domain avoids a redundant decode→re-encode round-trip on the hot path and makes encode_pub/encode_hpub/encode_sub consistent. The CRLF-injection protection is preserved (the bytes regex covers the same characters as the original string regex, and multi-byte UTF-8 sequences cannot contain ASCII control bytes).

Two minor issues flagged inline:

  1. request() discards _validate_subject return valuepublish() captures it (subject = _validate_subject(subject)); request() at line 1339 does not. Functionally identical today since the validator is documented to return the input unchanged, but a future normalization step would silently be skipped in request(). View inline comment →

  2. _subscribe() debug logs — the signature change to bytes means the log lines (logger.debug("->> SUB %s …", subject, …)) will now render as b'_INBOX.xxx.*' instead of _INBOX.xxx.*. View inline comment →

One observation on test coverage: The existing test_encode_sub test is updated correctly, but there are no new unit tests for _validate_subject/_validate_queue exercising the bytes-domain paths (e.g. non-UTF-8 input raising ValueError, whitespace rejection, strict-mode wildcard checks on bytes). Those cases are implicitly covered by integration tests that exercise subscribe()/publish(), but explicit unit tests would make the validator contract clearer going forward.

Comment thread nats-core/src/nats/client/__init__.py Outdated
subject_b = subject.encode() if isinstance(subject, str) else subject
if not self._skip_subject_validation:
_validate_subject(subject)
_validate_subject(subject_b)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The return value of _validate_subject is discarded here, while publish() captures it with subject = _validate_subject(subject). The contract is that _validate_subject returns the same bytes object unchanged, so this is functionally equivalent today — but if the function is ever changed to return a normalized form (e.g., NFC-normalized Unicode), request() would silently miss that normalization while publish() would not.

Suggested change
_validate_subject(subject_b)
subject_b = _validate_subject(subject_b)

return subscription

async def _subscribe(self, subject: str, sid: str, queue: str | None = None) -> None:
async def _subscribe(self, subject: bytes, sid: str, queue: bytes | None = None) -> None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Now that subject and queue are bytes, the logger.debug calls inside this method format them with %s, which produces b'_INBOX.abc.*'-style output rather than plain _INBOX.abc.*. The existing debug log lines would benefit from .decode() on those args — both callers (request() and the reconnect path) always pass well-formed UTF-8, so a bare .decode() is safe.

@caspervonb

Copy link
Copy Markdown
Collaborator Author

Review comments addressed, PTAL @claude

@synadia-claude-reviewer

synadia-claude-reviewer Bot commented Jul 13, 2026

Copy link
Copy Markdown

Claude finished @caspervonb's task in 46s —— View job


Re-review

  • Read current state of changed files
  • Verify fix for request() discarding _validate_subject return value
  • Verify fix for _subscribe() debug log bytes rendering
  • Post findings

Both previously flagged issues are addressed in commit ea545c2:

Issue 1 — request() capturing return value
nats-core/src/nats/client/__init__.py:1339 now reads subject_b = _validate_subject(subject_b), matching the publish() pattern.

Issue 2 — _subscribe() debug log rendering
Two separate paths are fixed correctly:

  • The public subscribe() path (lines 1251–1253) decodes to subject_str/queue_str before the logger.debug call.
  • The internal _subscribe() path (lines 1269–1271) calls .decode() inline on subject and queue in the log args.

Both are safe bare .decode() calls — the subjects are validated UTF-8 upstream before reaching these log sites.

No remaining issues. LGTM.

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