You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Subject validation accepts empty tokens and invalid wildcard usage #88
Observed on main at 4f16540fff91b4b021427a1c7696ae1b8c8c6025.
Bug
Sofascore\Nats\Connection::validateSubject() only rejects empty strings and whitespace/control characters. It does not reject malformed token structure or context-invalid wildcards.
The same validator is used by publish, request, subscribe, and queue subscribe paths, so publish/request subjects can pass client validation with wildcard tokens, and subscription subjects can pass with malformed wildcard placement.
Empty subject tokens such as foo..bar should be rejected.
Publish/request subjects should reject * and > wildcard tokens.
Subscribe subjects should only allow wildcards as whole tokens, and > should be the final token.
Impact
Invalid subjects can pass local validation and get serialized into protocol commands. Depending on server settings and timing, errors then happen later as server-side protocol errors instead of deterministic client-side validation failures. During reconnect, invalid publish commands can also enter the reconnect buffer before the server rejects them.
Likely fix
Make subject validation context-aware, e.g. validateSubject($subject, allowWildcards: false) for publish/request and allowWildcards: true for subscribe paths. The validator should split on . and reject empty tokens, partial wildcard tokens, and non-final > tokens.
Quick clarification on what "match reference behavior" means here, because the answer changed how I'd frame the fix:
What nats.go actually does
Checked nats.go directly. The client-side validateSubject() (nats.go:4390) only rejects empty strings and whitespace/control chars — same as sofascore/nats.php does today. There is no client-side tokenization or wildcard-position check in the Go client.
The strict, position-aware validation lives in nats-server, not in any client:
// nats-server/server/sublist.go (paraphrased)// IsValidSubject — used for subscribe paths (wildcards allowed in proper positions)// IsValidLiteralSubject — used for publish paths (no wildcards)funcisValidSubject(subjectstring, checkRunesbool) bool {
ifsubject=="" { returnfalse }
sfwc:=false// "seen full wildcard"for_, t:=rangestrings.Split(subject, ".") {
iflen(t) ==0||sfwc { returnfalse } // no empty tokens, nothing after `>`iflen(t) >1 {
ifstrings.ContainsAny(t, "\t\n\f\r ") { returnfalse }
continue// multi-char literal token is fine
}
ift[0] =='>' { sfwc=true } // `>` only valid as last token
}
returntrue
}
funcIsValidLiteralSubject(subjectstring) bool {
for_, t:=rangestrings.Split(subject, ".") {
iflen(t) ==0 { returnfalse }
iflen(t) ==1&& (t[0] =='*'||t[0] =='>') { returnfalse }
}
returntrue
}
Implication for this issue
The current sofascore/nats.php behavior actually matches nats.go client behavior — both are deliberately minimal and rely on the server to reject malformed subjects. So this issue is best framed as an enhancement ("be stricter than nats.go for better client-side ergonomics"), not a "bug vs reference parity."
Two reasonable resolutions:
Keep parity with nats.go client → close as "won't fix," document that the server does the strict check, and rely on the server's error reply.
Stricter, eager validation in the client (what the original issue body proposed) → port the nats-server algorithm into PHP. Same fail-fast-at-the-boundary spirit as fix: validate stream name in createStream/updateStream #82 for stream names — turns an opaque server-side error or buffered-then-rejected publish into a clear local exception.
Concrete PHP port if going the stricter route
publicstaticfunctionvalidateSubject(string$subject, bool$allowWildcards = false): void
{
if ($subject === '') {
thrownewBadSubjectException('Subject cannot be empty');
}
if (preg_match('/[\s\x00-\x1f\x7f]/', $subject)) {
thrownewBadSubjectException("Invalid subject: '{$subject}'");
}
$tokens = explode('.', $subject);
$seenFullWildcard = false;
foreach ($tokensas$token) {
if ($token === '' || $seenFullWildcard) {
// empty token (".foo", "foo..bar", "foo.") or anything after `>`thrownewBadSubjectException("Invalid subject: '{$subject}'");
}
if ($token === '*' || $token === '>') {
if (!$allowWildcards) {
thrownewBadSubjectException("Wildcards not allowed in publish subject: '{$subject}'");
}
if ($token === '>') {
$seenFullWildcard = true; // server rejects if not the last token
}
}
}
}
One subtle nats-server quirk worth an explicit decision
nats-server accepts foo*bar and foo>bar as literal tokens (the */> are only special when they are the entire token). A strict reading of the subject grammar (or a stricter client policy) rejects them. Whichever stance is taken, it's worth a comment in the validator explaining the choice — it surprises people on both sides.
Activity
matkomat commentedon May 12, 2026
Quick clarification on what "match reference behavior" means here, because the answer changed how I'd frame the fix:
What
nats.goactually doesChecked
nats.godirectly. The client-sidevalidateSubject()(nats.go:4390) only rejects empty strings and whitespace/control chars — same assofascore/nats.phpdoes today. There is no client-side tokenization or wildcard-position check in the Go client.The strict, position-aware validation lives in
nats-server, not in any client:Implication for this issue
The current
sofascore/nats.phpbehavior actually matchesnats.goclient behavior — both are deliberately minimal and rely on the server to reject malformed subjects. So this issue is best framed as an enhancement ("be stricter thannats.gofor better client-side ergonomics"), not a "bug vs reference parity."Two reasonable resolutions:
nats.goclient → close as "won't fix," document that the server does the strict check, and rely on the server's error reply.nats-serveralgorithm into PHP. Same fail-fast-at-the-boundary spirit as fix: validate stream name in createStream/updateStream #82 for stream names — turns an opaque server-side error or buffered-then-rejected publish into a clear local exception.Concrete PHP port if going the stricter route
Call sites:
publish/request/publishMessage/requestMessage→allowWildcards: false;subscribe/queueSubscribe/subscribeSync/queueSubscribeSync→allowWildcards: true.One subtle
nats-serverquirk worth an explicit decisionnats-serveracceptsfoo*barandfoo>baras literal tokens (the*/>are only special when they are the entire token). A strict reading of the subject grammar (or a stricter client policy) rejects them. Whichever stance is taken, it's worth a comment in the validator explaining the choice — it surprises people on both sides.Test cases that exercise the algorithm
allowWildcards: false)allowWildcards: true)foofoo.bar.baz.foofoo.foo..bar*foo.*foo.*.bar*mid-subject OK)>foo.>>.bar>not last)foo.>.bar>not last)foo barfoo*barnats-server)