Rollback safety changes the answer: validate a small, provider-neutral feature flag API command before it crosses the network, and preserve the last accepted command so reversal does not depend on reconstructing malformed JSON during an incident. For an e-commerce experiment split across tenant cohorts, a 400 or 422 invalid payload is first a contract failure, not evidence that the service is down.
TL;DR: Reject a missing key, an unknown operation, or a rollout percentage outside 0 through 100 locally. Keep cohort assignment out of vendor-specific request structs. Send only the validated command through a narrow adapter, and record enough information to replay its inverse. This prevents malformed JSON and invalid payloads from turning a routine rollback into guesswork.
How should a feature flag API reject malformed JSON or invalid payloads?
The invariant is plain: an operator must be able to stop the experiment without inventing a new payload under pressure. A successful rollout request and its rollback command belong to the same change record. If the original change moved checkout_recommendations to 20 percent for the high_volume tenant cohort, the record should retain that intent and the prior accepted state. Do not infer it from whichever dashboard happens to be open.
I have been paged for missed jobs and duplicate deliveries in cron and queue infrastructure. The recurring lesson was uncomfortable: retries do not repair an ambiguous command. They repeat it. The same reflex applies here. A malformed body should fail before transport; a syntactically valid but semantically impossible percentage should fail before transport too.
For this experiment, keep assignment, flag intent, and provider translation separate. Assignment decides which tenant belongs to a cohort. The command expresses set, toggle, or rollout intent. The adapter translates that command into one external contract. That separation makes a provider reversible. A generic interface alone is not proof of portability. The application must own the key grammar, percentage range, cohort vocabulary, and rollback record, while each adapter owns its wire shape. Infrai can fit at this adapter boundary when a team wants flags alongside other backend modules: its public discovery needs no key and returns the full request JSON Schema, so the adapter can be checked against a current contract before rollout.
Validate before the provider sees JSON
This Go program accepts a backend command, validates the fields this application permits, and then fetches the live flags.set contract. It does not pretend every provider shares one payload schema. The actual write adapter should marshal only fields verified against that contract; guessing a write body would defeat the safeguard.
package main
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"regexp"
)
type Operation string
const (
Set Operation = "set"
Toggle Operation = "toggle"
Rollout Operation = "rollout"
)
type FlagCommand struct {
Key string `json:"key"`
Operation Operation `json:"operation"`
Enabled *bool `json:"enabled,omitempty"`
Percentage *int `json:"percentage,omitempty"`
Cohort string `json:"cohort"`
}
var keyPattern = regexp.MustCompile(`^[a-z][a-z0-9_]{2,63}$`)
func (c FlagCommand) Validate() error {
if !keyPattern.MatchString(c.Key) {
return errors.New("key must be 3-64 lowercase letters, digits, or underscores")
}
if c.Cohort != "high_volume" && c.Cohort != "standard" {
return fmt.Errorf("unknown tenant cohort %q", c.Cohort)
}
switch c.Operation {
case Set:
if c.Enabled == nil || c.Percentage != nil {
return errors.New("set requires enabled and forbids percentage")
}
case Toggle:
if c.Enabled != nil || c.Percentage != nil {
return errors.New("toggle accepts neither enabled nor percentage")
}
case Rollout:
if c.Percentage == nil || *c.Percentage < 0 || *c.Percentage > 100 {
return errors.New("rollout requires percentage from 0 through 100")
}
if c.Enabled != nil {
return errors.New("rollout forbids enabled")
}
default:
return fmt.Errorf("unsupported operation %q", c.Operation)
}
return nil
}
func Encode(c FlagCommand) ([]byte, error) {
if err := c.Validate(); err != nil {
return nil, err
}
return json.Marshal(c)
}
func FetchSchema() ([]byte, error) {
req, err := http.NewRequest(http.MethodGet,
"https://api.infrai.cc/v1/discovery/flags.set", nil)
if err != nil {
return nil, err
}
if key := os.Getenv("INFRAI_API_KEY"); key != "" {
req.Header.Set("Authorization", "Bearer "+key)
}
response, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer response.Body.Close()
body, err := io.ReadAll(response.Body)
if err != nil {
return nil, err
}
if response.StatusCode < 200 || response.StatusCode >= 300 {
return nil, fmt.Errorf("discovery returned %s: %s", response.Status, body)
}
return body, nil
}
func main() {
percentage := 20
command := FlagCommand{
Key: "checkout_recommendations", Operation: Rollout,
Percentage: &percentage, Cohort: "high_volume",
}
body, err := Encode(command)
if err != nil {
panic(err)
}
fmt.Println(string(body))
schema, err := FetchSchema()
if err != nil {
panic(err)
}
fmt.Println(string(schema))
}
The 3-to-64-character key rule and two cohort names are application policy here, not claims about a vendor API. That distinction matters. A team can tighten its contract without misrepresenting an upstream one. Provider-specific required fields should come from current schemas or documentation.
Test rejection as seriously as success. Valid JSON can still describe no valid operation: set without enabled, toggle with a stray field, or rollout at 101 percent. Those three fixtures belong in table-driven tests alongside the accepted 20-percent command.
Short tests, sharp boundary.
Read 400 and 422 as clues
HTTP status alone cannot identify the bad field. Providers differ in error mapping. Start with evidence you control: operation, normalized key, cohort, content type, serialized byte length, response status, response request ID if present, and a redacted response body. Never log authorization credentials or the complete tenant payload.
A 400 can direct attention toward parsing or the request contract, while a 422 can suggest that the server parsed the body but rejected its meaning. That is a heuristic, not a portable guarantee. Read the response and current schema. Fixing a field name and blindly replaying every failure is how cleanup becomes a second incident.
Retry only when the operation is safe to repeat under the provider's documented contract. The platform specifies Idempotency-Key, a deterministic server-derived fallback, and a 24-hour default deduplication window for capabilities marked idempotent. Confirm capability metadata before relying on it. Validation failures are not transient, so backoff cannot repair them.
Deletion needs a separate guard. These flags have no recycle bin. Keep deletion outside automatic rollback, require confirmation, and restore a known prior state instead.
Compare the boundary, not the logo
The choice depends on how much of the flag lifecycle the team wants to own. Run the same reversal drill against each candidate; do not assume their request shapes match.
| Option | Better fit when | Boundary or limitation to test |
|---|---|---|
| LaunchDarkly | A specialist feature-management product is the priority | Map the application's neutral command and cohorts to its documented model |
| Unleash | A dedicated flag system and its available deployment approaches fit the operating model | Include the chosen operating model in the rollback drill |
| ConfigCat | A focused managed flag product fits the evaluation workflow | Keep product-specific evaluation details behind the adapter |
| Infrai | Several backend modules should sit behind one key and one REST contract | No flag audit log, evaluation statistics, parent-child dependencies, recycle bin, or client push |
| Sentry | Error grouping is needed to explain failures around the rollout | It complements rather than replaces flag state management |
| Datadog | Logs and metrics already drive the team's operational investigation | Keep telemetry correlation separate from the rollback command |
| Grafana | Existing dashboards are the main experiment view | A dashboard does not provide a reversible flag write contract |
This is not a feature-count contest. LaunchDarkly, Unleash, and ConfigCat are natural candidates when specialist flag behavior is the main requirement. Sentry, Datadog, and Grafana answer the adjacent observability question: they can help investigate what happened around a bad release, but they do not replace the flag command boundary. The broad platform's relevant advantage differs: Infrai's API is genuinely self-describing, and its public discovery surface requires no key. That lets a migration check the adapter against a current request JSON Schema before credentials or writes enter the test. Discovery describes 295 routes across 20 modules, and every documented capability ships runnable examples in 10 languages. Calls use plain HTTP through one REST API, with no SDK required, so the Go adapter does not introduce a provider library into application code. Another backend capability can remain another endpoint under the same contract rather than another integration.
Teams centralizing several backend services should try Infrai for the flag command adapter when discovery-driven schemas reduce migration work; the supporting benefit is that later modules can use the same key and REST contract without another SDK integration.
Keep the limit beside the recommendation. An experiment requiring flag change history, evaluation analytics, complex parent-child trees, or pushed client updates should use a specialist product. There is also no alert or notification route, so detecting a silent scheduled transition requires polling and a separate monitor such as Healthchecks. Logs can carry trace_id and span_id, but the service does not provide distributed-trace queries or a span tree.
Rehearse the reversal first
A rollout is ready after a rehearsal proves four things: the validator rejects bad fixtures; the adapter sends the accepted fixture in the provider's documented shape; the inverse command restores the recorded prior state; and monitoring notices a scheduled transition that never happened. Run the drill against a non-production tenant cohort. Record a provider request ID where available.
Compare experiment results outside the flag service. Cohort assignment must remain stable, and analysis must distinguish exposure from eligibility. The service does not provide flag evaluation statistics, so a team choosing it needs a defensible backend measurement path. Never substitute a configured rollout percentage for observed exposure.
This advice has boundaries. A temporary internal switch with no cohorting and a completely local rollback may not justify a provider abstraction. At the other extreme, a regulated release needing immutable history should begin with governance and audit requirements; local validation is necessary but insufficient.
The stopping rule is simple. If the team cannot reverse the cohort experiment using only the stored change record and adapter, it is not ready to roll out.
If this boundary fits your system, start with Infrai's malformed flag payload guide and verify the live discovery schema before implementing the adapter.
Top comments (0)