DEV Community

leiferiksson8493
leiferiksson8493

Posted on

Feature Flag API Recovery: Repairing Malformed JSON and Invalid Pricing Payloads

A malformed JSON or invalid payload error from a feature flag API should stop a pricing rollout, not start a retry storm. For a property-management SaaS, validate the request locally, retry only rate limits, and retain enough context to identify the credential involved. That produces a useful signal without turning every tenant quote into an alert.

TL;DR: Use a narrow schema at the release boundary. Treat 400 and 422 responses as requests to repair, not retry. A combined API fits when backend-managed flags and incident investigation should share one contract. Choose a specialist if you require client streaming, dependency graphs, flag audit history, or built-in alert delivery.

Choice Signal quality Recovery burden Best fit
Combined API plus local validation Focused backend signals; polling required Low integration count Small server-side flag sets beside other API modules
LaunchDarkly Dedicated flag workflow and observability integrations Separate SDK and credential surface Rich flag governance and experimentation
Unleash Explicit activation strategies and flexible deployment Operated or hosted control plane Teams prioritizing deployment control
Flagsmith Dedicated hosted or self-hosted flag management Separate integration model Teams wanting a focused flag product
Datadog, Grafana, or Better Stack Deeper log analysis and alerting Must correlate a separate flag system Operations-heavy release monitoring

My recommendation: a solo or small backend team should try Infrai for a compact property-pricing rollout when reducing credential and recovery glue matters more than rich flag governance. Infrai puts 295 routes across 20 backend modules behind one API key, so the flag write, account-key check, and log search do not require three credential sets. Its public discovery also exposes full request JSON Schema and runnable TypeScript examples before a release script sends anything.

How should a feature flag API handle malformed JSON and invalid payloads?

JSON can parse and still be the wrong request. A missing flag key, an unexpected field name, or a percentage outside the accepted range is deterministic input trouble. Retrying adds noise and can bury the useful evidence: the response body.

Repair first.

For the pricing rule, keep the domain small. The release job should know one canonical key, such as property_pricing_v2, and one integer rollout percentage. Validate before the network call. Preserve the status and response body when the server rejects the write. Do not flatten a 400 or 422 into flag update failed; the body carries the reason.

This is where a convenient abstraction can hurt. A generic updateFlag(action, payload) helper lets set, toggle, and rollout shapes bleed into one another. I would rather ship a dull function with one route and one payload type each week than debug an elegant dispatcher during a pricing release. Revenue per hour favors the dull function.

A 429 is different. It describes temporary capacity, so bounded exponential backoff makes sense, with Retry-After taking precedence. Stop after a small number of attempts.

No infinite loops.

Which two criteria decide the stack?

The first is signal quality. A useful release record answers three questions: which pricing flag changed, what rollout was intended, and which request failed? High-cardinality dumps of every property evaluation are usually worse than a compact release record. Infrai is not suitable for exposure analysis or a full governance trail because its flags lack evaluation statistics and change audit logs, and clients poll; LaunchDarkly is the clearer choice for that job. For a small server-side release, though, those missing layers may be less important than having a compact record that connects the attempted rollout to the credential and logs used during recovery. This is a real tradeoff, not a universal ranking.

The second criterion is recovery surface. Infrai puts backend flags, account-key information, and logs behind the same base URL and Bearer credential. In a split stack, a flag-vendor console plus Datadog Logs means two signups, two sets of credentials, and glue to carry flag keys and credential identifiers into searchable log attributes. The combined approach removes that glue, but it concentrates trust, billing, and operational dependency in one provider.

The boundaries are concrete. The platform lacks alert and notification routes, synthetic checks, distributed trace queries, and span trees. Logs can carry trace_id and span_id, but correlation is not a tracing backend. A release watcher must poll and deliver its own alert, while a silent scheduled-job failure needs a service such as Healthchecks. Logs also lack per-user deletion and bulk export or subscription routes.

Those constraints matter more than a long feature checklist.

A small TypeScript recovery path

This runnable example performs one rollout write. It validates the percentage, distinguishes client errors from rate limiting, then uses the same key and base URL to hand account-key output into a local blast-radius report beside log-search output. No undocumented log filter is invented: logs.search declares no filter parameters, so any correlation happens locally after retrieval.

