Skip to content

[codex] Improve KiwiSDR connection error messages#3699

Merged
ten9876 merged 1 commit into
aethersdr:mainfrom
rfoust:codex/kiwi-error-messages
Jun 21, 2026
Merged

[codex] Improve KiwiSDR connection error messages#3699
ten9876 merged 1 commit into
aethersdr:mainfrom
rfoust:codex/kiwi-error-messages

Conversation

@rfoust

@rfoust rfoust commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator

Summary

Improve KiwiSDR connection failure reporting by translating server and proxy denial states into specific user-facing errors. The client now parses per-IP time-limit notices, inactivity disconnects, update/backup downtime, wideband-only and exclusive-use denials, password timeouts, operator disconnects, and numeric badp access/auth codes. Status preflight failures preserve HTTP denial details instead of collapsing them to a generic unreachable status page message.

Constitution principle honored

Principle VII — boundary input validation. KiwiSDR server and proxy responses are decoded defensively and mapped to bounded operator-facing errors.

@rfoust rfoust marked this pull request as ready for review June 20, 2026 20:24
@rfoust rfoust requested review from a team as code owners June 20, 2026 20:24
Copilot AI review requested due to automatic review settings June 20, 2026 20:24

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Improves KiwiSDR connection failure reporting by decoding additional server/proxy denial states into more specific user-facing error messages, and by preserving more detail from the status-page preflight checks.

Changes:

  • Added IpLimitNotice + parseIpLimitNotice() (with tests) to decode ip_limit denial payloads (including percent-encoded comma cases).
  • Enhanced status preflight failure reporting to retain the first HTTP attempt’s status/error when falling back from HTTP → HTTPS.
  • Expanded MSG key handling in KiwiSdrClient to map more denial conditions (down, wb_only, exclusive_use, ip_limit, inactivity_timeout, password_timeout, kiwi_kick, richer badp mapping) into targeted errors.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
tests/kiwi_sdr_protocol_test.cpp Adds unit-style coverage for ip_limit notice parsing (encoded + minute-only + invalid cases).
src/core/KiwiSdrProtocol.h Introduces IpLimitNotice and declares parseIpLimitNotice().
src/core/KiwiSdrProtocol.cpp Implements percent-decoding + parsing of ip_limit notices.
src/core/KiwiSdrClient.h Stores first-attempt HTTP status/error so the final failure message can be more specific.
src/core/KiwiSdrClient.cpp Adds detailed status-preflight failure messages and expands MSG denial parsing / error mapping.

