Skip to content

fix(runtime): send realtime query params in the URL query string - #401

Merged
alexanderwanyoike merged 4 commits into
devfrom
fix/realtime-query-params
Oct 11, 2026
Merged

alexanderwanyoike merged 4 commits into
devfrom
fix/realtime-query-params

Conversation

@alexanderwanyoike

@alexanderwanyoike alexanderwanyoike commented Oct 11, 2026 •

Copy link
Copy Markdown
Owner

Background

Realtime (server mode) queries never delivered their params. RealtimeExecutor.Execute in runtime/internal/query/realtime.go POSTs to http://<pod-ip>:9476/<path> with the params only as a JSON body, but every SDK query server (Python, Node, Rust, .NET, Scala, Haskell, C++) reads params only from the URL query string into a map of strings. Handlers in realtime bots therefore always saw empty params. Scheduled (ephemeral) queries pass params through QUERY_PARAMS and were not affected; that path is unchanged. No prod bot is realtime, so nothing live was affected.

The fix is runtime only, so it ships with a runtime release and needs no SDK release. The JSON body stays as it is, so a later SDK change can read typed params from the body and fall back to the query string.

Behaviour of the params in the query string:

  • strings go as themselves; numbers, booleans, objects and arrays go as their JSON text (100, 0.25, true, {"from":"2026-01-01"}, ["BTC","ETH"]), all URL encoded with url.Values
  • null params stay out of the query string and are carried only by the JSON body, because SDK handlers fall back to a default for a missing key (req.get("limit", 10) in Python) and the string "null" would defeat that
  • handlers receive every value as a string in server mode for now; typed params are a separate, later contract change
  • no params means no query string is added (no stray ?), and a path that already carries a query string is sent untouched
  • with params, a path query string is merged: a param replaces a key of the same name, so each key appears once (SDKs disagree on which repeated key wins: Python takes the first, Node the last)
  • a malformed query string in the path fails the query with an error instead of silently dropping part of it

Tasks

  • Tests against a stand-in bot server that reads params from the URL query string like the SDKs do
  • Put realtime query params in the URL query string as well as the JSON body
  • Merge params with a query string already in the path, one value per key
  • Leave the URL alone when there are no params
  • Leave null params out of the query string, keeping them in the JSON body

Tested

New tests in runtime/internal/query/realtime_test.go, grouped by behaviour:

  • TestRealtimeExecutor_ParamsReachBotAsURLQueryString: string, whole and fractional numbers, booleans, nested object, array, and reserved plus non-ASCII characters in both key and value
  • TestRealtimeExecutor_NoParamsLeavesQueryStringOut: nil and empty params send exactly /portfolio, with no ?
  • TestRealtimeExecutor_NullParamsStayOutOfQueryString: a null param next to other params is absent from the query string while the JSON body still has it; a query with only null params sends exactly /portfolio
  • TestRealtimeExecutor_QueryStringInPathCombinesWithParams: a path query string passes through untouched without params; params join it and win on the same key, each key sent once; a malformed path query string is rejected without querying the bot

The existing TestRealtimeExecutor_ExecuteWithParams still passes, so the JSON body is unchanged.

Commit structure:

  1. test(runtime): expect realtime query params in the URL query string adds the tests only. At this commit the params cases, the merge case and the malformed case fail (the bot server sees an empty map); the no-params and path-only cases pass and act as guards.
  2. fix(runtime): send realtime query params in the URL query string makes them pass. At this point a null param was sent as the text null.
  3. test(runtime): expect null realtime params only in the JSON body replaces the "null arrives as JSON text" case with the null tests above. Both fail at this commit (cursor=null reaches the bot); their body assertions already pass.
  4. fix(runtime): leave null realtime params out of the query string makes them pass.

Commands run in runtime/ (after commit 2, and again after commit 4):

  • go build ./...: ok
  • go vet ./...: ok
  • go test -short -count=1 ./...: all packages ok
  • go test -short -count=1 -race -run TestRealtimeExecutor ./internal/query/: ok
  • gofmt -l on the touched files: clean

Tech Debt

  • Server-mode values are strings until the typed params contract change, where SDKs read the JSON body first.
  • The SDK server-mode 404 still omits the available list (SDK side, tracked with the bot contract work).