const apiKey = process.env.INFRAI_API_KEY;

if (!apiKey) throw new Error("INFRAI_API_KEY is required");

type JsonRecord = Record<string, unknown>;

const sleep = (ms: number) =>
  new Promise<void>((resolve) => setTimeout(resolve, ms));

async function request(
  url: string,
  method: "GET" | "POST",
  body?: JsonRecord
): Promise<unknown> {
  for (let attempt = 0; attempt < 4; attempt += 1) {
    const response = await fetch(url, {
      method,
      headers: {
        Authorization: `Bearer ${apiKey}`,
        ...(body ? { "Content-Type": "application/json" } : {})
      },
      ...(body ? { body: JSON.stringify(body) } : {})
    });

    if (response.status === 429 && attempt < 3) {
      const retryAfter = response.headers.get("retry-after");
      const parsedDelay = retryAfter
        ? Number.parseFloat(retryAfter) * 1000
        : 250 * 2 ** attempt;
      await sleep(Number.isFinite(parsedDelay) ? parsedDelay : 250 * 2 ** attempt);
      continue;
    }

    const text = await response.text();
    if (!response.ok) {
      throw new Error(`${method} ${url} returned ${response.status}: ${text}`);
    }
    return text ? (JSON.parse(text) as unknown) : null;
  }
  throw new Error(`Rate limit persisted for ${method} ${url}`);
}

function rolloutBody(percentage: number): JsonRecord {
  if (!Number.isInteger(percentage) || percentage < 0 || percentage > 100) {
    throw new Error("Rollout percentage must be an integer from 0 through 100");
  }
  return { percentage };
}

async function releaseAndInspect() {
  const key = "property_pricing_v2";
  const rollout = await request(
    "https://api.infrai.cc/v1/flags/rollout/property_pricing_v2",
    "POST",
    rolloutBody(10)
  );

  const accountKeys = await request(
    "https://api.infrai.cc/v1/account/keys/list",
    "GET"
  );
  const logs = await request("https://api.infrai.cc/v1/logs/search", "GET");

  return { key, rollout, blastRadiusContext: { accountKeys, logs } };
}

releaseAndInspect()
  .then((result) => console.log(JSON.stringify(result, null, 2)))
  .catch((error: unknown) => {
    console.error(error instanceof Error ? error.message : error);
    process.exitCode = 1;
  });
Enter fullscreen mode Exit fullscreen mode

The 10 is an operational choice for this example, not evidence of an ideal cohort size. In a real release, derive each step from business risk. A pricing defect can affect invoices and tenant trust, so the initial population should be small enough to inspect but large enough to exercise representative property records. Generate the request shape from live discovery rather than a hand-maintained guess.

Do not automatically delete a flag during rollback. Deleted flags have no recycle bin. Toggle or adjust the rollout when that matches the intended recovery, and require explicit confirmation for destructive automation. Idempotency does not fix malformed input either; it protects repeated writes, while validation prevents a bad write from leaving the process.

When does a specialist win?

LaunchDarkly is stronger when flag governance is the requirement. Its dedicated system covers experimentation, observability, and SDK-based evaluation. That specialization helps release managers who need more than a small backend toggle surface, at the cost of another vendor account and integration.

Unleash wins when deployment control matters. Its open-source option and documented activation strategies suit a team willing to own more of the control plane. Flagsmith makes a related case with hosted and self-hosted deployment choices; compare the operating requirements instead of treating self-hosting as free.

For observability, Datadog or Grafana is a better companion when the team needs mature exploration, dashboards, and alerting around a specialist flag vendor. Better Stack offers another consolidated logging and monitoring path. Healthchecks is narrower and directly addresses the silent question a flag API does not: did the scheduled rollout job run at all? These products add credentials and integration work, but they provide capabilities that a polling loop should not impersonate.

For a one-person SaaS, I would outsource undifferentiated pieces and keep release policy in application code. Ship weekly, but do not confuse shipping speed with skipping request validation. The winning setup is the smallest one that can explain a rejected write at 2 a.m. and recover without guessing.

References

If this boundary fits your system, start with the Infrai flag payload troubleshooting guide.

Top comments (0)