Skip to content

Traffic Control

Traffic policies control admission, rate, payload safety, upstream shaping, and response filtering.

PolicyPriorityPurpose
ipFilter1IP allow/deny with CIDR support.
geoIpFilter1Country allow/deny via header (default cf-ipcountry).
sslEnforce5Enforce HTTPS and apply HSTS.
requestLimit5Reject large requests via content-length.
jsonThreatProtection5Enforce JSON structural/body limits.
regexThreatProtection5Block suspicious patterns in path/query/headers/body.
rateLimit20Sliding-window rate limiting with pluggable store.
cache40Response caching with pluggable store.
dynamicRouting50Evaluate rules and set dynamic routing context values.
httpCallout50Call external services during pipeline execution.
interrupt100Conditionally short-circuit with static response.
trafficShadow92Mirror a sampled subset of traffic to shadow target.
resourceFilter92Remove/allow JSON response fields.

Sliding-window limiting. Uses InMemoryRateLimitStore by default.

import { rateLimit } from "@homegrower-club/stoma";
interface RateLimitConfig {
max: number;
windowSeconds?: number; // default: 60
keyBy?: (c: Context) => string | Promise<string>;
store?: RateLimitStore;
statusCode?: number; // default: 429
message?: string; // default: "Rate limit exceeded"
ipHeaders?: string[]; // default via extractClientIp
skip?: (c: Context) => boolean | Promise<boolean>;
}

Every response includes:

  • x-ratelimit-limit
  • x-ratelimit-remaining
  • x-ratelimit-reset

Rejected responses also include:

  • retry-after
interface RateLimitStore {
increment(key: string, windowSeconds: number): Promise<{ count: number; resetAt: number }>;
destroy?(): void;
}

Allow/deny by IP or CIDR.

import { ipFilter } from "@homegrower-club/stoma";
interface IpFilterConfig {
allow?: string[];
deny?: string[];
mode?: "allow" | "deny"; // default: "deny"
ipHeaders?: string[];
skip?: (c: Context) => boolean | Promise<boolean>;
}
  • allow mode denies unless the IP matches.
  • deny mode allows unless the IP matches.

Error: 403 ip_denied.


Allow/deny by country code from a request header.

import { geoIpFilter } from "@homegrower-club/stoma";
interface GeoIpFilterConfig {
allow?: string[];
deny?: string[];
mode?: "allow" | "deny"; // default: "deny"
countryHeader?: string; // default: "cf-ipcountry"
skip?: (c: Context) => boolean | Promise<boolean>;
}

Error: 403 geo_denied.


Response cache with configurable TTL, method filter, key strategy, and store backend.

import { cache } from "@homegrower-club/stoma";
interface CacheConfig {
ttlSeconds?: number; // default: 300
methods?: string[]; // default: ["GET"]
cacheKeyFn?: (c: Context) => string;
varyHeaders?: string[];
store?: CacheStore; // default: InMemoryCacheStore
respectCacheControl?: boolean; // default: true
cacheStatusHeader?: string; // default: "x-cache"
bypassDirectives?: string[]; // default: ["no-store", "no-cache"]
skip?: (c: Context) => boolean | Promise<boolean>;
}
interface CacheStore {
get(key: string): Promise<Response | null>;
put(key: string, response: Response, ttlSeconds: number): Promise<void>;
delete(key: string): Promise<boolean>;
}

Responses get x-cache: HIT|MISS|BYPASS (or your configured header name).


Enforce HTTPS and set HSTS on secure responses.

import { sslEnforce } from "@homegrower-club/stoma";
interface SslEnforceConfig {
redirect?: boolean; // default: true
hstsMaxAge?: number; // default: 31536000
includeSubDomains?: boolean; // default: false
preload?: boolean; // default: false
skip?: (c: Context) => boolean | Promise<boolean>;
}

Behavior:

  • Non-HTTPS with redirect: true: returns 301 with Location on https://.
  • Non-HTTPS with redirect: false: throws 403 ssl_required.
  • HTTPS responses: adds Strict-Transport-Security.

Reject requests whose declared content-length exceeds maxBytes.

import { requestLimit } from "@homegrower-club/stoma";
interface RequestLimitConfig {
maxBytes: number;
message?: string; // default: "Request body too large"
skip?: (c: Context) => boolean | Promise<boolean>;
}

Error: 413 request_too_large.


Validate JSON body structure and size limits before business logic.

