Skip to content

fe(recommendations): guard min_savings query params with Number.isFinite (defense-in-depth) #65

Description

@cristim

Problem

frontend/src/api/recommendations.ts:17-19 serializes the savings-floor filters straight to the query string without a finite-number guard:

if (filters.minSavingsUsd) params.set('min_savings_usd', String(filters.minSavingsUsd));
if (filters.minSavingsPct) params.set('min_savings_pct', String(filters.minSavingsPct));

If filters.minSavingsUsd is ever a non-finite TypeScript number (NaN, Infinity, -Infinity), String(...) produces "NaN" / "Infinity" and the request goes out as ?min_savings_usd=NaN. The backend now (correctly) returns a 400 (PR LeanerCloud/cloud-commitments-cli#1233 fixed parseMinSavingsParam), so the user-visible failure mode is opaque — the recommendations list silently shows an error toast instead of falling back to the unfiltered list or surfacing the input as invalid.

Today no UI input writes these fields (a grep across frontend/src/** finds only the API wrapper, types, and tests — no widget reads a value into minSavingsUsd / minSavingsPct), so this is defense-in-depth, not a live user-facing bug. Filing as a follow-up so that when the eventual sliders / number inputs land, they go through a finite-number gate.

Suggested fix

Per feedback_strict_int_parse.md (strict numeric parsing on the FE), guard the filter setters before serialising:

if (filters.minSavingsUsd !== undefined && Number.isFinite(filters.minSavingsUsd) && filters.minSavingsUsd > 0) {
  params.set('min_savings_usd', String(filters.minSavingsUsd));
}
if (filters.minSavingsPct !== undefined && Number.isFinite(filters.minSavingsPct) && filters.minSavingsPct > 0) {
  params.set('min_savings_pct', String(filters.minSavingsPct));
}

Two-line change. Add a Jest case that constructs the filter with minSavingsUsd: NaN and asserts the param is absent from the request URL.

Why

Currently the backend (PR LeanerCloud/cloud-commitments-cli#1233) is the only line of defense. If a slider component lands and clamps an unset value to NaN before the user interacts with it, every page-load fires a 400 against the recommendations API. The strict-parsing rule in the project memory garden exists exactly to keep the FE from shipping non-finite numbers to the backend.

Acceptance

Labels (triage rubric)

priority/p3, severity/low, urgency/low, impact/low, effort/xs, type/defense-in-depth, area/frontend, triaged

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions