Skip to content

fix(plugin-redis): list databases when the server refuses CONFIG or INFO, and the Redis defects found with it - #3038

Merged
datlechin merged 14 commits into
mainfrom
fix/redis-refused-config-fallback
Sep 21, 2026
Merged

datlechin merged 14 commits into
mainfrom
fix/redis-refused-config-fallback

Conversation

@datlechin

Copy link
Copy Markdown
Member

Fixes #3036.

The reported bug

RedisPluginDriver.databaseCount read CONFIG GET databases through RedisCommandChannel.run, which throws on any error reply. So the function's own "16 if it does not answer" fallback only ever caught a successful reply of the wrong shape. ElastiCache and Azure Cache for Redis remove CONFIG (ERR unknown command), Memorystore and ACL users without config|get deny it (NOPERM), and every one of them got the error in the window instead of a database list. INFO keyspace in the same two functions failed the same way for ACL users outside @dangerous.

The issue suggested try?, which would also have turned a dropped socket, -BUSY, -NOAUTH and a +QUEUED from an open MULTI into a healthy-looking list of 16. Instead:

  • RedisMetadataRead names the one rule: a read the app makes on its own is declined when the error class is ERR or NOPERM (first word, compared whole). Everything else still throws, and every decline is logged as a notice.
  • RedisDatabaseCount resolves the count once for both the sidebar and the database list: the server's own count when it gives one, otherwise the largest of 16, the highest database INFO keyspace names plus one, and the session's database plus one. Azure allows 64 databases and Memorystore 100, both with CONFIG refused, so a flat 16 would hide a populated db20.
  • A declined INFO keyspace leaves key counts unknown (NULL) instead of failing the list.
  • RedisServerInfo.mode also reads Valkey 8's server_mode. Without it the new fallback would list 16 empty databases for a Standalone connection pointed at a Valkey Sentinel port, where today it at least errors.
  • A sidebar click whose SELECT the server refuses (a single-database service answering ERR DB index is out of range) now shows the server's error on the tab the click retargeted, instead of an empty grid.

Everything else found while investigating, all fixed here at your request

Commit What was wrong
fix(plugin-redis): keep the app's own commands out of a user's MULTI block… The sidebar's CONFIG/INFO, the key browser's SCAN/TYPE, the key tree and the health monitor's PING all joined a MULTI block the user left open: an allowed one padded EXEC with extra replies, a refused one aborted the block with EXECABORT. A reconnect replayed into a new session that no longer held the block or WATCH. A pure RedisSessionFootprint now tracks the block and WATCH from the server's replies; every send states a scope (.session, .outsideBlock, .cleanSession), app reads are held back from a block, and a lost block or WATCH is reported to the user's next command instead of replayed.
fix(plugin-redis): read, count and export each Redis database from its own row… Row counts, statistics, the DDL preview and export all read the session's current database whatever row they were asked about, and a tab whose SELECT failed showed the session's keys under its own name after a refresh. KEYBROWSE takes DB <n>, a table's own browse and export queries carry it, and a read on another database visits it and returns.
fix(plugin-redis): write Redis grid edits to their row's database… Follows from the row above: the grid's writes now target the row's database too (SELECT nSELECT home, queued inside the save's MULTI). The session tracks where it is and where it belongs, so a read abandoned part way (a cancelled stream) cannot leave the next command on the database it was visiting. A SELECT typed inside a block now answers QUEUED and is recorded, so EXEC's replies stay paired with their commands.
fix(plugin-redis): show a refused TYPE or TTL as unknown… A key an ACL user cannot read showed type UNKNOWN, TTL -1 (which means "never expires") and an empty collection. Those cells are now NULL, and a Value edit is skipped when the type is unknown.
fix(plugin-redis): report a shard's refusal instead of counting it as zero in Redis Cluster The cluster aggregator counted a shard's error reply as 0, so a partial DEL reported success and a sidebar count came up short. A shard's error now wins for every policy but one_succeeded, and SCRIPT EXISTS folds element-wise.
fix(editor): stop offering Explain on Redis… Explain on Redis sent DEBUG OBJECT, which Redis 7+ refuses by default and ElastiCache and Azure remove, and the Query menu enabled Explain where the editor bar did not. One canExplain rule now drives both. Only Redis used the driver-built fallback that is removed.
fix(sidebar): show a failed Redis key tree load instead of No items Every key tree failure (a refused SCAN, an open block, a dropped socket) read as "No items". It now shows the error row, with Refresh on the Keys section.
fix(connection-form): let Redis Database Index reach every database… and …refuse to save a stepper value outside its range Database Index stopped at 15. Every stepper setting is now a text field paired with a stepper (HIG), and a value outside the range blocks Save with a message.
fix(ios): report a refused or queued Redis SCAN… The iOS driver turned a refused or queued SCAN into an empty key list.

