UI: Swap — "Market data service unavailable" banner fires on 400 from /route/solve; healthy-indexer route-simulation failures misclassified as transport outage #326

Closed
opened 2026-06-05 11:30:00 +00:00 by totdking · 7 comments
totdking commented 2026-06-05 11:30:00 +00:00 (Migrated from gitlab.com)
No description provided.
totdking commented 2026-06-05 11:34:51 +00:00 (Migrated from gitlab.com)

Summary

When the indexer is running and reachable but /api/v1/route/solve returns a 400 Bad Request (e.g. "router simulation failed for the given route and hybrid parameters"), the swap page displays the "Market data service unavailable." banner. This is the same banner shown when the indexer is genuinely unreachable. The banner disappears when the indexer is stopped entirely — the inverse of the message it shows.

The root cause is that isIndexerUnavailableError classifies any "Indexer API error:" message (all non-2xx, non-404 status codes) as a transport outage. A 400 from a healthy indexer is a semantic / route-simulation error — the service is available, the specific route failed.


Root cause (code verified)

client.ts:43–44 — all non-OK responses become "Indexer API error: <status>":

if (!resp.ok) {
  throw new Error(`Indexer API error: ${resp.status} ${resp.statusText}`)
}

A 400 from /route/solve throws "Indexer API error: 400 Bad Request".

indexerErrors.ts:7–13 — isIndexerUnavailableError catches this:

export function isIndexerUnavailableError(err: unknown): boolean {
  if (isIndexerPairNotFoundError(err)) return false   // only 404 is excluded
  const m = err.message
  if (m.includes('Indexer API error:')) return true   // ← 400 matches here
  ...
}

The function excludes 404 (pair not found) but does not exclude other 4xx codes. A 400 is treated as an unreachable service.

marketDataOutage.ts:9–11 → swapIndexerOutage.ts:13 → SwapPage.tsx:595:

// marketDataOutage.ts




return queries.some((q) => q.isError && isIndexerUnavailableError(q.error))

// swapIndexerOutage.ts




return detectMarketDataOutage(simQuery) || !!simData?.indexerTransportFailed

// SwapPage.tsx:783




{indexerOutage && (<OutageBanner ... "Market data service unavailable." />)}

Because simQuery.isError = true and isIndexerUnavailableError returns true for the 400, indexerOutage becomes true and the banner fires.

The 404 special-case precedent:

isIndexerPairNotFoundError already correctly excludes 404 (pair not in catalog) from the outage classification. The same treatment is needed for 400 (route simulation failed, semantic error from a healthy service).


Steps to reproduce

  1. Run the full QA stack with the indexer running: make start-qa or make deploy-local && make indexer-dev
  2. Open the swap page at /
  3. Select a token pair (observed: EMBER → CORAL) on a fresh deploy where pool liquidity has not fully settled or the indexer has not completed its initial sync
  4. Observe: "Market data service unavailable." banner appears
  5. Stop the indexer
  6. Observe: banner disappears — demonstrating the indexer's presence (not absence) triggers it
  7. Restart the indexer — banner returns until the /route/solve 400 errors stop

Expected behavior

A 400 response from /api/v1/route/solve should be classified as a route-simulation failure, not a service outage. The swap page should either:

  • Show a route-specific message (e.g. "Route unavailable for this pair — trying direct path") without the outage banner, or
  • Fall back to the client-side BFS route silently (already implemented) and suppress the outage banner entirely when the indexer is reachable

The "Market data service unavailable" banner should only appear when the indexer is genuinely unreachable (network error, AbortError, Failed to fetch, or 5xx).


Actual behavior

Any 400 Bad Request from /route/solve — a healthy indexer responding to an unresolvable route — triggers the outage banner. The banner implies service downtime when the service is up and responding. The error is transient (resolves after pool sync completes on a fresh deploy) but during that window users see a misleading outage state.


Screenshot

image.png{width=900 height=565}