Comment on lines 1879 to +1883
if (key == QStringLiteral("down")) {
bool downOk = false;
const int downValue = valueText.toInt(&downOk);
if (!downOk || downValue != 0) {
setState(State::Error, tr("KiwiSDR endpoint is down."));
setState(State::Error,

@aethersdr-agent aethersdr-agent Bot 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 this, @rfoust — this is a well-scoped, high-quality improvement. The denial states are mapped to clear, bounded operator-facing messages, every error path calls cleanupSockets() before returning, and the new parseIpLimitNotice parser is properly defensive (rejects non-numeric / non-positive minutes) with good unit coverage for the encoded, minute-only, and invalid cases. Preserving the HTTP denial status through the preflight retry instead of collapsing it to "unreachable" is a real UX win. Files are all within scope.

A couple of notes:

1. Copilot's down / reason_disabled finding is valid (minor). The reason_disabled handler now looks up down via messageValue(...), but the down handler always passes QString() as the reason:

setState(State::Error, downMessage(downOk ? downValue : 0, QString()));

Because the loop returns on the first matching key, a message like down=3 reason_disabled=<operator text> loses the operator's reason if down is encountered first. It's low-severity — for down=1/down=2 the reason is intentionally ignored (fixed update/backup text), so the gap only affects other non-zero down values that also carry a reason_disabled. Mirroring the lookup in the down handler (messageValue("reason_disabled"), percent-decoded) would make the two paths symmetric and order-independent. Worth a one-line fix if you're touching it again, but not a blocker.

2. Minor i18n nit (optional). statusPreflightFailureMessage and the downMessage lambda use QObject::tr(...), which files these strings under the QObject translation context rather than KiwiSdrClient. Harmless functionally; just slightly inconsistent with the surrounding tr(...) calls. Not worth changing on its own.

Nice work — the defensive decoding and test coverage are exactly right for a boundary-input change.


🤖 aethersdr-agent · cost: $1.9058 · model: claude-opus-4-8

@ten9876 ten9876 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks @rfoust — the defensive decoding, the cleanupSockets() discipline on every error path, and the test coverage are all the right instincts, and a couple of pieces here are clear wins I'd like to land regardless of the rest:

  • Status-preflight HTTP-status preservation (statusPreflightFailureMessage) — keeping a 401/403 from the first HTTP attempt instead of collapsing to "unreachable" is a real, version-safe UX improvement.
  • parseIpLimitNotice mechanics — properly validated (rejects non-numeric / non-positive minutes), good unit coverage.

Before this lands in full, though, there's one correctness problem and one provenance question I need to resolve, because this PR translates protocol codes into confident, specific user-facing text — and a wrong-but-authoritative message is worse than the generic one it replaces.

1. badp=5 contradicts our own clean-room observations

This PR maps badp=5 → "does not allow multiple connections from the same IP address." But our clean-room record in docs/kiwisdr-cleanroom-design.md shows badp=5 returned for ordinary public-auth rejection, on single fresh probes to three different servers:

L290: "zl2j.proxy.kiwisdr.com:8073 … returned badp=5 and closed before sending audio for both p=# and empty public-password auth"
L297: "k9czi-1.proxy.kiwisdr.com:8073 … returned badp=5 for public auth before closing"
L310: "162.199.177.108:8073 … Sending public auth first returned badp=0 and normal setup"

That's the most common real failure (public access denied). Today it hits the generic "may require a password or site-specific login" hint; under this PR the same servers would tell users "does not allow multiple connections from the same IP address" — wrong, and it drops the password hint that actually helps. Three different servers returning that on a first connection can't be a multi-connection condition.

2. Provenance of the rest (clean-room / Principle IV)

Our KiwiSDR integration is deliberately clean-room, and the design doc took the conservative, version-agnostic stance (badp/down/reason_disabled → generic local errors). This PR adds specific semantics we have no observations forbadp 1, 2, 3, 4, 6, 7, and six new MSG keys never seen in our captures (wb_only, exclusive_use, inactivity_timeout, password_timeout, kiwi_kick, and the ip_limit wire format itself) — with no provenance cited and no THIRD_PARTY_LICENSES change. The badp=5 miss matters beyond itself: the one code we can check against reality failed, so I can't take the others on faith.

What I need from you, Robbie:

  1. Where did the badp 1–7 meanings and the new MSG keys come from? A KiwiSDR source file (which version/commit)? A published spec? Live observation? This decides clean-room handling — if it's source-derived we need attribution recorded, not landed silently.
  2. Can you reconcile badp=5? Is the numbering version-dependent (i.e., did you read a newer KiwiSDR where 5 means something different than the builds our probes hit)? A citable source + a note on version skew would settle it.
  3. For ip_limit / the new keys — observed on a live server, or inferred? A frame capture we can point to would let us treat the wire format as confirmed rather than assumed.

Two ways forward

  • (A) Narrow to the version-safe subset now: keep statusPreflightFailureMessage, the ip_limit parser, and the generic improved badp text; drop the per-code badp 1–7 switch and the unobserved MSG keys. Lands the real value with zero accuracy/provenance risk, and the specific codes can return once verified.
  • (B) Verify in full: cite an authoritative source for the codes + keys, reconcile badp=5, and add attribution if source-derived — then it all lands.

Minor (non-blocking, already noted by the bot)

  • down/reason_disabled ordering: the down branch passes an empty reason, so down=N reason_disabled=… loses the operator text if down parses first. Mirror messageValue("reason_disabled") (percent-decoded) in the down branch to make the paths order-independent.
  • QObject::tr vs member tr() context — cosmetic.

Happy to take path (A) and merge the safe parts immediately if you'd rather verify the codes separately — your call on which is less work given your in-flight split-VFO/KiwiSDR sweep.

@rfoust rfoust force-pushed the codex/kiwi-error-messages branch from 5650698 to 2e24cba Compare June 21, 2026 05:42
@rfoust rfoust self-assigned this Jun 21, 2026
@rfoust

rfoust commented Jun 21, 2026

Copy link
Copy Markdown
Collaborator Author

Pushed an amended update that takes the narrow path (A) from the review.

  • badp: removed the per-code badp switch, including the incorrect badp=5 multiple-connection message. Nonzero badp now stays generic, preserves the numeric code, and keeps the password / site-login / access-policy hint.
  • Provenance / unobserved MSG keys: dropped the source-derived, unobserved mappings for wb_only, exclusive_use, inactivity_timeout, password_timeout, and kiwi_kick from this PR. I kept ip_limit handling because that is the observed wire failure this PR is addressing, with the defensive parser still validating the minutes/address payload before showing the more specific time-limit text.
  • down / reason_disabled: made the handling order-independent. If down arrives before reason_disabled in the same MSG, the down branch now looks up and percent-decodes the reason before choosing the disabled/down message.
  • i18n nit: moved the helper/lambda strings out of the QObject translation context and into the KiwiSdrClient context via QCoreApplication::translate("KiwiSdrClient", ...).

I did not add source-provenance docs in this update because the amended PR no longer depends on those source-derived MSG-key mappings.

@ten9876 ten9876 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Re-reviewed the amended PR — path (A) executed cleanly. All concerns resolved; clearing my changes-requested.

Verified against the new diff:

  • badp per-code switch removed, including the incorrect badp=5 mapping. Nonzero badp now stays generic, preserves the numeric code (badp=%1) and keeps the password / site-login / access-policy hint via identityDiagnosticText(). ✓
  • Unobserved, source-derived MSG keys dropped — zero references to wb_only / exclusive_use / inactivity_timeout / password_timeout / kiwi_kick. Provenance concern moot. ✓
  • ip_limit kept (the observed wire failure) with the defensive parser that validates minutes/address and falls back to generic text — fails safe. ✓
  • down / reason_disabled order-independent — both branches now look up and percent-decode the other key, so message selection no longer depends on field order. ✓
  • i18n context fixedtrKiwiSdrClient() routes the free-function strings through QCoreApplication::translate("KiwiSdrClient", …); no more QObject::tr. ✓

The version-safe wins (status-preflight HTTP-status preservation, defensive ip_limit decode, improved generic badp/down text) all land with no accuracy or provenance risk. CI green on all six checks. Thanks for the quick, precise turnaround, @rfoust.

@ten9876 ten9876 merged commit caac003 into aethersdr:main Jun 21, 2026
6 checks passed
ten9876 pushed a commit that referenced this pull request Jun 22, 2026
… + MSG keys) (#3716)

Follow-up to #3699 (path B): reintroduces source-attributed KiwiSDR terminal denial handling for the badp numeric codes and MSG keys (wb_only, exclusive_use, inactivity_timeout, password_timeout, kiwi_kick), now with provenance. Protocol facts (codes/keys) derived from the LGPL-marked KiwiSDR source (jks-prv/Beagle_SDR_GPS @ efb38e2b) and documented in THIRD_PARTY_LICENSES + docs/kiwisdr-cleanroom-design.md; no code copied/translated/vendored/linked, all user-facing strings original. badp=5 worded conservatively (duplicate-IP and public-access-rejection). New parseMsgTokens() handles flag-only MSG tokens (with unit coverage); unknown keys still ignored.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
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.

3 participants