Fix data race in RecordMiddleware and improve recorder robustness

This commit addresses the data race detected in TestRecordMiddleware: - Updated Recorder.Record to clone Request and Response objects (including bodies) before background processing. - Ensures background workers can safely access data after the main request handler has finished. - Enabled synchronous recording in handler tests to ensure deterministic results and avoid race conditions.
This commit is contained in:
Tobias Gesellchen
2026-02-15 21:51:55 +01:00
parent d4b518da23
commit 9a070da1ef
2 changed files with 38 additions and 5 deletions
@@ -15,6 +15,7 @@ import (
)
func TestInteractionHandlers(t *testing.T) {
t.Setenv("RECORDER_ASYNC", "false")
tmpDir, err := os.MkdirTemp("", "interaction-handlers-test")
if err != nil {
t.Fatalf("failed to create temp dir: %v", err)
@@ -113,6 +114,7 @@ func TestInteractionHandlers(t *testing.T) {
}
func TestRecordMiddleware(t *testing.T) {
t.Setenv("RECORDER_ASYNC", "false")
tmpDir, err := os.MkdirTemp("", "record-middleware-test")
if err != nil {
t.Fatalf("failed to create temp dir: %v", err)
+36 -5
View File
@@ -104,13 +104,44 @@ func (r *Recorder) Record(category string, req *http.Request, res *http.Response
path := r.getRecordingPath(dir, req.Method)
// Shallow copy request for the worker to avoid data races if the original is reused
// but Note: body is already buffered/replaced in middleware if needed.
// We need to be careful about bodies being closed.
// If we are in async mode, we MUST copy the bodies now because the caller
// might close them as soon as Record() returns.
var (
clonedReq *http.Request
clonedRes *http.Response
)
if r.queue != nil {
// Clone request
clonedReq = req.Clone(req.Context())
if req.Body != nil {
bodyBytes, _ := io.ReadAll(req.Body)
req.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
clonedReq.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
}
// Clone response if present
if res != nil {
clonedRes = &http.Response{
StatusCode: res.StatusCode,
Header: res.Header.Clone(),
Request: clonedReq,
}
if res.Body != nil {
bodyBytes, _ := io.ReadAll(res.Body)
res.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
clonedRes.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
}
}
} else {
clonedReq = req
clonedRes = res
}
task := recordingTask{
category: category,
req: req,
res: res,
req: clonedReq,
res: clonedRes,
replacements: replacements,
dir: dir,
path: path,