Verification

  • Live, before and after, against redis-server 8.10.1, running the real plugin sources from a swiftc harness:
    • --rename-command CONFIG '' with --databases 32 and a key in db20: fails with CONFIG: ERR unknown command → 21 databases with counts. ACL +@all -config → 16 with counts. ACL +@all -@dangerous (refused CONFIG and INFO) → 16 with unknown counts.
    • MULTI; SET then sidebar reads and the health probe: EXEC returned 5 replies → 1. A user without +ping: EXECABORT → applied. Connection killed mid-block: a silent replay onto the new session → "the open MULTI block was lost and nothing in it ran".
    • Session on db0, row db3: counts, DDL, browse and export read db0 → db3. One-database server: showed db0's keys under db3 → the server's ERR DB index is out of range. Deleting a db3 row deleted db0's key of the same name → db3's.
    • ACL ~app:* user: UNKNOWN | -1 → NULL. Two-master cluster: partial DEL reported 1 → the shard's NOPERM.
  • Unit tests: 1,874 of 1,874 across the 170 Redis-related suites, plus 123 of 123 across the connection form and the new review-fix suites. New suites cover the refusal rule, the count resolver and its bounds, the session footprint's transitions and admission, database targeting and the stale-visit return, write addressing, cluster aggregation, the key tree load state, Explain availability and the range check.
  • iOS: 740 passed, 30 skipped, 0 failed on the TableProMobile suite.
  • Build: the app and all 41 plugins build (AllPlugins). Lint is clean on every line this branch wrote.
  • Review: Codex was unavailable (usage limit until 2026-09-22), so the diff was reviewed by code-review at high effort and by security-review. The review's findings are fixed in the branch: grid writes following the session and a cancelled stream able to leave the session on a visited database (the grid-write commit), an out-of-range stepper value being saved (the range commit), and, from a second pass over those fixes, the health monitor's ungated PING sending the session home in the middle of a visit (each command now moves to its own database right before it is sent). The security review found nothing at its exploitability bar.

Not covered

  • UI automation: the Redis flows need a live Redis, which the UI test runner does not have. The new connection form UI test (RedisConnectionModeUITests) is written but could not run locally: macOS held the runner at an "XCTest is trying to Enable UI Automation" password prompt.
  • Screenshots: pending, for the same prompt and because another session was driving the app. The screens that changed are the tab error for a refused database, the key tree error row with Refresh, the dimmed Query > Explain Query on Redis, and the Database Index text field with its stepper.
  • Accepted cost: Upstash, Redis Cloud and Azure Managed Redis refuse CONFIG and serve db0 only, so they now list 16 databases and every one but db0 shows the server's error when clicked. That replaces an empty sidebar. No read-only command reports the count, and probing with SELECT would queue into an open MULTI.
  • iOS: a user whose ACL denies SCAN now gets a connect error instead of an empty key list, because iOS has no "connected, but the key list failed" state.

@mintlify

mintlify Bot commented Sep 21, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated
TablePro 🟢 Ready View Preview Sep 21, 2026, 9:38 AM

💡 Tip: Enable Automations to automatically generate PRs for you.

@datlechin
datlechin merged commit 497b289 into main Sep 21, 2026
7 of 8 checks passed
@datlechin
datlechin deleted the fix/redis-refused-config-fallback branch September 21, 2026 10:26
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.

Redis: CONFIG GET databases error aborts the sidebar instead of falling back to 16

1 participant