fix(swap): do not treat route/solve 400 as market-data outage (#326) #800

Merged
PlasticDigits merged 1 commit from cursor/gitlab-issue-workflow-b0ae into main 2026-06-05 12:32:43 +00:00
PlasticDigits commented 2026-06-05 12:28:36 +00:00 (Migrated from gitlab.com)

Summary

Fixes https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/work_items/326

When the indexer is healthy but GET /api/v1/route/solve returns 400 (e.g. router simulation failed on a fresh deploy), the swap page incorrectly showed "Market data service unavailable." — the same banner as a genuine transport outage.

Root cause: isIndexerUnavailableError treated any Indexer API error: message as outage, excluding only 404.

Fix: Add isIndexerClientError (all indexer 4xx) and exclude it from outage detection. Outage now means network/AbortError/Failed to fetch or 5xx only. The swap page already falls back to client-side BFS + LCD sim when route/solve fails; with this change the fallback no longer sets indexerTransportFailed for 400s and the outage banner stays hidden.

Acceptance checklist

Criterion Verification Result
400 from /route/solve is not classified as transport outage npx vitest run src/utils/__tests__/indexerErrors.test.ts PASS
detectMarketDataOutage ignores 400 npx vitest run src/utils/__tests__/marketDataOutage.test.ts PASS
detectSwapIndexerOutage ignores 400 npx vitest run src/utils/swapIndexerOutage.test.ts PASS
Swap page: 400 + LCD fallback → no outage banner npx vitest run src/pages/SwapPage.test.tsx (GitLab #326 case) PASS
502 still shows outage banner SwapPage.test.tsx #241 cases PASS
Full frontend unit suite make test-frontend (843 tests) PASS
Lint make lint-frontend PASS
Manual repro (fresh deploy, EMBER→CORAL, indexer up) Requires full LocalTerra stack — not run in agent VM this session SKIP

Verification for third parties

export PATH="$HOME/.nvm/versions/node/$(cat .nvmrc)/bin:$PATH"
make test-frontend
make lint-frontend

Optional manual QA (fresh deploy):

  1. make setup-cloud-localterra (or existing QA stack with indexer running)
  2. Open / → select a pair that triggers route/solve 400 before pool sync settles
  3. Confirm no swap-market-data-outage-banner; swap quote may still appear via LCD fallback
  4. Stop indexer → banner should appear (genuine outage)

Docs

  • skills/AGENTS_FRONTEND_MARKET_DATA_OUTAGE.md — 4xx vs outage rule
  • docs/frontend.md — swap outage invariants updated for #326

Issue left open until merge.


Note

Low Risk
Low-risk error-classification change with broad unit/page tests; behavior only shifts for indexer 4xx (especially 400), which now follow LCD/fallback paths instead of outage UX.

Overview
Fixes false “market data service unavailable” banners when the indexer is up but route/solve returns 400 (simulation failed) while LCD pool fallback still produces a valid quote (#326).

indexerErrors.ts adds isIndexerClientError for indexer Indexer API error: 4xx responses and narrows isIndexerUnavailableError to transport failures and 5xx only (404/400 and other 4xx are excluded). That flows through detectMarketDataOutage / detectSwapIndexerOutage and swap’s indexerTransportFailed gate, which already depend on isIndexerUnavailableError.

Docs and agent playbooks reframe 404 vs outage as 4xx vs outage (404 → not-found/retry; 400 route-solve → LCD fallback, no banner). Vitest covers 400/404/502 classification, outage detection, swap outage helper, and a new SwapPage case: 400 on route/solve + successful LCD sim → no swap-market-data-outage-banner, Swap enabled.

Reviewed by Cursor Bugbot for commit df9b9e54a5. Bugbot is set up for automated code reviews on this repo. Configure here.

## Summary Fixes https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/work_items/326 When the indexer is healthy but `GET /api/v1/route/solve` returns **400** (e.g. router simulation failed on a fresh deploy), the swap page incorrectly showed **"Market data service unavailable."** — the same banner as a genuine transport outage. **Root cause:** `isIndexerUnavailableError` treated any `Indexer API error:` message as outage, excluding only 404. **Fix:** Add `isIndexerClientError` (all indexer **4xx**) and exclude it from outage detection. Outage now means network/`AbortError`/`Failed to fetch` or **5xx** only. The swap page already falls back to client-side BFS + LCD sim when route/solve fails; with this change the fallback no longer sets `indexerTransportFailed` for 400s and the outage banner stays hidden. ## Acceptance checklist | Criterion | Verification | Result | |-----------|--------------|--------| | 400 from `/route/solve` is not classified as transport outage | `npx vitest run src/utils/__tests__/indexerErrors.test.ts` | PASS | | `detectMarketDataOutage` ignores 400 | `npx vitest run src/utils/__tests__/marketDataOutage.test.ts` | PASS | | `detectSwapIndexerOutage` ignores 400 | `npx vitest run src/utils/swapIndexerOutage.test.ts` | PASS | | Swap page: 400 + LCD fallback → no outage banner | `npx vitest run src/pages/SwapPage.test.tsx` (GitLab #326 case) | PASS | | 502 still shows outage banner | `SwapPage.test.tsx` #241 cases | PASS | | Full frontend unit suite | `make test-frontend` (843 tests) | PASS | | Lint | `make lint-frontend` | PASS | | Manual repro (fresh deploy, EMBER→CORAL, indexer up) | Requires full LocalTerra stack — not run in agent VM this session | SKIP | ## Verification for third parties ```bash export PATH="$HOME/.nvm/versions/node/$(cat .nvmrc)/bin:$PATH" make test-frontend make lint-frontend ``` Optional manual QA (fresh deploy): 1. `make setup-cloud-localterra` (or existing QA stack with indexer running) 2. Open `/` → select a pair that triggers route/solve 400 before pool sync settles 3. Confirm **no** `swap-market-data-outage-banner`; swap quote may still appear via LCD fallback 4. Stop indexer → banner **should** appear (genuine outage) ## Docs - `skills/AGENTS_FRONTEND_MARKET_DATA_OUTAGE.md` — 4xx vs outage rule - `docs/frontend.md` — swap outage invariants updated for #326 Issue left open until merge. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Low-risk error-classification change with broad unit/page tests; behavior only shifts for indexer 4xx (especially 400), which now follow LCD/fallback paths instead of outage UX. > > **Overview** > Fixes **false “market data service unavailable” banners** when the indexer is up but **`route/solve` returns 400** (simulation failed) while **LCD pool fallback** still produces a valid quote ([#326](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/work_items/326)). > > **`indexerErrors.ts`** adds **`isIndexerClientError`** for indexer **`Indexer API error: 4xx`** responses and narrows **`isIndexerUnavailableError`** to **transport failures and 5xx** only (404/400 and other 4xx are excluded). That flows through **`detectMarketDataOutage`** / **`detectSwapIndexerOutage`** and swap’s **`indexerTransportFailed`** gate, which already depend on **`isIndexerUnavailableError`**. > > Docs and agent playbooks reframe **404 vs outage** as **4xx vs outage** (404 → not-found/retry; 400 route-solve → LCD fallback, no banner). **Vitest** covers 400/404/502 classification, outage detection, swap outage helper, and a new **`SwapPage`** case: 400 on route/solve + successful LCD sim → **no** `swap-market-data-outage-banner`, **Swap** enabled. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit df9b9e54a5fab95d97cdcdff76413cd0fea00f77. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
ghost1 commented 2026-06-05 12:28:43 +00:00 (Migrated from gitlab.com)

changed the description

changed the description
PlasticDigits commented 2026-06-05 12:28:45 +00:00 (Migrated from gitlab.com)

changed the description

changed the description
PlasticDigits commented 2026-06-05 12:28:45 +00:00 (Migrated from gitlab.com)

mentioned in issue #326

mentioned in issue #326
ghost1 commented 2026-06-05 12:28:53 +00:00 (Migrated from gitlab.com)

changed the description

changed the description
PlasticDigits commented 2026-06-05 12:30:01 +00:00 (Migrated from gitlab.com)

Security review

Commit reviewed: df9b9e54a5fab95d97cdcdff76413cd0fea00f77

Scope: Frontend outage classification for indexer HTTP errors — isIndexerClientError (4xx), narrowed isIndexerUnavailableError (transport + 5xx), docs/skills, Vitest (indexerErrors, marketDataOutage, swapIndexerOutage, SwapPage #326).

Method: MR diff + sink tracing (fetchJson → error strings → detectMarketDataOutage / detectSwapIndexerOutage / SwapPage indexerTransportFailed / CTA gates). Checked injection, XSS via error copy, stale-quote / swap-without-sim paths, and authz/outage bypass via status spoofing.

Outcome: FINDINGS: 0 medium+

Inline threads: None (no qualifying findings).

Notes (informational, below reporting threshold)

  • 4xx vs outage is a client-side UX boundary only; swap execution still requires a successful simQuery with LCD/simulate_swap data (simData cleared on simQuery.isError; CTA stays Quote unavailable when sim fails).
  • 400 + LCD fallback (the #326 fix) does not enable swap without a fresh on-chain sim — it only stops the global outage banner when the indexer is healthy but route/solve rejects semantically.
  • Error classification keys off resp.status embedded in Indexer API error: ${status} … from client.ts; user-facing paths use humanizeUserFacingErrorFromUnknown / sanitizeOpaqueErrorMessage.
  • Broader 429 handling now follows the same 4xx (non-outage) rule — UX/rate-limit, not a funds-at-risk boundary in this diff.

Automated security review (Cursor Cloud Agent).

## Security review **Commit reviewed:** `df9b9e54a5fab95d97cdcdff76413cd0fea00f77` **Scope:** Frontend outage classification for indexer HTTP errors — `isIndexerClientError` (4xx), narrowed `isIndexerUnavailableError` (transport + 5xx), docs/skills, Vitest (`indexerErrors`, `marketDataOutage`, `swapIndexerOutage`, `SwapPage` #326). **Method:** MR diff + sink tracing (`fetchJson` → error strings → `detectMarketDataOutage` / `detectSwapIndexerOutage` / `SwapPage` `indexerTransportFailed` / CTA gates). Checked injection, XSS via error copy, stale-quote / swap-without-sim paths, and authz/outage bypass via status spoofing. **Outcome:** `FINDINGS: 0` medium+ **Inline threads:** None (no qualifying findings). ### Notes (informational, below reporting threshold) - **4xx vs outage** is a client-side UX boundary only; swap execution still requires a successful `simQuery` with LCD/`simulate_swap` data (`simData` cleared on `simQuery.isError`; CTA stays **Quote unavailable** when sim fails). - **400 + LCD fallback** (the #326 fix) does not enable swap without a fresh on-chain sim — it only stops the global outage banner when the indexer is healthy but route/solve rejects semantically. - Error classification keys off `resp.status` embedded in `Indexer API error: ${status} …` from [`client.ts`](frontend-dapp/src/services/indexer/client.ts); user-facing paths use `humanizeUserFacingErrorFromUnknown` / `sanitizeOpaqueErrorMessage`. - Broader **429** handling now follows the same 4xx (non-outage) rule — UX/rate-limit, not a funds-at-risk boundary in this diff. --- *Automated security review (Cursor Cloud Agent).*
PlasticDigits commented 2026-06-05 12:32:43 +00:00 (Migrated from gitlab.com)

mentioned in commit e269c0ec85

mentioned in commit e269c0ec857655ac2993f8b3f88a61fcaddfb4b5
PlasticDigits (Migrated from gitlab.com) merged commit e269c0ec85 into main 2026-06-05 12:32:43 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
code/cl8y-dex-terraclassic!800
No description provided.