Environment

  • Chain: localterra
  • LCD: http://localhost:1317
  • Wallet: Keplr (Terra Classic)
  • Browser: Chromium (DevTools open)
  • Page: / (Swap)
  • Stack: fresh deploy after make reset-qa / make deploy-local
  • Indexer response observed: "router simulation failed for the given route and hybrid parameters"
  • Network throttle applied: No

Severity: P3(Nit) no funds at risk and swaps may still succeed via LCD fallback, but the banner falsely declares the market data service down during a window when it is healthy. Particularly misleading on fresh deploys where this is the first state a tester or user encounters.

cc: @PlasticDigits

### Summary When the indexer is running and reachable but `/api/v1/route/solve` returns a `400 Bad Request` (e.g. `"router simulation failed for the given route and hybrid parameters"`), the swap page displays the **"Market data service unavailable."** banner. This is the same banner shown when the indexer is genuinely unreachable. The banner disappears when the indexer is stopped entirely — the inverse of the message it shows. The root cause is that `isIndexerUnavailableError` classifies any `"Indexer API error:"` message (all non-2xx, non-404 status codes) as a transport outage. A 400 from a healthy indexer is a semantic / route-simulation error — the service is available, the specific route failed. --- ### Root cause (code verified) **`client.ts:43–44` — all non-OK responses become `"Indexer API error: <status>"`:** ```ts if (!resp.ok) { throw new Error(`Indexer API error: ${resp.status} ${resp.statusText}`) } ``` A 400 from `/route/solve` throws `"Indexer API error: 400 Bad Request"`. **`indexerErrors.ts:7–13` — `isIndexerUnavailableError` catches this:** ```ts export function isIndexerUnavailableError(err: unknown): boolean { if (isIndexerPairNotFoundError(err)) return false // only 404 is excluded const m = err.message if (m.includes('Indexer API error:')) return true // ← 400 matches here ... } ``` The function excludes 404 (pair not found) but does not exclude other 4xx codes. A 400 is treated as an unreachable service. **`marketDataOutage.ts:9–11` → `swapIndexerOutage.ts:13` → `SwapPage.tsx:595`:** ```ts // marketDataOutage.ts return queries.some((q) => q.isError && isIndexerUnavailableError(q.error)) // swapIndexerOutage.ts return detectMarketDataOutage(simQuery) || !!simData?.indexerTransportFailed // SwapPage.tsx:783 {indexerOutage && (<OutageBanner ... "Market data service unavailable." />)} ``` Because `simQuery.isError = true` and `isIndexerUnavailableError` returns `true` for the 400, `indexerOutage` becomes `true` and the banner fires. **The 404 special-case precedent:** `isIndexerPairNotFoundError` already correctly excludes 404 (pair not in catalog) from the outage classification. The same treatment is needed for 400 (route simulation failed, semantic error from a healthy service). --- ### Steps to reproduce 1. Run the full QA stack with the indexer running: `make start-qa` or `make deploy-local && make indexer-dev` 2. Open the swap page at `/` 3. Select a token pair (observed: EMBER → CORAL) on a fresh deploy where pool liquidity has not fully settled or the indexer has not completed its initial sync 4. Observe: **"Market data service unavailable."** banner appears 5. Stop the indexer 6. Observe: banner **disappears** — demonstrating the indexer's presence (not absence) triggers it 7. Restart the indexer — banner returns until the `/route/solve` 400 errors stop --- ### Expected behavior A 400 response from `/api/v1/route/solve` should be classified as a route-simulation failure, not a service outage. The swap page should either: - Show a route-specific message (e.g. "Route unavailable for this pair — trying direct path") without the outage banner, or - Fall back to the client-side BFS route silently (already implemented) and suppress the outage banner entirely when the indexer is reachable The "Market data service unavailable" banner should only appear when the indexer is genuinely unreachable (network error, `AbortError`, `Failed to fetch`, or 5xx). --- ### Actual behavior Any `400 Bad Request` from `/route/solve` — a healthy indexer responding to an unresolvable route — triggers the outage banner. The banner implies service downtime when the service is up and responding. The error is transient (resolves after pool sync completes on a fresh deploy) but during that window users see a misleading outage state. --- ## Screenshot ![image.png](/uploads/3748bd951031e488b7266d34160d39ea/image.png){width=900 height=565} --- ### Environment - Chain: localterra - LCD: [http://localhost:1317](http://localhost:1317) - Wallet: Keplr (Terra Classic) - Browser: Chromium (DevTools open) - Page: `/` (Swap) - Stack: fresh deploy after `make reset-qa` / `make deploy-local` - Indexer response observed: `"router simulation failed for the given route and hybrid parameters"` - Network throttle applied: No --- **Severity:** P3(Nit) no funds at risk and swaps may still succeed via LCD fallback, but the banner falsely declares the market data service down during a window when it is healthy. Particularly misleading on fresh deploys where this is the first state a tester or user encounters. cc: @PlasticDigits
ghost1 commented 2026-06-05 12:28:19 +00:00 (Migrated from gitlab.com)