Every SDK query server in server mode reads params only from the URL
query string into a map of strings, but the realtime executor sends them
only as a JSON body, so handlers always see empty params.

These tests run the executor against a stand-in bot server that reads
params the way the SDKs do. They cover strings, numbers, booleans, null,
objects and arrays (as JSON text), characters that need encoding, a path
that already carries a query string, and a malformed one. The no-params
and path-only cases pass today and guard against a stray "?" or a
rewritten path query; the rest fail until the executor adds the params
to the URL.
The realtime executor POSTed params only as a JSON body, while every SDK
query server in server mode reads them only from the URL query string,
so realtime handlers always saw empty params. Scheduled queries were
unaffected because they pass params through QUERY_PARAMS.

The executor now also puts the params in the query string and keeps the
JSON body unchanged, so the fix ships with the runtime alone and a later
SDK change can read the typed body. Strings go as themselves and every
other value as its JSON text, so handlers receive strings in server mode
for now. A param replaces a key of the same name already in the path,
because SDKs disagree on which of a repeated key wins, and a malformed
query string in the path fails the query instead of silently losing part
of it.
@alexanderwanyoike

Copy link
Copy Markdown
Owner Author

mini-code-review: merge after fixing the null param

Bugs

  • fix: runtime/internal/query/realtime.go paramText: a null param is sent in the URL as the text null. SDK handlers read URL params as strings and use defaults for missing keys, so a Python handler doing req.get("limit", 10) gets the truthy string "null" instead of 10, and int(...) on it raises. Leave null-valued params out of the URL query string; the JSON body keeps them as real nulls.

Design/DRY

None. addParamsToQuery keeps the JSON body, uses url.Values encoding, adds no stray ? without params, and replaces a same-named key from the path. The comments give the why (SDKs only read the query string; SDKs disagree on repeated keys).

Nits

  • note: When params are present, a malformed query string already in the path now fails the query instead of being passed through. That is reasonable, since the path comes from the caller, and a test covers it.

Tests: an httptest server reads params the way the SDKs do and covers strings, numbers, booleans, objects, arrays, encoding, no params and path query strings. Two atomic commits: failing tests, then the fix.

SDK handlers read URL params as strings and fall back to a default for a
missing key, so sending a null param as the text "null" hands them a
truthy string where they expected their default (Python's
req.get("limit", 10) gets "null" instead of 10).

The null case now expects the param to be absent from the query string
while the JSON body still carries it, and a query with only null params
must add no query string at all. Both fail until the executor skips
nulls when building the query string.
SDK handlers read URL params as strings and fall back to a default for a
missing key, so a null param sent as the text "null" reached them as a
truthy string instead of letting the default apply. The executor now
skips null params when building the query string; the JSON body still
carries them, and a query with only null params adds no query string.
@github-actions

Copy link
Copy Markdown

Security scan summary

Policy: visibility-only target; findings are reported here and would block release if this targeted main or a release tag.

Scanner Unique release-blocking findings
Trivy CRITICAL 0
Trivy HIGH 0
Yarn audit CRITICAL 0
Yarn audit HIGH 0
Govulncheck reachable distinct vulnerabilities 0
.NET audit CRITICAL 0
.NET audit HIGH 0
pip-audit known vulnerabilities 0
cargo-audit CRITICAL 0
cargo-audit HIGH 0
OSV-Scanner CRITICAL 0
OSV-Scanner HIGH 0
Total release blockers 0

Accepted residual findings listed in .github/security/accepted-vulnerabilities.txt are excluded from release-blocker totals.

Scan coverage

Scanner Coverage Diagnostics
Govulncheck: runtime go packages: 19
go modules: 163
exit code: 0

Full JSON reports and compact per-scan summaries are attached as workflow artifacts.

Result: no release-blocking findings detected.

@alexanderwanyoike

Copy link
Copy Markdown
Owner Author

Follow-up: the null param fix landed in 1148de9 (failing test) and 50707be (fix). A null param that names a key already in the path leaves the path's value in place, which matches 'not provided'. Verdict: merge once CI is green.

@alexanderwanyoike
alexanderwanyoike merged commit e0d6463 into dev Oct 11, 2026
21 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant