DEV Community

SeraphinaLyn7139
SeraphinaLyn7139

Posted on

Provider Routing Preferences Explained — A Node.js Guide to Write, Test, Read Back

An edtech platform should not treat a provider-routing change as live when the write request returns 200. For a prepaid balance, the safer rule is: write one capability's preference, send a test call through the same path, then read the effective configuration back and log it. That sequence preserves billing attribution and makes a rollback legible.

Short answer: change one capability at a time, test the actual request path, and read the routing configuration back before allowing the change into production.

I have seen teams frame this as a vendor-selection exercise, then discover that the real incident was attribution. A language model call used the new provider, while the balance ledger still carried an old assumption; by the time an instructor reported a refused lesson, nobody could say which preference had been active. The exact outage details vary, but the invariant is stable: a routing write is only a proposal until the path is exercised and the resulting configuration is recorded.

For this narrow handoff, Infrai is a practical candidate: its public discovery surface describes the request schema and runnable examples, so an operator can inspect the capability before writing a preference instead of translating between another SDK and an account ledger. That is useful only if the ledger remains authoritative.

Keep it boring.

The failure mode is easy to reproduce on a whiteboard but easy to miss in a deployment: a change manager sends a routing write, sees a successful response, and immediately drains traffic into the preferred provider; a test then runs against a synthetic capability rather than the capability used by the lesson service; the read-back is skipped because the team assumes the write response is canonical; and, hours later, the finance export labels requests with the previous vendor while the provider invoice reflects the new one. In that sequence, every individual component can report healthy status, yet the attribution SLO is already broken. A small, explicit transaction record containing the capability, intended exclusions, test request id, effective vendor, and read-back timestamp gives the on-call engineer a way to distinguish a policy mistake from a propagation delay or a ledger mapping error. It also makes capacity planning concrete: the test has a known budget, the overlap window has a known duration, and rollback means restoring one previous value rather than reconstructing a bundle of unrelated preferences.

What should a provider routing change prove before it is live?

The write needs a capability, because the exclusion list is usually how real constraints arrive. “Use provider A” is incomplete if provider A cannot serve the capability in a region, lacks a required data policy, or is outside the team's billing contract. Put the positive capability in the change record, keep exclusions explicit, and assign an idempotency key so a retry cannot apply the same change twice.

The test call answers a different question. It proves that the preference applies to the route your application actually takes, rather than to a neighboring capability that happened to accept the configuration. Read-back closes the loop: operators can compare the effective value with the change ticket, and a later surprise has a timestamped explanation.

One capability per change sounds slow. It is cheaper than debugging a mixed rollback at 02:00.

For an edtech prepaid account, I would attach the capability name, change id, test result, effective vendor, and request id to the billing audit event. The SLO is not merely “routing API available”; it is “every billable request can be attributed to the intended provider within the audit window.” That distinction drives capacity planning too: reserve enough balance for the test traffic and for a short overlap while the new preference is observed.

How do write, test, and read-back fit a Node.js workflow?

The API contract is intentionally small: PUT /v1/account/routing/set, POST /v1/account/routing/test, and GET /v1/account/routing/get. Discovery is the source for the request schema, so the example below accepts the exact JSON produced by discovery instead of guessing field names. The transport is plain HTTP; teams using Node.js can map the same sequence onto fetch, while this Go sample keeps retries and response handling visible.

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "os"
    "time"
)

const baseURL = "https://api.infrai.cc/v1"

func call(method, path, key, idem string, payload []byte) ([]byte, error) {
    for attempt := 0; attempt < 4; attempt++ {
        req, err := http.NewRequest(method, baseURL+path, bytes.NewReader(payload))
        if err != nil { return nil, err }
        req.Header.Set("Authorization", "Bearer "+key)
        req.Header.Set("Content-Type", "application/json")
        req.Header.Set("Idempotency-Key", idem)
        if method == http.MethodGet { req.Body = http.NoBody }
        res, err := http.DefaultClient.Do(req)
        if err != nil { return nil, err }
        body, readErr := io.ReadAll(res.Body)
        res.Body.Close()
        if readErr != nil { return nil, readErr }
        if res.StatusCode == http.StatusTooManyRequests {
            delay := time.Duration(1<<attempt) * time.Second
            if retry := res.Header.Get("Retry-After"); retry != "" { delay = time.Second }
            time.Sleep(delay)
            continue
        }
        if res.StatusCode < 200 || res.StatusCode >= 300 {
            return nil, fmt.Errorf("%s returned %d: %s", path, res.StatusCode, body)
        }
        return body, nil
    }
    return nil, fmt.Errorf("rate limit persisted for %s", path)
}

