Redaction¶
Redaction is httptape's most distinctive feature. Sensitive data (secrets, PII, credentials) is redacted or replaced with deterministic fakes before it touches disk. This is the second R in httptape's Record, Redact, Replay pipeline.
The redaction pipeline is implemented in Go as a Pipeline of SanitizeFunc transformations (the Go types use "sanitize" terminology). Each function receives a Tape and returns a (possibly modified) copy. Functions are applied in order.
type SanitizeFunc func(Tape) Tape
type Pipeline struct { /* ... */ }
func NewPipeline(funcs ...SanitizeFunc) *Pipeline
func (p *Pipeline) Sanitize(t Tape) Tape
The Pipeline implements the Sanitizer interface, which the Recorder accepts:
Building a redaction pipeline¶
sanitizer := httptape.NewPipeline(
httptape.RedactHeaders(),
httptape.RedactBodyPaths("$.password", "$.ssn"),
httptape.FakeFields("my-seed", "$.email", "$.user_id"),
)
rec := httptape.NewRecorder(store,
httptape.WithSanitizer(sanitizer),
)
Functions are applied in order. In this example: headers are redacted first, then body fields are redacted, then remaining fields get deterministic fakes.
RedactHeaders¶
Replaces header values with "[REDACTED]" in both request and response headers.
Default sensitive headers: - Authorization -- bearer tokens, basic auth - Cookie -- session tokens - Set-Cookie -- server-set sessions - X-Api-Key -- API key auth - Proxy-Authorization -- proxy auth - X-Forwarded-For -- client IPs (PII)
To redact specific headers:
Header matching is case-insensitive per the HTTP spec.
You can retrieve the default list programmatically:
defaults := httptape.DefaultSensitiveHeaders()
// ["Authorization", "Cookie", "Set-Cookie", "X-Api-Key", "Proxy-Authorization", "X-Forwarded-For"]
RedactBodyPaths¶
Redacts fields within JSON request and response bodies at specified paths.
Path syntax¶
Paths use a JSONPath-like syntax:
| Pattern | Description |
|---|---|
$.field | Top-level field |
$.nested.field | Nested field access |
$.array[*].field | Field within each element of an array |
Redaction behavior¶
Redacted values are type-aware:
| JSON type | Redacted value |
|---|---|
| string | "[REDACTED]" |
| number | 0 |
| boolean | false |
| null, object, array | Unchanged |
If the body is not valid JSON, it is left unchanged (no error). Missing paths are silently skipped.
Example¶
Input body:
With RedactBodyPaths("$.password", "$.profile.ssn"):
FakeFields¶
Replaces field values with deterministic fakes derived from HMAC-SHA256. The same seed and input value always produce the same fake output, preserving cross-fixture consistency.
The seed parameter¶
The first argument is a project-level seed used as the HMAC key. Different seeds produce different fakes. The same seed and input always produce the same output.
The seed must be non-empty and unique to your project. Treat it as a moderately sensitive value: anyone who knows the seed can predict the fake output for any input. Do not use an empty string, a default placeholder, or a seed shared across unrelated projects. Store it alongside other project configuration (e.g. environment variables), not in public source code.
Faking strategies¶
The fake value depends on the detected type of the original value:
| Detected type | Fake format | Example |
|---|---|---|
Email (contains @) | user_<hash>@example.com | user_a1b2c3d4@example.com |
| UUID (8-4-4-4-12 hex) | Deterministic UUID v5 | a1b2c3d4-e5f6-5789-abcd-0123456789ab |
| Number (float64) | Positive integer [1, 2^31-1] | 1234567890 |
| Other string | fake_<hash> | fake_a1b2c3d4 |
| Boolean, null, object, array | Unchanged | -- |
Example¶
Input body:
{
"user": {
"email": "alice@company.com",
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Alice Smith"
}
}
With FakeFields("my-seed", "$.user.email", "$.user.id", "$.user.name"):
{
"user": {
"email": "user_7f3a2b1c@example.com",
"id": "7f3a2b1c-4d5e-5f60-8a9b-c0d1e2f3a4b5",
"name": "fake_7f3a2b1c"
}
}
The key property: if alice@company.com appears in another fixture, it will be faked to the same value. This preserves relational consistency across your fixture set.
Typed fakers¶
FakeFields auto-detects the faking strategy from each value's runtime type. When you need explicit control -- for example, to force a generic-looking string into the credit-card format, or to fake a numeric ID as a fixed-length digit string -- use the typed-faker API.
The contract is the Faker interface:
Implementations take an HMAC seed and the original JSON value (already decoded by encoding/json, so strings are string, numbers are float64, booleans are bool, nulls are nil, objects are map[string]any, arrays are []any). They must be deterministic: the same seed and original must always produce the same fake.
Typed fakers are wired into a pipeline with FakeFieldsWith, which takes a seed and a path-to-Faker map:
sanitizer := httptape.NewPipeline(
httptape.FakeFieldsWith("my-seed", map[string]httptape.Faker{
"$.user.email": httptape.EmailFaker{},
"$.user.phone": httptape.PhoneFaker{},
"$.card.number": httptape.CreditCardFaker{},
"$.card.cvv": httptape.NumericFaker{Length: 3},
}),
)
Path syntax is identical to RedactBodyPaths and FakeFields. Invalid JSON bodies, missing paths, and invalid path strings are silently skipped (no error). All twelve built-in fakers are constructed as struct literals; none has a NewXFaker constructor.
Auto-detect vs. typed -- when to use each¶
| Use case | API |
|---|---|
| Mixed body, you trust the value-type heuristic, want minimum config | FakeFields(seed, paths...) |
| You need a specific format (credit card, fixed-length digits, pattern, prefix) | FakeFieldsWith(seed, fields) |
| You want to fully redact a leaf rather than fake it | FakeFieldsWith(...) with RedactedFaker{} |
| You want a constant value at a path regardless of input | FakeFieldsWith(...) with FixedFaker{Value: ...} |
| You need a custom format the built-ins do not cover | Implement Faker and pass it to FakeFieldsWith |
Built-in fakers -- redaction-style¶
These fakers replace values without preserving any information from the original. Use them when the original content is sensitive enough that even a deterministic transform of it should not appear in fixtures.
RedactedFaker¶
Replaces strings with "[REDACTED]", numbers with 0, and booleans with false. Other types (nil, objects, arrays) pass through unchanged. Equivalent to the leaf behavior of RedactBodyPaths, but addressable through the typed-faker map so you can mix it with other fakers in a single FakeFieldsWith call.
httptape.FakeFieldsWith("seed", map[string]httptape.Faker{
"$.password": httptape.RedactedFaker{},
"$.email": httptape.EmailFaker{},
})
FixedFaker¶
Always returns its Value field, ignoring both seed and original. Useful for stamping a known sentinel into a field (e.g., "status": "active") so downstream tests can assert against it.
httptape.FixedFaker{Value: "active"}
httptape.FixedFaker{Value: float64(1)}
httptape.FixedFaker{Value: true}
Value is any, so the encoded JSON type follows Go's encoding/json rules.
Built-in fakers -- generic deterministic¶
These fakers produce a deterministic value derived from HMAC-SHA256(seed, original), with no PII shape. Use them when you need consistency across fixtures but do not need the output to look like any particular format.
HMACFaker¶
Mirrors the auto-detect default for generic strings and numbers. Strings become "fake_<8-hex>"; numbers become a positive integer in [1, 2^31-1]; booleans, nulls, objects, and arrays pass through unchanged.
NumericFaker¶
Generates a string of Length HMAC-derived digits. Useful for fixed-width numeric IDs (CVVs, OTPs, account numbers) where digit-count matters.
If the input is not a string, it is returned unchanged. Output length always equals Length, even if Length exceeds 32 (the HMAC is re-chained).
PatternFaker¶
Fills a template where # becomes a digit, ? becomes a lowercase letter, and any other character is copied literally.
httptape.PatternFaker{Pattern: "###-##-####"} // SSN-shaped
httptape.PatternFaker{Pattern: "??-#####"} // 2 letters, dash, 5 digits
httptape.PatternFaker{Pattern: "ORDER-####-????"} // mixed literal + variable
If the input is not a string, it is returned unchanged.
PrefixFaker¶
Generates "<Prefix><16-hex>". Useful when an upstream issues namespaced identifiers (e.g., cust_*, order_*) and downstream tests look at the prefix.
httptape.PrefixFaker{Prefix: "cust_"} // "cust_a1b2c3d4e5f60718"
httptape.PrefixFaker{Prefix: "order_"} // "order_a1b2c3d4e5f60718"
If the input is not a string, it is returned unchanged.
DateFaker¶
Generates a date string formatted with Format (Go reference layout; defaults to "2006-01-02" when empty). The date is drawn deterministically from a ~100-year window starting at 2000-01-01.
httptape.DateFaker{} // "2042-09-13"
httptape.DateFaker{Format: "2006-01-02"} // "2042-09-13"
httptape.DateFaker{Format: time.RFC3339} // "2042-09-13T00:00:00Z"
If the input is not a string, it is returned unchanged.
Built-in fakers -- PII-shaped¶
These fakers preserve a recognizable shape (so downstream parsers do not break) while replacing the underlying content. They are the right choice for fields whose format matters to consumers (clients that validate emails, payment processors that check Luhn, address forms that expect a US zip).
EmailFaker¶
Replaces strings with "user_<8-hex>@example.com". Non-string inputs pass through unchanged.
PhoneFaker¶
Replaces digits in the input with HMAC-derived digits while preserving every non-digit character (spaces, dashes, parentheses, plus signs). Output length always equals input length.
If the input is not a string, it is returned unchanged.
CreditCardFaker¶
Generates a 16-digit number formatted as XXXX-XXXX-XXXX-XXXX. The first 6 digits (issuer prefix) are taken from the original; if the original has fewer than 6 digits, the prefix 400000 is used. The middle 9 digits are HMAC-derived; the last digit is a valid Luhn check digit.
If the input is not a string, it is returned unchanged.
NameFaker¶
Picks a first name and a last name from internal fixed lists using two HMAC bytes. Output is "<First> <Last>".
If the input is not a string, it is returned unchanged.
AddressFaker¶
Generates a US-style address: "<number> <street> <suffix>, <city>, <ST> <zip>". House number is in [1, 9999]; zip is 5 digits; city, state, and street components are picked from internal fixed lists.
If the input is not a string, it is returned unchanged.
Custom fakers¶
Anything that satisfies the Faker interface works. A typical use case is wrapping an existing data source (a list of canonical fake company names, a generator of valid IBANs, etc.) so the recorded fixtures are coherent with the rest of your test data.
import (
"crypto/hmac"
"crypto/sha256"
)
type CompanyFaker struct {
Names []string
}
func (f CompanyFaker) Fake(seed string, original any) any {
s, ok := original.(string)
if !ok || len(f.Names) == 0 {
return original
}
// Deterministic pick using the HMAC of seed||s.
mac := hmac.New(sha256.New, []byte(seed))
mac.Write([]byte(s))
h := mac.Sum(nil)
idx := int(h[0]) % len(f.Names)
return f.Names[idx]
}
sanitizer := httptape.NewPipeline(
httptape.FakeFieldsWith("my-seed", map[string]httptape.Faker{
"$.employer": CompanyFaker{Names: []string{"Acme", "Globex", "Initech"}},
}),
)
Make sure your implementation is deterministic (same seed + original always produces the same output) and does not mutate original -- httptape passes the value pulled out of json.Unmarshal directly.
Combining redaction and faking¶
Order matters. Typically, redact first (remove things that should be gone entirely), then fake (replace things that need consistent stand-in values):
sanitizer := httptape.NewPipeline(
// Step 1: Remove sensitive headers entirely
httptape.RedactHeaders(),
// Step 2: Redact body fields that should be blank
httptape.RedactBodyPaths("$.password", "$.credit_card.number"),
// Step 3: Replace PII with deterministic fakes
httptape.FakeFields("my-seed",
"$.user.email",
"$.user.phone",
"$.user.id",
),
)
CLI and Docker¶
Redaction is available in all httptape modes (record, proxy) via a JSON config file:
# Record with redaction
httptape record --upstream https://api.example.com --fixtures ./mocks --config redact.json
# Proxy with redaction (applied to L2/disk cache only)
httptape proxy --upstream https://api.example.com --fixtures ./cache --config redact.json
See CLI and Docker for full usage.
Declarative configuration¶
Instead of building pipelines in code, you can define redaction rules in a JSON config file. See Config for details.
{
"version": "1",
"rules": [
{ "action": "redact_headers" },
{ "action": "redact_body", "paths": ["$.password"] },
{ "action": "fake", "seed": "my-seed", "paths": ["$.user.email"] }
]
}
The fake action also accepts a fields map that selects a typed faker per path -- the JSON-config equivalent of FakeFieldsWith. See Config for syntax and the full list of shorthands.
SSE event redaction¶
For SSE (Server-Sent Events) responses, httptape provides two SanitizeFunc constructors that operate on individual event payloads. Each event's Data field is treated as an independent JSON body, so the same path syntax applies as RedactBodyPaths and FakeFields.
These functions are no-ops for non-SSE tapes, so they compose safely in a pipeline that handles both regular and SSE responses.
RedactSSEEventData¶
Redacts fields within each SSE event's JSON data:
Example: an LLM streaming response where each event looks like:
After RedactSSEEventData("$.choices[*].delta.content"):
Each event is redacted independently. Non-JSON event data (e.g., [DONE]) is left unchanged.
FakeSSEEventData¶
Replaces fields within each SSE event's JSON data with deterministic fakes:
This uses the same HMAC-SHA256 faking strategy as FakeFields. The same seed and input always produce the same fake, so cross-event consistency is preserved.
Complete pipeline for LLM streaming¶
A typical pipeline for recording LLM API traffic with streaming redaction:
sanitizer := httptape.NewPipeline(
// Step 1: Redact auth headers.
httptape.RedactHeaders("Authorization", "X-Api-Key"),
// Step 2: Redact sensitive fields in regular (non-SSE) response bodies.
httptape.RedactBodyPaths("$.api_key"),
// Step 3: Redact PII from SSE event payloads.
httptape.RedactSSEEventData("$.choices[*].delta.content"),
// Step 4: Fake user identifiers in SSE events with deterministic values.
httptape.FakeSSEEventData("my-seed", "$.user.email", "$.user.id"),
)
rec := httptape.NewRecorder(store, httptape.WithSanitizer(sanitizer))
The order is: headers first, regular body paths, SSE event redaction, SSE event faking. SSE-specific functions are no-ops for non-SSE tapes, and RedactBodyPaths/FakeFields are no-ops for SSE tapes (since SSE tapes have nil Body), so all functions coexist safely in one pipeline.
Custom sanitize functions¶
You can write your own SanitizeFunc and add it to the pipeline:
func maskIPAddresses() httptape.SanitizeFunc {
return func(t httptape.Tape) httptape.Tape {
// Your custom transformation logic
// Remember: do not mutate the input tape -- copy fields you modify
return t
}
}
sanitizer := httptape.NewPipeline(
httptape.RedactHeaders(),
maskIPAddresses(),
)
See also¶
- Config -- declarative JSON configuration
- Recording -- attaching the redaction pipeline to recorders
- Proxy Mode -- redaction in proxy mode (L2 writes only)
- API Reference -- full type signatures