internal/httprr/README.md
The httprr package provides deterministic HTTP record and replay functionality for testing. It allows tests to record real HTTP interactions during development and replay them during CI/testing, ensuring consistent and fast test execution.
func TestMyAPI(t *testing.T) {
// Skip test gracefully if no credentials and no recording exists
httprr.SkipIfNoCredentialsOrRecording(t, "API_KEY")
// Create recorder/replayer
rr, err := httprr.OpenForTest(t, http.DefaultTransport)
if err != nil {
t.Fatal(err)
}
defer rr.Close()
// Use rr.Client() for all HTTP calls
client := rr.Client()
resp, err := client.Get("https://api.example.com/data")
// ... test continues
}
-httprecord=.): Makes real HTTP requests and saves them to .httprr files.httprr files and replays the responses-httprecord=<regexp>: Re-record traces for files matching the regexp pattern (use "." to match all)-httprecord-delay=<ms>: Add delay in milliseconds between HTTP requests during recording (helps avoid rate limits).httprr files for easier debugging.httprr and .httprr.gz filesOpenForTest(t *testing.T, rt http.RoundTripper) (*RecordReplay, error)The primary API for most test cases. Creates a recorder/replayer for the given test.
testdata/TestName.httprrt.Name()testdata/ subdirectorySkipIfNoCredentialsOrRecording(t *testing.T, envVars ...string)Gracefully skips tests when they cannot run (no API keys) and have no recorded data.
// Skip if OPENAI_API_KEY not set AND no recording exists
httprr.SkipIfNoCredentialsOrRecording(t, "OPENAI_API_KEY")
// Skip if neither API_KEY nor BACKUP_KEY is set AND no recording exists
httprr.SkipIfNoCredentialsOrRecording(t, "API_KEY", "BACKUP_KEY")
Open(file string, rt http.RoundTripper) (*RecordReplay, error)Low-level API for custom file management. Most tests should use OpenForTest instead.
Client() *http.ClientReturns an HTTP client that routes through the recorder/replayer.
ScrubReq(scrubs ...func(*http.Request) error)Adds request scrubbing functions to sanitize sensitive data before recording.
rr.ScrubReq(func(req *http.Request) error {
req.Header.Set("Authorization", "Bearer test-api-key")
return nil
})
ScrubResp(scrubs ...func(*bytes.Buffer) error)Adds response scrubbing functions to sanitize sensitive data in responses.
Recording() boolReports whether the recorder is in recording mode.
Close() errorCloses the recorder/replayer. Use with defer for automatic cleanup.
func TestOpenAIChat(t *testing.T) {
httprr.SkipIfNoCredentialsOrRecording(t, "OPENAI_API_KEY")
rr, err := httprr.OpenForTest(t, http.DefaultTransport)
if err != nil {
t.Fatal(err)
}
defer rr.Close()
// Scrub sensitive data
rr.ScrubReq(func(req *http.Request) error {
req.Header.Set("Authorization", "Bearer test-api-key")
return nil
})
// Create client with recording support
llm, err := openai.New(openai.WithHTTPClient(rr.Client()))
require.NoError(t, err)
// Test continues with recorded/replayed HTTP calls
response, err := llm.GenerateContent(ctx, messages)
require.NoError(t, err)
}
func createTestClient(t *testing.T) *MyAPIClient {
t.Helper()
httprr.SkipIfNoCredentialsOrRecording(t, "MY_API_KEY")
rr, err := httprr.OpenForTest(t, http.DefaultTransport)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { rr.Close() })
return NewMyAPIClient(WithHTTPClient(rr.Client()))
}
func TestFeatureA(t *testing.T) {
client := createTestClient(t)
// ... test continues
}
func TestFeatureB(t *testing.T) {
client := createTestClient(t)
// ... test continues
}
func TestMultiAPIIntegration(t *testing.T) {
httprr.SkipIfNoCredentialsOrRecording(t, "OPENAI_API_KEY", "SERPAPI_KEY")
rr, err := httprr.OpenForTest(t, http.DefaultTransport)
if err != nil {
t.Fatal(err)
}
defer rr.Close()
// Both clients will use the same recording
openaiClient := openai.New(openai.WithHTTPClient(rr.Client()))
searchClient := serpapi.New(serpapi.WithHTTPClient(rr.Client()))
// All HTTP calls are recorded/replayed together
}
# Record all tests
go test ./... -httprecord=.
# Record specific test
go test ./pkg -httprecord=. -run TestSpecificFunction
# Record with pattern matching
go test ./... -httprecord="TestOpenAI.*"
# Normal test run (uses recorded data)
go test ./...
# Skip tests that need credentials
OPENAI_API_KEY="" go test ./... # Tests will skip gracefully
testdata/
├── TestBasicFunction.httprr # Uncompressed recording
├── TestWithSubtest-subcase.httprr # Subtest recording
├── TestOldFunction.httprr.gz # Compressed recording
└── TestComplexAPI-setup.httprr # Multi-part test
TestMyFunction → File: TestMyFunction.httprrTestMyFunction/subcase → File: TestMyFunction-subcase.httprr# Compress all recordings (for repository storage)
go run ./internal/devtools/rrtool pack -r
# Check compression status
go run ./internal/devtools/rrtool check
# Decompress for debugging
go run ./internal/devtools/rrtool unpack -r
When recording tests that make many API calls, use the delay flag to avoid hitting rate limits:
# Record with 1 second delay between requests
go test -httprecord=. -httprecord-delay=1000 ./...
# Record specific test with 500ms delay
go test -httprecord=. -httprecord-delay=500 -run TestMyAPI ./mypackage
// ✅ Good: Test skips gracefully when it can't run
httprr.SkipIfNoCredentialsOrRecording(t, "API_KEY")
// ❌ Bad: Test fails when API key missing
rr, err := httprr.OpenForTest(t, http.DefaultTransport)
// ✅ Good: Replace real API keys with test values
rr.ScrubReq(func(req *http.Request) error {
req.Header.Set("Authorization", "Bearer test-api-key")
return nil
})
// ❌ Bad: Real API keys recorded in files
// (No scrubbing - keys end up in repository)
// ✅ Good: Reusable test setup
func createTestLLM(t *testing.T) *openai.LLM {
t.Helper()
httprr.SkipIfNoCredentialsOrRecording(t, "OPENAI_API_KEY")
// ... setup code
}
// ❌ Bad: Duplicate setup in every test
func TestA(t *testing.T) {
httprr.SkipIfNoCredentialsOrRecording(t, "OPENAI_API_KEY")
rr, err := httprr.OpenForTest(t, http.DefaultTransport)
// ... repeated setup
}
// ✅ Good: Automatic cleanup
defer rr.Close()
// or
t.Cleanup(func() { rr.Close() })
// ❌ Bad: Manual cleanup (can be forgotten)
// (No defer or cleanup)
Problem: Test is trying to make an HTTP request not in the recording.
Solutions:
# Re-record the test
go test ./pkg -httprecord=. -run TestName
# Check if you have required environment variables
export OPENAI_API_KEY="your-key-here"
go test ./pkg -httprecord=. -run TestName
Problem: .httprr.gz file is corrupted or not actually compressed.
Solutions:
# Check and fix compression
go run ./internal/devtools/rrtool check
go run ./internal/devtools/rrtool pack -r
# Or remove the corrupted file and re-record
rm testdata/TestName.httprr.gz
go test ./pkg -httprecord=. -run TestName
Problem: Test is skipping when you expect it to run.
Debug steps:
# Check if environment variables are set
echo $OPENAI_API_KEY
# Check if recording exists
ls testdata/TestName.httprr*
# Run with verbose output
go test ./pkg -run TestName -v
The system automatically handles conflicts, but you can resolve manually:
# Check which file is newer
ls -la testdata/TestName.httprr*
# Remove older file (system will warn and use newer)
rm testdata/TestName.httprr.gz # if .httprr is newer
# Or compress the newer one
gzip testdata/TestName.httprr
OpenForTestWithSkip (Old API)// ❌ Old API (removed)
rr := httprr.OpenForTestWithSkip(t, http.DefaultTransport, "API_KEY")
defer rr.Close()
// ✅ New API
httprr.SkipIfNoCredentialsOrRecording(t, "API_KEY")
rr, err := httprr.OpenForTest(t, http.DefaultTransport)
if err != nil {
t.Fatal(err)
}
defer rr.Close()
httprr operations return errors// For custom file management (rarely needed)
rr, err := httprr.Open("custom/path/recording.httprr", http.DefaultTransport)
if err != nil {
t.Fatal(err)
}
defer rr.Close()
func TestWithConditionalRecording(t *testing.T) {
// Only record if we have credentials
if os.Getenv("API_KEY") != "" {
// Will record new interactions
rr, err := httprr.OpenForTest(t, http.DefaultTransport)
// ...
} else {
// Will only replay existing recordings
httprr.SkipIfNoCredentialsOrRecording(t, "API_KEY")
rr, err := httprr.OpenForTest(t, http.DefaultTransport)
// ...
}
}
rr.ScrubReq(func(req *http.Request) error {
// Remove API keys
req.Header.Set("Authorization", "Bearer test-key")
// Scrub request body
if req.Body != nil {
body := req.Body.(*httprr.Body)
bodyStr := string(body.Data)
bodyStr = strings.ReplaceAll(bodyStr, "real-secret", "test-secret")
body.Data = []byte(bodyStr)
}
return nil
})
rr.ScrubResp(func(buf *bytes.Buffer) error {
// Remove sensitive data from responses
content := buf.String()
content = strings.ReplaceAll(content, "sensitive-data", "redacted")
buf.Reset()
buf.WriteString(content)
return nil
})
When adding new tests that use external APIs:
SkipIfNoCredentialsOrRecording for graceful degradationFor questions or issues with the httprr system, see the main project documentation or open an issue.