import { jsonThreatProtection } from "@homegrower-club/stoma";
interface JsonThreatProtectionConfig {
maxDepth?: number; // default: 20
maxKeys?: number; // default: 100
maxStringLength?: number; // default: 10000
maxArraySize?: number; // default: 100
maxBodySize?: number; // default: 1048576
contentTypes?: string[]; // default: ["application/json"]
skip?: (c: Context) => boolean | Promise<boolean>;
}

Errors:

  • 413 body_too_large
  • 400 invalid_json
  • 400 json_threat

Run regex threat rules against path/query/headers/body.

import { regexThreatProtection } from "@homegrower-club/stoma";
interface RegexPatternRule {
regex: string;
targets: Array<"path" | "headers" | "body" | "query">;
message?: string;
}
interface RegexThreatProtectionConfig {
patterns: RegexPatternRule[];
flags?: string; // default: "i"
contentTypes?: string[]; // default: ["application/json", "text/plain"]
maxBodyScanLength?: number; // default: 65536
skip?: (c: Context) => boolean | Promise<boolean>;
}

Error: 400 threat_detected.


Conditionally short-circuit the pipeline and return a static response.

import { interrupt } from "@homegrower-club/stoma";
interface InterruptConfig {
condition: (c: Context) => boolean | Promise<boolean>;
statusCode?: number; // default: 200
body?: unknown;
headers?: Record<string, string>; // default: {}
skip?: (c: Context) => boolean | Promise<boolean>;
}

When condition is true, next() is not called.


Evaluate routing rules and expose match results via context keys.

import { dynamicRouting } from "@homegrower-club/stoma";
interface RoutingRule {
name?: string;
condition: (c: Context) => boolean | Promise<boolean>;
target: string;
rewritePath?: (path: string) => string;
headers?: Record<string, string>;
}
interface DynamicRoutingConfig {
rules: RoutingRule[];
fallthrough?: boolean; // default: true
skip?: (c: Context) => boolean | Promise<boolean>;
}

When a rule matches, it sets:

  • c.set("_dynamicTarget", string)
  • c.set("_dynamicRewrite", (path) => string) (optional)
  • c.set("_dynamicHeaders", Record<string,string>) (optional)

Error when fallthrough: false and no rule matches: 404 no_route.


Make an external HTTP call during request processing.

import { httpCallout } from "@homegrower-club/stoma";
interface HttpCalloutConfig {
url: string | ((c: Context) => string | Promise<string>);
method?: string; // default: "GET"
headers?: Record<string, string | ((c: Context) => string | Promise<string>)>;
body?: unknown | ((c: Context) => unknown | Promise<unknown>);
timeout?: number; // default: 5000
onResponse: (response: Response, c: Context) => void | Promise<void>;
onError?: (error: unknown, c: Context) => void | Promise<void>;
abortOnFailure?: boolean; // default: true
skip?: (c: Context) => boolean | Promise<boolean>;
}

Default error path throws 502 callout_failed.


Mirror sampled requests to a secondary target after primary response flow.

import { trafficShadow } from "@homegrower-club/stoma";
interface TrafficShadowConfig {
target: string;
percentage?: number; // default: 100
methods?: string[]; // default: ["GET","POST","PUT","PATCH","DELETE"]
mirrorBody?: boolean; // default: true
timeout?: number; // default: 5000
onError?: (error: unknown) => void;
skip?: (c: Context) => boolean | Promise<boolean>;
}

Notes:

  • Shadow failures never affect the primary response.
  • If adapter waitUntil is available, shadow work is scheduled there.

Filter JSON response fields in allow or deny mode.

import { resourceFilter } from "@homegrower-club/stoma";
interface ResourceFilterConfig {
mode: "allow" | "deny";
fields: string[]; // dot paths, e.g. "user.password"
contentTypes?: string[]; // default: ["application/json"]
applyToArrayItems?: boolean; // default: true
skip?: (c: Context) => boolean | Promise<boolean>;
}
  • deny: remove listed fields.
  • allow: keep only listed fields.

import {
ipFilter,
sslEnforce,
requestLimit,
jsonThreatProtection,
regexThreatProtection,
rateLimit,
} from "@homegrower-club/stoma";
[
ipFilter({ deny: ["203.0.113.0/24"] }),
sslEnforce({ redirect: true }),
requestLimit({ maxBytes: 256_000 }),
jsonThreatProtection({ maxDepth: 10, maxBodySize: 256_000 }),
regexThreatProtection({ patterns: [{ regex: "<script", targets: ["body", "query"] }] }),
rateLimit({ max: 100, windowSeconds: 60 }),
]