mentioned in commit df9b9e54a5

mentioned in commit df9b9e54a5fab95d97cdcdff76413cd0fea00f77
PlasticDigits commented 2026-06-05 12:28:38 +00:00 (Migrated from gitlab.com)

mentioned in merge request !800

mentioned in merge request !800
PlasticDigits commented 2026-06-05 12:28:45 +00:00 (Migrated from gitlab.com)

Implementation complete

MR: https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/merge_requests/82

Changes

  • Added isIndexerClientError in indexerErrors.ts — all indexer 4xx are semantic/client errors, not transport outage
  • isIndexerUnavailableError now excludes 4xx (404 pair-not-found unchanged; 400 route-solve simulation failure no longer triggers banner)
  • Swap noteIndexerFailure / indexerTransportFailed path inherits the fix automatically
  • Docs: skills/AGENTS_FRONTEND_MARKET_DATA_OUTAGE.md, docs/frontend.md

Verification

Criterion Result
400 not classified as outage (indexerErrors.test.ts) PASS
detectMarketDataOutage ignores 400 PASS
detectSwapIndexerOutage ignores 400 PASS
SwapPage: 400 + LCD fallback, no banner (#326) PASS
502 outage banner regression (#241) PASS
make test-frontend (843 tests) PASS
make lint-frontend PASS
Manual fresh-deploy repro SKIP (needs full LocalTerra stack)

Issue remains open pending MR merge.

## Implementation complete MR: https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/merge_requests/82 ### Changes - Added `isIndexerClientError` in `indexerErrors.ts` — all indexer **4xx** are semantic/client errors, not transport outage - `isIndexerUnavailableError` now excludes 4xx (404 pair-not-found unchanged; **400** route-solve simulation failure no longer triggers banner) - Swap `noteIndexerFailure` / `indexerTransportFailed` path inherits the fix automatically - Docs: `skills/AGENTS_FRONTEND_MARKET_DATA_OUTAGE.md`, `docs/frontend.md` ### Verification | Criterion | Result | |-----------|--------| | 400 not classified as outage (`indexerErrors.test.ts`) | PASS | | `detectMarketDataOutage` ignores 400 | PASS | | `detectSwapIndexerOutage` ignores 400 | PASS | | SwapPage: 400 + LCD fallback, no banner (#326) | PASS | | 502 outage banner regression (#241) | PASS | | `make test-frontend` (843 tests) | PASS | | `make lint-frontend` | PASS | | Manual fresh-deploy repro | SKIP (needs full LocalTerra stack) | Issue remains **open** pending MR merge.
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) closed this issue 2026-06-05 12:32:44 +00:00
PlasticDigits commented 2026-06-05 12:44:31 +00:00 (Migrated from gitlab.com)

mentioned in merge request !798

mentioned in merge request !798
PlasticDigits commented 2026-07-13 10:33:12 +00:00 (Migrated from gitlab.com)

mentioned in issue #485

mentioned in issue #485
Sign in to join this conversation.
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#326
No description provided.