fix: code quality improvements across server, matcher, and proxy#29
Conversation
Previously captures.Wait() ran before the timeout context was created, so a hanging capture goroutine would block shutdown indefinitely. Now the timeout is split between capture drain and HTTP drain, each with its own deadline. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ation The same io.MultiReader + anonymous struct pattern for rebuilding r.Body appeared in matcher.go and twice in server.go. Extracted to internal/httputil.ReconstitutedBody. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The lazy body reader previously used a single bool for both I/O errors and body-too-large. Now returns a typed error so callers can distinguish the two cases. errBodyTooLarge is a named sentinel for the policy limit. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The verbose-logging and resolveBodyFile code both did linear scans over all coats to find the source file path on every matched request. Now FilePath is threaded through the matcher via NewWithPaths and included in MatchResult, making both lookups O(1). The ambiguity detection is removed since the matcher already picks deterministically by order. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
resolveBodyFile used filepath.EvalSymlinks on the target path, which produces a confusing 'unable to resolve symlinks' error when the file simply doesn't exist. Now checks os.Stat first and returns a clear 'body_file not found' message. The HTTP response to clients remains generic to avoid leaking file paths. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Previously renderTemplate silently returned the raw template source when execution failed. Now accepts a logger and logs a warning with the error, aiding debugging when templates contain valid syntax but fail at execution time. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Previously each proxied request spawned an unbounded goroutine that captured *http.Request fields after the handler returned. Now request data is copied before the goroutine starts, and a semaphore caps concurrent captures at 20 to prevent goroutine/disk I/O storms. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR improves request matching, logging, proxy capture behavior, and shared HTTP body handling across the server/matcher/proxy components, with accompanying tests.
Changes:
- Thread coat source file paths through the matcher (
MatchResult.FilePath+NewWithPaths) to avoid per-request scans and improve logging/body_file resolution behavior. - Extract shared request-body “reconstitute” logic into
internal/httputil.ReconstitutedBodyand use it from server + matcher. - Improve proxy shutdown/capture behavior (timeout split, bounded capture concurrency) and expand tests around logging and body_file/template behavior.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| internal/server/server.go | Uses matcher.NewWithPaths, logs template execution failures, resolves body_file using match file path, and adopts httputil.ReconstitutedBody. |
| internal/server/server_test.go | Updates body_file ambiguity expectations and adds new tests for clearer missing-file logs and template execution warning logging. |
| internal/server/verbose_test.go | Updates verbose logging expectations now that MatchResult.FilePath is always available. |
| internal/proxy/proxy.go | Adds capture semaphore, copies request fields before spawning capture goroutines, and splits shutdown timeout between capture drain and HTTP drain. |
| internal/proxy/proxy_test.go | Adds tests intended to cover bounded capture concurrency and shutdown timeout behavior. |
| internal/matcher/matcher.go | Adds MatchResult.FilePath, NewWithPaths, typed body read errors, and uses httputil.ReconstitutedBody. |
| internal/matcher/matcher_test.go | Adds tests for body I/O error handling and FilePath propagation. |
| internal/httputil/body.go | Introduces ReconstitutedBody helper. |
| internal/httputil/body_test.go | Adds unit tests for ReconstitutedBody. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Rename TestServe_VerboseLogging_AmbiguousCoatOmitsFilePath to TestServe_VerboseLogging_DuplicateCoatsLogsFirstFilePath - Rename TestServe_BodyFile_AmbiguousCoatSources to TestServe_BodyFile_DuplicateCoatsFirstWins - Add panic in NewWithPaths when len(paths) != len(coats) - Add early isSubPath check before os.Stat in resolveBodyFile to prevent leaking file existence for traversal paths Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR improves request matching, server response handling, and proxy capture robustness by reducing duplicated body-reconstitution code, propagating coat source paths through the matcher, and tightening shutdown/capture behavior.
Changes:
- Thread coat source
FilePaththroughmatcher.MatchResultviamatcher.NewWithPaths, removing per-request scans in the server. - Extract shared request-body reconstruction into
internal/httputil.ReconstitutedBodyand use it from server + matcher. - Improve operational behavior: proxy shutdown now bounds time spent waiting on captures; proxy capture uses a semaphore and copies request fields before async capture; server logs template execution failures and clarifies
body_fileerrors.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| internal/server/server.go | Uses NewWithPaths/MatchResult.FilePath, factors body reconstitution, logs template exec failures, refines body_file resolution. |
| internal/server/server_test.go | Updates/extends tests for duplicate coats behavior, clearer body_file errors, and template exec warning logging. |
| internal/server/verbose_test.go | Updates verbose logging expectations for duplicate coats to log the first coat’s file path deterministically. |
| internal/matcher/matcher.go | Adds NewWithPaths, propagates FilePath into MatchResult, and returns typed body-read errors while restoring request bodies. |
| internal/matcher/matcher_test.go | Adds coverage for body I/O errors and MatchResult.FilePath propagation. |
| internal/proxy/proxy.go | Bounds concurrent capture work with a semaphore, copies request fields for async capture, and splits shutdown timeout between capture drain and HTTP drain. |
| internal/proxy/proxy_test.go | Adds tests for bounded-capture behavior under load and shutdown returning within a bounded time. |
| internal/httputil/body.go | Introduces ReconstitutedBody helper for replaying captured bytes + delegating Close to the original body. |
| internal/httputil/body_test.go | Adds unit tests for ReconstitutedBody behavior with captured bytes and empty capture. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
os.Stat follows symlinks, so a symlink inside the coat directory pointing outside could probe external paths before EvalSymlinks runs. os.Lstat checks the entry itself without following, leaving symlink resolution to the subsequent EvalSymlinks + isSubPath check. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
Shutdownnow splits timeout between capture drain and HTTP drain instead of blocking indefinitely oncaptures.Wait()ReconstitutedBodyhelper: Eliminated triple-duplicatedio.MultiReader+ anonymous struct pattern intointernal/httputil.ReconstitutedBodylazyBodyReaderreturns typederrorinstead ofbool, distinguishing I/O errors from body-too-largeFilePathonMatchResult: Eliminated two O(n) linear scans per request by threading file path through the matcher viaNewWithPathsbody_fileerrors:resolveBodyFilechecksos.StatbeforeEvalSymlinksso missing files get "not found" instead of confusing symlink errorsrenderTemplatenow logs a warning when template execution fails instead of silently returning raw template source*http.Requestafter handler returnsTest Plan
-raceflaggolangci-lint run ./...— 0 issuesgo vet ./...— cleangofmt -l .— all formattedTestProxyShutdownRespectsTimeoutWithPendingCapturesTestReconstitutedBody,TestReconstitutedBodyEmptyTestMatchBodyIOErrorTestMatchResultFilePathTestServe_BodyFile_Missing_ClearLogMessageTestServe_TemplateExecutionErrorLoggedTestProxyCapturesConcurrencyBounded🤖 Generated with Claude Code