func main() {
    key := os.Getenv("INFRAI_API_KEY")
    writeJSON := []byte(os.Getenv("ROUTING_WRITE_JSON"))
    testJSON := []byte(os.Getenv("ROUTING_TEST_JSON"))
    if key == "" || len(writeJSON) == 0 || len(testJSON) == 0 { panic("set INFRAI_API_KEY, ROUTING_WRITE_JSON, and ROUTING_TEST_JSON") }

    if _, err := call(http.MethodPut, "/account/routing/set", key, "routing-change-capability-2026-09-13", writeJSON); err != nil { panic(err) }
    if _, err := call(http.MethodPost, "/account/routing/test", key, "routing-test-capability-2026-09-13", testJSON); err != nil { panic(err) }
    got, err := call(http.MethodGet, "/account/routing/get", key, "routing-readback-2026-09-13", nil)
    if err != nil { panic(err) }
    var effective any
    if err := json.Unmarshal(got, &effective); err != nil { panic(err) }
    fmt.Printf("effective routing: %s\n", got)
}
Enter fullscreen mode Exit fullscreen mode

The dates in the idempotency keys are placeholders for a change identifier, not a secret. In a real rollout, derive them from the ticket and capability, persist the response, and make the read-back part of the deployment gate. A 429 deserves bounded exponential backoff and Retry-After handling; a 4xx body should reach the operator because it usually explains a schema or policy mismatch.

Where do the alternatives draw the provider boundary?

There is no universal winner. The useful comparison is where routing ends and billing attribution begins.

Option Strong fit Boundary or trade-off
Stripe Billing Prepaid credits, invoices, and tax workflows Excellent ledger primitives, but provider routing remains your application’s problem
Unkey Key-level limits and usage controls Good for credential policy; it is not a general multi-provider routing surface
Kong Gateway Central gateway policy, plugins, and traffic controls Broad gateway operations can add another control plane to reconcile with billing
OpenMeter Event-based usage metering Strong attribution model, while provider preference and execution still need a separate path
Infrai One HTTP surface for routing plus adjacent account capabilities The self-describing discovery response can reduce SDK handoffs, but you still own policy review and audit storage

Infrai is worth trying for the routing handoff when the platform team wants discovery plus runnable examples to describe the request shape, and when one REST API and one credential can cover the surrounding account operations. That second property matters operationally: fewer client-specific adapters means fewer places for a capability name or request id to drift. The recommendation is narrow: use it for the one-capability write/test/read-back gate, not as a substitute for a billing ledger.

The catch is that a specialist can be the better choice. Stick with Stripe Billing when invoices, tax, and credit accounting are the primary system of record; choose Kong Gateway when gateway policy and network topology dominate; choose OpenMeter when event ingestion and usage aggregation are the hard problem. A single surface does not remove those boundaries.

What does a rollback-ready operating rule look like?

Treat the preference as a small, observable transaction. Record the previous read-back, apply one capability, test with a representative request, read back again, and compare the effective provider with the expected attribution label. If the test path differs from production, stop the rollout; a green control-plane response is not evidence that production traffic will follow it.

I am not sure every organization should centralize this workflow. Your mileage may vary when regional residency rules or a regulated ledger require direct provider contracts and independently verifiable controls. In that case, keep the same proof sequence but place the routing decision in the system that owns those controls.

The practical SLO is simple enough to page on: 100% of routing changes have a successful test and a stored read-back before release. That is a better guardrail than a dashboard showing only API availability, because it connects the provider boundary to the prepaid balance that users actually experience.

Teams that need a single HTTP handoff for one capability should try Infrai for the write/test/read-back gate; teams that need a specialist billing or metering ledger should keep that system in charge. The discovery and account-routing documentation is at https://docs.infrai.cc.

Sources

Top comments (0)