feat(guardrails): route rewrite calls through workflows + remove synthetic internal guardrail transport#210
Conversation
📝 WalkthroughWalkthroughAdds a new Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant Guardrails as GuardrailService
participant Executor as InternalChatExecutor
participant LLM
participant Provider
Client->>Guardrails: Submit request (instructions + input/messages)
Guardrails->>Guardrails: Select messages by role, skip by prefix
Guardrails->>Executor: ChatCompletion requests for texts to rewrite
Note right of Executor: context set to rgba(255,165,0,0.5) "guardrail" and effective user_path
Executor->>LLM: POST /v1/chat/completions (system + <TEXT_TO_ALTER>)
LLM-->>Executor: Rewritten content
Executor-->>Guardrails: Rewritten texts
Guardrails->>Guardrails: Apply rewrites to instructions/input/messages
Guardrails-->>Provider: Forward rewritten envelope
Provider-->>Client: Final provider response
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Pull request overview
This PR refactors guardrail execution to route internal guardrail-driven rewrites through the existing translated workflow/planning path (without a synthetic internal HTTP transport), and introduces a new llm_based_altering guardrail type that performs auxiliary LLM-based message rewriting.
Changes:
- Added a transport-free internal translated chat executor (
InternalChatCompletionExecutor) that reuses execution planning, audit, usage, and response cache behavior. - Implemented
llm_based_alteringguardrails (config, registry/definitions, UI/admin plumbing) and expanded Responses request rewriting to preserve input-item shape. - Added request-origin context + execution-policy overrides to prevent guardrail recursion (disable guardrails + clear guardrails hash for internal guardrail calls).
Reviewed changes
Copilot reviewed 33 out of 33 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| README.md | Updates guardrails capability summary wording/types. |
| internal/server/translated_inference_service.go | Centralizes cache-related context wiring (plan + guardrails hash). |
| internal/server/native_batch_service.go | Passes request context into execution-policy application. |
| internal/server/model_validation.go | Uses shared translated-plan builder and passes ctx into policy application. |
| internal/server/internal_chat_completion_executor.go | Adds transport-free internal translated chat execution path (with cache/audit/usage). |
| internal/server/internal_chat_completion_executor_test.go | Tests internal executor planning, audit capture, and cache routing. |
| internal/server/execution_policy.go | Applies request-origin-based policy overrides (disable guardrails for guardrail-origin calls). |
| internal/server/execution_policy_test.go | Updates tests for new policy application signature. |
| internal/server/execution_plan_helpers.go | Extracts reusable translatedExecutionPlan(...) helper. |
| internal/responsecache/responsecache.go | Adds HandleInternalRequest to reuse cache middleware for internal execution. |
| internal/guardrails/system_prompt.go | Minor slice construction simplification. |
| internal/guardrails/service.go | Adds executor injection + snapshot rebuild for LLM-based guardrails. |
| internal/guardrails/service_test.go | Adds coverage for LLM-based altering pipelines and executor swapping. |
| internal/guardrails/provider.go | Extends Responses input rewriting/preservation for guardrails (esp. structured inputs). |
| internal/guardrails/provider_test.go | Adds coverage for LLM-based altering behavior and Responses input rewriting. |
| internal/guardrails/llm_based_altering.go | Implements the LLM-based altering guardrail. |
| internal/guardrails/llm_based_altering_test.go | Adds unit tests for config normalization, rewrite behavior, and concurrency limits. |
| internal/guardrails/factory.go | Threads auxiliary executor into guardrails factory/service construction. |
| internal/guardrails/executor.go | Updates Responses path to handle new message extraction/apply error returns. |
| internal/guardrails/definitions.go | Adds LLM-based guardrail type config/normalization, descriptors, and “unavailable” guardrail behavior. |
| internal/core/context.go | Introduces request-origin context propagation. |
| internal/auditlog/middleware.go | Refactors body capture helpers and adds entry-level enrich helpers. |
| internal/auditlog/entry_capture.go | Adds internal JSON exchange capture for transport-free execution. |
| internal/app/app.go | Wires internal guardrail executor + extends guardrail config type handling and user_path plumbing. |
| internal/admin/handler_guardrails_test.go | Expands admin tests for new guardrail type defaults/normalization/validation. |
| internal/admin/dashboard/templates/index.html | Updates guardrail form UI to support checkbox fields + type select sync. |
| internal/admin/dashboard/static/js/modules/timezone-layout.test.js | Updates template assertions for new guardrail UI wiring. |
| internal/admin/dashboard/static/js/modules/guardrails.test.js | Adds tests for checkbox fields + type select syncing + defaults behavior. |
| internal/admin/dashboard/static/js/modules/guardrails.js | Adds array/checkbox handling and robust guardrail-type resolution/sync. |
| go.mod | Promotes golang.org/x/sync to a direct dependency. |
| docs/advanced/guardrails.mdx | Documents llm_based_altering, user_path, and internal execution behavior. |
| config/config.go | Adds YAML config schema for llm_based_altering + user_path. |
| config/config.example.yaml | Adds example YAML for llm_based_altering guardrails. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if err := decoder.Decode(&cfg); err != nil { | ||
| return llmBasedAlteringDefinitionConfig{}, newValidationError("invalid llm_based_altering config: "+err.Error(), err) | ||
| } | ||
| if decoder.More() { | ||
| return llmBasedAlteringDefinitionConfig{}, newValidationError("invalid llm_based_altering config: trailing data", nil) |
There was a problem hiding this comment.
decodeLLMBasedAlteringDefinitionConfig attempts to detect trailing JSON by calling decoder.More(), but More() only reports whether the current array/object has more elements; it won’t catch extra tokens after the top-level object (e.g. {...} garbage). Use a second decoder.Decode(&struct{}{}) and require io.EOF (or equivalent) to reliably reject trailing data.
| ruleType := strings.ToLower(strings.TrimSpace(rule.Type)) | ||
| switch ruleType { | ||
| case "llm-based-altering": | ||
| ruleType = "llm_based_altering" |
There was a problem hiding this comment.
configGuardrailDefinitions normalizes the hyphenated alias for llm_based_altering, but not for system_prompt (even though guardrails.normalizeDefinitionType supports system-prompt). If a user sets type: system-prompt in YAML, it will currently fail as “unsupported type”. Consider normalizing system-prompt to system_prompt here as well for consistent config parsing.
| ruleType = "llm_based_altering" | |
| ruleType = "llm_based_altering" | |
| case "system-prompt": | |
| ruleType = "system_prompt" |
There was a problem hiding this comment.
Actionable comments posted: 12
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
internal/guardrails/factory.go (1)
60-90: 🛠️ Refactor suggestion | 🟠 MajorReject extra executors instead of silently dropping them.
The new variadic constructors read like multi-executor support, but
internal/guardrails/service.goonly preservesexecutors[0]. Passing two executors now succeeds and quietly ignores the second one. Either fail fast onlen(executors) > 1or make the API singular so the contract matches the implementation.♻️ Proposed fix
func newResult(ctx context.Context, storeConn storage.Storage, refreshInterval time.Duration, executors ...ChatCompletionExecutor) (*Result, error) { + if len(executors) > 1 { + return nil, fmt.Errorf("at most one ChatCompletionExecutor may be provided") + } store, err := createStore(ctx, storeConn) if err != nil { return nil, err } service, err := NewService(store, executors...)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/guardrails/factory.go` around lines 60 - 90, The constructors accept variadic ChatCompletionExecutor but the implementation (NewService) only uses executors[0], so callers can silently pass extra executors; add a fast-fail check to reject multiple executors. In newResult (and optionally in New and NewWithSharedStorage) check if len(executors) > 1 and return a clear error like "only one ChatCompletionExecutor is supported" before any work is done; this enforces the single-executor contract and avoids silently dropping extra executors (refer to the functions New, NewWithSharedStorage, newResult and the ChatCompletionExecutor parameter and NewService usage).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@internal/admin/dashboard/templates/index.html`:
- Around line 384-388: The User Path input (x-model="guardrailForm.user_path")
only affects auxiliary rewrite guardrails, so either conditionally render it
only when guardrailForm.type === 'llm_based_altering' or update the hint to
state it only applies to rewrite-type guardrails; locate the input block around
the label with class "alias-form-field" and either wrap it in a conditional
(e.g., x-show/x-if using guardrailForm.type === 'llm_based_altering') or change
the small.alias-form-hint text to explicitly say "Only used for auxiliary
rewrite (llm_based_altering) guardrails; ignored for other guardrail types."
Ensure the visible behavior and aria-label remain unchanged.
- Around line 411-423: The checkbox collection currently rendered when
field.input === 'checkboxes' lacks a semantic group; replace the outer div with
a fieldset and render field.label inside a legend (keeping the same visual
styling) so screen readers announce a shared group label, and keep the template
x-for loop using the same input handling functions guardrailArrayFieldSelected
and toggleGuardrailArrayFieldValue; ensure the hint (field.help) remains
associated (e.g., aria-describedby on inputs or the fieldset) and verify the
rendered editor with accessibility tooling after the change.
In `@internal/admin/handler_guardrails_test.go`:
- Around line 296-316: Update the TestUpsertGuardrailRejectsSlashInName test to
assert the full OpenAI-compatible error envelope instead of only the HTTP 400
code: after calling h.UpsertGuardrail(c) and checking rec.Code, parse/inspect
rec.Body and reuse the same envelope helper used in the delete tests (the helper
used there to validate the `error` object shape) to assert the response contains
an `error` object with `type`, `message`, `param`, and `code` fields and
appropriate values for the invalid name case; this ensures the handler method
UpsertGuardrail returns the required contract.
In `@internal/auditlog/entry_capture.go`:
- Around line 203-218: In boundedAuditBody, the nested nil check inside the
len(body) <= MaxBodyCapture branch is redundant and unclear; explicitly handle a
nil body first (e.g., if body == nil { return []byte{}, false }) then proceed to
the length-based logic so callers get a consistent empty slice vs nil result,
and remove the unreachable inner nil check in boundedAuditBody to simplify the
control flow.
In `@internal/guardrails/llm_based_altering_test.go`:
- Around line 198-217: Add a second test that cancels the context during
processing to exercise mid-flight cancellation: create a cancellable ctx, pass a
mockChatCompletionExecutor into NewLLMBasedAlteringGuardrail whose chatFn blocks
on a channel or sync point (e.g., make chatFn wait on a started channel), then
start Process in a goroutine and trigger cancel() after the chatFn has begun
(unblock/notify started channel or sleep briefly then call cancel), and assert
Process returns a context cancellation error; reference
TestLLMBasedAltering_Process_PropagatesContextCancellation,
NewLLMBasedAlteringGuardrail, g.Process, mockChatCompletionExecutor, and chatFn
to locate where to implement the blocking/unblocking and cancellation logic.
In `@internal/guardrails/llm_based_altering.go`:
- Around line 22-228: The large hard-coded prompt DefaultLLMBasedAlteringPrompt
should be externalized; create an external template file (e.g.,
llm_based_altering_prompt.txt) and replace the long const with a var (e.g.,
DefaultLLMBasedAlteringPrompt string) that is populated at init by reading the
file (use go:embed or a config loader) with a fallback to the original string if
the file is missing; update any code that references
DefaultLLMBasedAlteringPrompt to use the var so deployments can override the
prompt without recompiling.
- Around line 438-440: The loop over texts currently spawns detached goroutines
via group.Go() and uses i := i; text := text to avoid captures; instead, process
each text synchronously within the current context (respecting ctx) and remove
the inner reassignments; also change the function's return type from error to
core.GatewayError and convert any returned errors into that typed value before
returning; locate usages of group.Go, the loop over texts, and the function
signature in llm_based_altering.go to implement these changes.
In `@internal/guardrails/provider.go`:
- Around line 1423-1432: The cloneStringAnyMap function currently performs a
shallow copy and will share nested references (maps, slices, etc.) between src
and the clone; update the code by adding a clear comment above cloneStringAnyMap
describing this shallow-copy semantics and that nested structures are not
deep-copied (and mark intended usage assumptions: values are primitives or fully
replaced), and if any caller (e.g., response input patching code) might mutate
nested structures instead of replacing them, replace cloneStringAnyMap with a
deep-cloning implementation or add a helper deepCloneStringAnyMap to avoid
accidental shared mutations.
In `@internal/responsecache/responsecache.go`:
- Line 177: The code creates a new Echo instance for every internal request (c
:= echo.New().NewContext(req, rec)), causing allocations; modify the middleware
to accept or cache a shared Echo instance in the middleware struct (e.g., set a
field in NewResponseCacheMiddleware) and use that instance to create contexts in
HandleInternalRequest (replace echo.New() with the cached/shared echo instance),
or add an optional constructor parameter to pass an existing *echo.Echo so the
context creation reuses the same router/middleware chain.
- Around line 168-170: The code currently silently replaces a nil ctx with
context.Background(), which masks caller bugs; instead remove the fallback and
fail fast: in the function(s) where you see the snippet that sets "if ctx == nil
{ ctx = context.Background() }" (any method in the responsecache package that
accepts a ctx parameter), change it to validate ctx != nil and return an error
(or propagate an error type) immediately when ctx is nil; update the function
signature to return an error if it doesn't already (or document panics if you
prefer), and update all callers to pass a non-nil context or handle the returned
error so request-scoped values are preserved.
In `@internal/server/translated_inference_service.go`:
- Around line 183-199: The current withCacheRequestContext uses s.guardrailsHash
as a fallback and only writes a guardrails hash into the context when
guardrailsHash != "", which lets an incoming context keep a stale caller hash
when plan.Policy is present but plan.GuardrailsHash() returns "". Update
withCacheRequestContext so that when a plan exists and plan.Policy != nil you
always call core.WithGuardrailsHash(ctx, plan.GuardrailsHash()) to explicitly
overwrite/clear any inherited hash (even if empty); otherwise, if no plan
policy, fall back to applying s.guardrailsHash only when non-empty (using
core.WithGuardrailsHash). This ensures guardrail-origin requests can clear the
caller’s cache namespace.
- Around line 164-167: Remove the redundant conditional around setting the
request context: always call c.SetRequest(c.Request().WithContext(ctx)) after
obtaining ctx from withCacheRequestContext(c.Request().Context(), plan). The
withCacheRequestContext function always returns a wrapped context (it uses
core.WithExecutionPlan and may add core.WithGuardrailsHash), so drop the if ctx
!= c.Request().Context() check and unconditionally update the request context
via c.SetRequest; this matches the pattern used elsewhere in the file.
---
Outside diff comments:
In `@internal/guardrails/factory.go`:
- Around line 60-90: The constructors accept variadic ChatCompletionExecutor but
the implementation (NewService) only uses executors[0], so callers can silently
pass extra executors; add a fast-fail check to reject multiple executors. In
newResult (and optionally in New and NewWithSharedStorage) check if
len(executors) > 1 and return a clear error like "only one
ChatCompletionExecutor is supported" before any work is done; this enforces the
single-executor contract and avoids silently dropping extra executors (refer to
the functions New, NewWithSharedStorage, newResult and the
ChatCompletionExecutor parameter and NewService usage).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: f3408508-3b14-4fd9-9408-61ade1a71bce
📒 Files selected for processing (33)
README.mdconfig/config.example.yamlconfig/config.godocs/advanced/guardrails.mdxgo.modinternal/admin/dashboard/static/js/modules/guardrails.jsinternal/admin/dashboard/static/js/modules/guardrails.test.jsinternal/admin/dashboard/static/js/modules/timezone-layout.test.jsinternal/admin/dashboard/templates/index.htmlinternal/admin/handler_guardrails_test.gointernal/app/app.gointernal/auditlog/entry_capture.gointernal/auditlog/middleware.gointernal/core/context.gointernal/guardrails/definitions.gointernal/guardrails/executor.gointernal/guardrails/factory.gointernal/guardrails/llm_based_altering.gointernal/guardrails/llm_based_altering_test.gointernal/guardrails/provider.gointernal/guardrails/provider_test.gointernal/guardrails/service.gointernal/guardrails/service_test.gointernal/guardrails/system_prompt.gointernal/responsecache/responsecache.gointernal/server/execution_plan_helpers.gointernal/server/execution_policy.gointernal/server/execution_policy_test.gointernal/server/internal_chat_completion_executor.gointernal/server/internal_chat_completion_executor_test.gointernal/server/model_validation.gointernal/server/native_batch_service.gointernal/server/translated_inference_service.go
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@internal/auditlog/entry_capture.go`:
- Around line 96-101: CaptureInternalJSONExchange currently calls
internalJSONAuditRequest and internalJSONAuditResponse unconditionally which
causes headers-only auditing to still attempt body marshaling and lose synthetic
header metadata when bodies are nil/unserializable; update
internalJSONAuditRequest and internalJSONAuditResponse to always construct and
return header/request metadata (including synthetic trace/user-path/X-Request-ID
via requestIDForEntry) and add a logBodies boolean parameter (pass cfg.LogBodies
from CaptureInternalJSONExchange) so the helpers only perform JSON marshaling of
bodies when logBodies is true, then call PopulateRequestData and
PopulateResponseData with the metadata and marshaled body only when available;
apply the same change to the other call sites referenced (the other
CaptureInternalJSONExchange usages).
In `@internal/guardrails/llm_based_altering_test.go`:
- Around line 154-174: The test TestLLMBasedAltering_Process_SkipsPrefix can
return a false positive because Process is fail-open on executor errors; update
the mockChatCompletionExecutor chatFn used in NewLLMBasedAlteringGuardrail to
explicitly fail the test if invoked (e.g., call t.Fatalf or set a called flag
and assert it is false after Process), so you verify the executor is never
called when SkipContentPrefix matches; reference
mockChatCompletionExecutor.chatFn, NewLLMBasedAlteringGuardrail and Process to
locate where to inject the fail-fast assertion.
In `@internal/guardrails/provider.go`:
- Around line 923-932: The function_call_output branch currently flattens
item.Output to text via stringifyResponsesValue and then writes that text back
into the message, losing original types; change the assignment so we preserve
typed values: call stringifyResponsesValue(item.Output) as before, then try to
json.Unmarshal the resulting content string into a temporary interface{}; if
unmarshalling succeeds use that decoded value for preserved.Output (or
Message.Content/Output field as appropriate), otherwise fall back to the
original item.Output (so arrays/objects/numbers/null remain typed) — update the
logic around stringifyResponsesValue, item.Output, preserved.Output (and the
case "function_call_output" handling) to implement this.
In `@internal/responsecache/responsecache.go`:
- Around line 242-252: The mapping in internalCacheType is fragile because it
checks hardcoded strings ("HIT (exact)" / "HIT (semantic)") that are composed
elsewhere in writeCachedResponse; define canonical header-value constants (e.g.,
CacheHeaderExact, CacheHeaderSemantic) next to CacheTypeExact and
CacheTypeSemantic in semantic.go and update both writeCachedResponse to use
those constants when writing the header and internalCacheType to compare against
them (instead of the current literal strings) so there is a single source of
truth for the header formats.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: dce8d1a8-9ed0-4722-95bd-2df02a60d7a3
📒 Files selected for processing (14)
internal/admin/dashboard/static/css/dashboard.cssinternal/admin/dashboard/static/js/modules/timezone-layout.test.jsinternal/admin/dashboard/templates/index.htmlinternal/admin/handler_guardrails_test.gointernal/auditlog/entry_capture.gointernal/guardrails/factory.gointernal/guardrails/llm_based_altering_test.gointernal/guardrails/provider.gointernal/guardrails/service.gointernal/guardrails/service_test.gointernal/responsecache/handle_request_test.gointernal/responsecache/responsecache.gointernal/server/translated_inference_service.gointernal/server/translated_inference_service_test.go
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@internal/auditlog/entry_capture.go`:
- Around line 117-145: internalJSONAuditRequest currently only attaches a
RequestSnapshot to the context when logBodies is true and json.Marshal succeeds,
which can leave an ingress snapshot in place and leak outer request data; change
internalJSONAuditRequest to always create and attach a synthetic
core.RequestSnapshot whenever logBodies is true (use core.NewRequestSnapshot and
core.WithRequestSnapshot), setting capturedBody to the marshaled bytes on
success and to an empty or nil captured body with bodyTooBig/body-not-captured
flags set appropriately on nil body or marshal failure; update the function to
compute capturedBody/bodyTooBig deterministically and attach the snapshot to
reqCtx regardless of marshal outcome, and add a regression test that pre-seeds
an ingress snapshot and exercises an unmarshalable internal body to assert the
internal snapshot is used.
In `@internal/guardrails/llm_based_altering_test.go`:
- Around line 300-347: Replace raw int32 counters with sync/atomic helper types
inside TestLLMBasedAltering_Process_LimitsConcurrentRewrites: change inFlight,
maxInFlight, and requestsSeen to atomic.Int32, update increments/decrements to
inFlight.Add(1)/inFlight.Add(-1) and requestsSeen.Add(1), replace
atomic.LoadInt32(&maxInFlight)/atomic.CompareAndSwapInt32(&maxInFlight,
previous, current) in the mockChatCompletionExecutor.chatFn CAS loop with
maxInFlight.Load() and maxInFlight.CompareAndSwap(previous, current) (or use
maxInFlight.Swap/Store as appropriate), and update the final checks to use
requestsSeen.Load() and maxInFlight.Load(); keep all logic identical but use the
atomic.Int32 methods for cleaner code.
In `@internal/guardrails/provider.go`:
- Around line 873-900: In coerceResponsesInputElements, remove the intermediate
[]any allocation in the case []map[string]any branch and instead directly
convert each map to a core.ResponsesInputElement: allocate elements :=
make([]core.ResponsesInputElement, len(typed)) and for each map in the input
marshal the map to JSON and unmarshal into elements[i], returning elements, nil;
update the case []map[string]any in function coerceResponsesInputElements
accordingly and preserve error handling that returns core.NewInvalidRequestError
on marshal/unmarshal failures.
In `@internal/responsecache/responsecache.go`:
- Around line 179-185: The current fallback that silently calls echo.New() when
m.echo is nil should be removed because it masks constructor bugs; instead,
detect when m != nil && m.echo == nil and log a warning or error (using the
package logger e.g., response cache's logger or the standard log) indicating the
unexpected nil m.echo (include context like the ResponseCache instance ID or
constructor used), and then fail-fast by returning an error or nil to the caller
rather than creating a new Echo instance; update the calling code or the
constructor (e.g., NewResponseCache / any New* functions that initialize m.echo)
to ensure m.echo is always initialized so this branch never executes.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 21eacc1e-59ef-4ee7-92e4-a9f6980498e6
📒 Files selected for processing (8)
internal/auditlog/entry_capture.gointernal/auditlog/entry_capture_test.gointernal/guardrails/llm_based_altering_test.gointernal/guardrails/provider.gointernal/guardrails/provider_test.gointernal/responsecache/responsecache.gointernal/responsecache/semantic.gointernal/responsecache/stream_cache.go
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@internal/auditlog/entry_capture_test.go`:
- Around line 57-84: The test only covers the marshal-failure/no-body path for
CaptureInternalJSONExchange; add two more subtests in
TestCaptureInternalJSONExchange_PreservesHeadersWhenBodyMarshalFails (or new
tests) that exercise the responseErr branch and the oversized/truncation branch:
1) call CaptureInternalJSONExchange with a non-nil responseErr value and assert
that entry.Data.ResponseError (or the field your helper sets for synthesized
errors) is populated and headers are still preserved; 2) simulate an oversized
payload/truncation by using the helper path that marks payloads as truncated (or
set the config/inputs that trigger the oversized marker) and assert that
RequestBody/ResponseBody are replaced with the truncation marker while headers
remain intact. Reference CaptureInternalJSONExchange, Config, and the
responseErr/synthesized-error/truncation logic when adding assertions so the new
tests cover both branches.
In `@internal/guardrails/provider.go`:
- Around line 1017-1019: Remove the no-op assignment "_ = typed" in the switch's
default branch and instead ignore the value at the switch declaration or case
pattern so the compiler doesn't require silencing; specifically locate the
switch that declares the variable "typed" and change the case that falls to
default to use the blank identifier (or bind to _ in the case pattern) rather
than assigning "_ = typed" in the default branch, then delete the "_ = typed"
line.
In `@internal/responsecache/responsecache.go`:
- Around line 180-188: The nil-receiver branch currently creates a new Echo
instance which hides a call-site bug; change HandleInternalRequest to fail fast
when m == nil by logging an error and returning a provider/internal server error
instead of allocating echo.New(); keep the existing behavior for the case where
m != nil but m.echo == nil (log and return provider error as done now). Update
the switch handling in HandleInternalRequest (the method on the response cache
type) to detect m == nil first and return an explicit error with a clear log
message rather than creating a new Echo.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 032aeb5e-921c-4975-b0d6-db642248c8de
📒 Files selected for processing (9)
config/config.example.yamlconfig/config.gointernal/admin/dashboard/static/css/dashboard.cssinternal/auditlog/entry_capture.gointernal/auditlog/entry_capture_test.gointernal/guardrails/llm_based_altering_test.gointernal/guardrails/provider.gointernal/responsecache/handle_request_test.gointernal/responsecache/responsecache.go
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 40 out of 40 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if m == nil { | ||
| err = next(c) | ||
| } else { | ||
| err = m.HandleRequest(c, body, func() error { return next(c) }) | ||
| } |
There was a problem hiding this comment.
HandleInternalRequest already returns an error when the receiver is nil (switch case m == nil), so the later if m == nil { ... } else { ... } branch is unreachable on the m == nil path. This makes the control flow harder to reason about and can be simplified by removing the dead branch and directly running m.HandleRequest after the nil/uninitialized checks (or by making HandleInternalRequest a standalone function if nil receivers should be supported).
| if m == nil { | |
| err = next(c) | |
| } else { | |
| err = m.HandleRequest(c, body, func() error { return next(c) }) | |
| } | |
| err = m.HandleRequest(c, body, func() error { return next(c) }) |
| if strings.Contains(text, alteringTextWrapperStart) { | ||
| text = strings.Replace(text, alteringTextWrapperStart, "", 1) | ||
| text = strings.TrimPrefix(text, "\n") | ||
| } | ||
| if strings.Contains(text, alteringTextWrapperEnd) { | ||
| text = strings.Replace(text, alteringTextWrapperEnd, "", 1) | ||
| text = strings.TrimSuffix(text, "\n") | ||
| } |
There was a problem hiding this comment.
unwrapAlteredText removes wrapper tokens using strings.Contains + strings.Replace, which will also strip the tokens if they appear in the actual rewritten content (e.g., user text legitimately contains <TEXT_TO_ALTER>). To avoid data loss, only remove wrappers when they are at the expected boundaries (e.g., HasPrefix/HasSuffix with the newline framing produced by wrapAlteringText), or otherwise parse more strictly and leave interior occurrences untouched.
| if strings.Contains(text, alteringTextWrapperStart) { | |
| text = strings.Replace(text, alteringTextWrapperStart, "", 1) | |
| text = strings.TrimPrefix(text, "\n") | |
| } | |
| if strings.Contains(text, alteringTextWrapperEnd) { | |
| text = strings.Replace(text, alteringTextWrapperEnd, "", 1) | |
| text = strings.TrimSuffix(text, "\n") | |
| } | |
| prefix := alteringTextWrapperStart + "\n" | |
| suffix := "\n" + alteringTextWrapperEnd | |
| if strings.HasPrefix(text, prefix) && strings.HasSuffix(text, suffix) { | |
| return strings.TrimSuffix(strings.TrimPrefix(text, prefix), suffix) | |
| } |
There was a problem hiding this comment.
Actionable comments posted: 4
♻️ Duplicate comments (3)
internal/auditlog/entry_capture_test.go (1)
145-162:⚠️ Potential issue | 🟡 MinorAssert the oversized response was actually truncated.
ResponseBody != nilstill passes if the full oversized payload is retained, and this branch never rechecks the user-path header it is supposed to preserve. Assert the truncation marker/shape here (or otherwise prove the full payload was not kept) pluscore.UserPathHeader.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/auditlog/entry_capture_test.go` around lines 145 - 162, The test currently only checks ResponseBody != nil and ResponseBodyTooBigToHandle, which doesn’t prove the oversized response was truncated or that the user-path header was preserved; update the assertions around entry.Data.ResponseBody and entry.Data.ResponseHeaders to (a) assert the ResponseBody shows the expected truncation shape/marker or is shorter than the original oversized payload (so the full payload wasn’t retained) and (b) assert entry.Data.ResponseHeaders[http.CanonicalHeaderKey(core.UserPathHeader)] equals the preserved user-path value; keep the existing checks for ResponseBodyTooBigToHandle and RequestBodyTooBigToHandle and use the entry.Data.* symbols to locate the checks.internal/guardrails/provider.go (1)
1055-1060:⚠️ Potential issue | 🟠 MajorRestore
function_call_output.outputon the typed-envelope path too.Only the map-backed patch path reconstructs typed JSON output. This branch still writes
modified.Contentverbatim, so a no-op guardrail pass turns object/array/number/null outputs into strings for[]core.ResponsesInputElementrequests.💡 Minimal fix
case "function_call_output": if modified.Role != "tool" { return core.ResponsesInputElement{}, core.NewInvalidRequestError("guardrails cannot reorder or retag responses input items", nil) } - preserved.Output = modified.Content + preserved.Output = restoreResponsesInputOutputValue(original.Output, modified.Content) return preserved, nilAlso applies to: 1156-1157
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/guardrails/provider.go` around lines 1055 - 1060, In the "function_call_output" branch the code assigns preserved.Output = modified.Content, which collapses typed JSON outputs into strings on the typed-envelope path; change this to restore the original typed value (assign preserved.Output from the field that holds the reconstructed/typed output rather than modified.Content) so that object/array/number/null values survive no-op guardrail passes—update the same pattern at the other occurrence referenced (the similar assignment at the later branch around lines 1156-1157) and ensure you use the typed output field (the value used by the map-backed patch path) when setting preserved.Output.internal/responsecache/responsecache.go (1)
179-199:⚠️ Potential issue | 🟠 MajorRemove the unreachable nil-receiver branch; it is causing the current SA5011 failure.
HandleInternalRequestalready returns whenm == nil, so the laterif m == nilblock can never run. Keeping that dead path leavesm/epossibly nil in staticcheck's view and is what the linter is flagging now.♻️ Simplify the guard flow
rec := httptest.NewRecorder() - var e *echo.Echo - switch { - case m == nil: + if m == nil { slog.Error("response cache: HandleInternalRequest called on nil middleware") return nil, core.NewProviderError("", http.StatusInternalServerError, "response cache middleware is not initialized", nil) - case m.echo == nil: + } + if m.echo == nil { slog.Error("response cache: HandleInternalRequest called with uninitialized Echo instance") return nil, core.NewProviderError("", http.StatusInternalServerError, "response cache middleware is not initialized", nil) - default: - e = m.echo } - c := e.NewContext(req, rec) + c := m.echo.NewContext(req, rec) - var err error - if m == nil { - err = next(c) - } else { - err = m.HandleRequest(c, body, func() error { return next(c) }) - } - if err != nil { + if err := m.HandleRequest(c, body, func() error { return next(c) }); err != nil { return nil, err }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/responsecache/responsecache.go` around lines 179 - 199, The second nil-receiver branch is unreachable and triggers SA5011; remove the later "if m == nil { err = next(c) } else { err = m.HandleRequest(...)" block and always call m.HandleRequest(c, body, func() error { return next(c) }) after you create the Echo context (since the function already returned when m == nil), using the previously assigned e and c; this ensures HandleInternalRequest, m.HandleRequest, next and e.NewContext are used consistently and eliminates the dead m==nil path.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@internal/app/app.go`:
- Around line 411-423: The failure branches after calling
guardrailResult.Service.SetExecutor and executionPlanResult.Service.Refresh
forget to close the response cache middleware (rcm) opened earlier by
responsecache.NewResponseCacheMiddleware; update both error cleanup sequences to
include rcm.Close() in the errors.Join(...) calls (and any subsequent closeErr
handling) so rcm is always closed when SetExecutor or Refresh fails.
In `@internal/auditlog/entry_capture_test.go`:
- Around line 105-121: The test currently only checks X-Request-ID and
error.message; extend it to assert the full synthesized-error contract by
verifying that entry.Data.RequestHeaders and entry.Data.ResponseHeaders include
core.UserPathHeader (using http.CanonicalHeaderKey lookup) and that
entry.Data.ResponseBody (the synthesized error envelope) contains an "error"
object with all OpenAI-compatible fields "type", "message", "param", and "code"
(with expected values or non-nil presence as appropriate) so the test fails if
any of those fields or headers are omitted.
In `@internal/executionplans/service_test.go`:
- Around line 625-628: Replace the inline map[string]any payload used in the
test (currently passed to mustMarshalJSON) with a small strongly-typed struct
(e.g., ModelConfig or TestModelConfig) that has fields matching the shape: Model
string and Roles []string; instantiate that struct with Model: "gpt-4o-mini" and
Roles: []string{"user"} and pass it to mustMarshalJSON instead of the map to
ensure compile-time safety and easier evolution of the payload shape.
In `@internal/responsecache/responsecache.go`:
- Around line 246-254: internalCacheType currently only matches exact header
values and returns empty for the "HIT (<cacheType>)" shape emitted by
stream_cache; update internalCacheType to parse that generic form: trim the
header, if it equals CacheHeaderExact or CacheHeaderSemantic return the
corresponding CacheType; else if it matches the pattern "HIT (<inner>)" (use
strings.HasPrefix/HasSuffix to detect and extract the inner value), trim the
inner value and run the same switch on it to return
CacheTypeExact/CacheTypeSemantic; otherwise return "" — make changes inside
internalCacheType to keep all other call sites unchanged.
---
Duplicate comments:
In `@internal/auditlog/entry_capture_test.go`:
- Around line 145-162: The test currently only checks ResponseBody != nil and
ResponseBodyTooBigToHandle, which doesn’t prove the oversized response was
truncated or that the user-path header was preserved; update the assertions
around entry.Data.ResponseBody and entry.Data.ResponseHeaders to (a) assert the
ResponseBody shows the expected truncation shape/marker or is shorter than the
original oversized payload (so the full payload wasn’t retained) and (b) assert
entry.Data.ResponseHeaders[http.CanonicalHeaderKey(core.UserPathHeader)] equals
the preserved user-path value; keep the existing checks for
ResponseBodyTooBigToHandle and RequestBodyTooBigToHandle and use the
entry.Data.* symbols to locate the checks.
In `@internal/guardrails/provider.go`:
- Around line 1055-1060: In the "function_call_output" branch the code assigns
preserved.Output = modified.Content, which collapses typed JSON outputs into
strings on the typed-envelope path; change this to restore the original typed
value (assign preserved.Output from the field that holds the reconstructed/typed
output rather than modified.Content) so that object/array/number/null values
survive no-op guardrail passes—update the same pattern at the other occurrence
referenced (the similar assignment at the later branch around lines 1156-1157)
and ensure you use the typed output field (the value used by the map-backed
patch path) when setting preserved.Output.
In `@internal/responsecache/responsecache.go`:
- Around line 179-199: The second nil-receiver branch is unreachable and
triggers SA5011; remove the later "if m == nil { err = next(c) } else { err =
m.HandleRequest(...)" block and always call m.HandleRequest(c, body, func()
error { return next(c) }) after you create the Echo context (since the function
already returned when m == nil), using the previously assigned e and c; this
ensures HandleInternalRequest, m.HandleRequest, next and e.NewContext are used
consistently and eliminates the dead m==nil path.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 98fd0f2e-a9f7-481d-ae4b-4331d0483d57
📒 Files selected for processing (6)
internal/app/app.gointernal/auditlog/entry_capture_test.gointernal/executionplans/service_test.gointernal/guardrails/provider.gointernal/responsecache/handle_request_test.gointernal/responsecache/responsecache.go
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@internal/auditlog/entry_capture.go`:
- Around line 169-180: The current switch in CaptureInternalJSONExchange
prioritizes bodyValue over responseErr causing partial successful audit entries;
update the logic in CaptureInternalJSONExchange to check responseErr first and,
if non-nil, marshal the error envelope (using the same errors.As gatewayErr
handling and core.NewProviderError(...).ToJSON() fallback) into body, otherwise
fall back to marshalling bodyValue; keep the existing default return behavior
unchanged.
In `@internal/executionplans/service_test.go`:
- Around line 769-785: The test helper assertPipelineRewrite currently only
checks the rewritten message content; add an assertion to validate the message
role wasn't mutated. After calling pipeline.Process and before checking content,
assert that msgs[0].Role equals the original role ("user") (or change the helper
signature to accept a wantRole and assert msgs[0].Role == wantRole) so the test
fails if the rewrite changes role semantics; reference assertPipelineRewrite and
the guardrails.Message produced by pipeline.Process when adding this additional
check.
In `@internal/guardrails/llm_based_altering.go`:
- Around line 484-495: The current acceptance of the first choice in
llm_based_altering.go can let truncated or non-terminal auxiliary completions
(e.g., finish_reason != "stop") replace the original text; update the validation
to reject choices whose FinishReason is not the terminal value (e.g., not
"stop") by checking resp.Choices[0].FinishReason (and ideally any other selected
choice) and returning an error (so the caller falls back to fail-open) before
extracting content or returning unwrapAlteredText(content); keep the existing
checks for nil resp, tool calls, and empty content.
In `@internal/responsecache/responsecache.go`:
- Around line 244-248: The switch on headerValue contains dead comparisons to
CacheHeaderExact and CacheHeaderSemantic because headerValue has already been
stripped of the "HIT (...)" wrapper; remove the CacheHeaderExact and
CacheHeaderSemantic cases and only compare against CacheTypeExact and
CacheTypeSemantic (referencing the headerValue variable and the constants
CacheTypeExact/CacheTypeSemantic) so the switch is simplified to the effective,
reachable branches.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 3090e2c3-556e-48b2-bd5d-0d7525d17923
📒 Files selected for processing (10)
internal/app/app.gointernal/auditlog/entry_capture.gointernal/auditlog/entry_capture_test.gointernal/executionplans/service_test.gointernal/guardrails/llm_based_altering.gointernal/guardrails/llm_based_altering_test.gointernal/guardrails/provider.gointernal/responsecache/handle_request_test.gointernal/responsecache/responsecache.gotests/perf/hotpath_test.go
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@internal/admin/dashboard/templates/execution-plan-chart.html`:
- Line 65: The connector for async audit flows (the div with class "ep-conn
ep-conn-async") is currently hidden by x-show="{{.}}.showUsage &&
{{.}}.showAudit", which makes audit-only flows invisible when showAudit is true
but showUsage is false; change the x-show to rely on audit visibility (e.g.,
x-show="{{.}}.showAudit") or to show when either flag is set (e.g.,
x-show="{{.}}.showUsage || {{.}}.showAudit") so the Audit node remains
connected; update the attribute on the element with class ep-conn-async and
ensure this aligns with the flags set in execution-plans.js (showUsage and
showAudit).
In `@internal/auditlog/entry_capture_test.go`:
- Around line 48-50: The test inconsistently accesses the Traceparent header
directly; change the lookup to use http.CanonicalHeaderKey("Traceparent") like
the other assertions so it reads
entry.Data.RequestHeaders[http.CanonicalHeaderKey("Traceparent")] and keep the
same t.Fatal message; this keeps the pattern consistent with the other checks
that use http.CanonicalHeaderKey.
In `@internal/responsecache/responsecache.go`:
- Around line 221-237: internalRequestHeaders currently copies all headers from
snapshot.GetHeaders(), which can leak sensitive credentials (Authorization,
X-Api-Key, Cookie, etc.); change it to filter using an explicit allowlist (e.g.,
Content-Type, Accept, User-Agent, standard tracing headers like
Traceparent/Request-Id and X-Request-ID) instead of copying everything from
snapshot.GetHeaders(), and preserve the existing behavior that sets Content-Type
and X-Request-ID when absent; update the logic in internalRequestHeaders to
iterate snapshot.GetHeaders() but only Add values for keys present in the
allowlist and ignore/omit sensitive names such as Authorization, Cookie, and
X-Api-Key.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 80f584e4-d175-4f81-9563-cf6fff09c235
📒 Files selected for processing (8)
internal/admin/dashboard/static/js/modules/execution-plans-layout.test.jsinternal/admin/dashboard/templates/execution-plan-chart.htmlinternal/auditlog/entry_capture.gointernal/auditlog/entry_capture_test.gointernal/executionplans/service_test.gointernal/guardrails/llm_based_altering.gointernal/guardrails/llm_based_altering_test.gointernal/responsecache/responsecache.go
| <span class="ep-node-label">Usage</span> | ||
| </div> | ||
| <div class="ep-conn ep-conn-async" x-show="{{.}}.showAudit"></div> | ||
| <div class="ep-conn ep-conn-async" x-show="{{.}}.showUsage && {{.}}.showAudit"></div> |
There was a problem hiding this comment.
Restore connector visibility for audit-only async flows
At Line 65, gating the connector with showUsage && showAudit hides it in valid states where showAudit=true and showUsage=false (see internal/admin/dashboard/static/js/modules/execution-plans.js:1088-1122, where those flags are independently derived). This leaves the Audit node visually disconnected.
Suggested fix
- <div class="ep-conn ep-conn-async" x-show="{{.}}.showUsage && {{.}}.showAudit"></div>
+ <div class="ep-conn ep-conn-async" x-show="{{.}}.showAudit"></div>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <div class="ep-conn ep-conn-async" x-show="{{.}}.showUsage && {{.}}.showAudit"></div> | |
| <div class="ep-conn ep-conn-async" x-show="{{.}}.showAudit"></div> |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal/admin/dashboard/templates/execution-plan-chart.html` at line 65, The
connector for async audit flows (the div with class "ep-conn ep-conn-async") is
currently hidden by x-show="{{.}}.showUsage && {{.}}.showAudit", which makes
audit-only flows invisible when showAudit is true but showUsage is false; change
the x-show to rely on audit visibility (e.g., x-show="{{.}}.showAudit") or to
show when either flag is set (e.g., x-show="{{.}}.showUsage || {{.}}.showAudit")
so the Audit node remains connected; update the attribute on the element with
class ep-conn-async and ensure this aligns with the flags set in
execution-plans.js (showUsage and showAudit).
| if got := entry.Data.RequestHeaders["Traceparent"]; got == "" { | ||
| t.Fatal("RequestHeaders[Traceparent] = empty, want propagated trace header") | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Inconsistent header key access compared to other assertions.
Lines 42-46 use http.CanonicalHeaderKey() for header lookups, but line 48 accesses "Traceparent" directly. While http.CanonicalHeaderKey("Traceparent") happens to return "Traceparent", using the function consistently makes the pattern clearer and more maintainable.
♻️ Minor consistency fix
- if got := entry.Data.RequestHeaders["Traceparent"]; got == "" {
+ if got := entry.Data.RequestHeaders[http.CanonicalHeaderKey("Traceparent")]; got == "" {
t.Fatal("RequestHeaders[Traceparent] = empty, want propagated trace header")
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if got := entry.Data.RequestHeaders["Traceparent"]; got == "" { | |
| t.Fatal("RequestHeaders[Traceparent] = empty, want propagated trace header") | |
| } | |
| if got := entry.Data.RequestHeaders[http.CanonicalHeaderKey("Traceparent")]; got == "" { | |
| t.Fatal("RequestHeaders[Traceparent] = empty, want propagated trace header") | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal/auditlog/entry_capture_test.go` around lines 48 - 50, The test
inconsistently accesses the Traceparent header directly; change the lookup to
use http.CanonicalHeaderKey("Traceparent") like the other assertions so it reads
entry.Data.RequestHeaders[http.CanonicalHeaderKey("Traceparent")] and keep the
same t.Fatal message; this keeps the pattern consistent with the other checks
that use http.CanonicalHeaderKey.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@internal/responsecache/responsecache.go`:
- Around line 23-36: The internalRequestHeaderAllowlist currently omits the
standard "Cache-Control" header, so HandleInternalRequest can forward requests
that then bypass the same cache-bypass behavior used by HandleRequest; add
http.CanonicalHeaderKey("Cache-Control") to internalRequestHeaderAllowlist in
internal/responsecache/responsecache.go so internal requests preserve the
standard Cache-Control semantics, and update the new allowlist unit test (the
test that asserts forwarded headers for internal requests) to include a case
asserting "Cache-Control" is allowed and preserved.
- Around line 205-208: m.HandleRequest's returned error must be normalized to a
core.GatewayError before returning: after calling err := m.HandleRequest(...),
if err != nil use errors.As to check whether err is already a *core.GatewayError
and if so return it unchanged, otherwise wrap it in a new core.GatewayError
(preserving the original message/cause) and return that; add errors to the
imports. Ensure you reference m.HandleRequest and core.GatewayError in the
change.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 71e10060-4d9c-4fa9-9b96-26ffe76d43fa
📒 Files selected for processing (2)
internal/responsecache/handle_request_test.gointernal/responsecache/responsecache.go
| err := m.HandleRequest(c, body, func() error { return next(c) }) | ||
| if err != nil { | ||
| return nil, err | ||
| } |
There was a problem hiding this comment.
Wrap non-core.GatewayError failures before returning here.
This transport-free path no longer goes through Echo's HTTP error handler, so returning m.HandleRequest's error verbatim lets plain cache/backend/provider errors escape unless every caller normalizes them. Preserve existing *core.GatewayErrors, but wrap unknown errors here before returning.
🩹 Suggested change
err := m.HandleRequest(c, body, func() error { return next(c) })
if err != nil {
- return nil, err
+ var gatewayErr *core.GatewayError
+ if errors.As(err, &gatewayErr) {
+ return nil, gatewayErr
+ }
+ slog.Error("response cache: internal request failed", "error", err)
+ return nil, core.NewProviderError("", http.StatusInternalServerError, "internal cache request failed", nil)
}You'll also need to add errors to the imports.
As per coding guidelines: "All errors returned to clients must be instances of core.GatewayError."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal/responsecache/responsecache.go` around lines 205 - 208,
m.HandleRequest's returned error must be normalized to a core.GatewayError
before returning: after calling err := m.HandleRequest(...), if err != nil use
errors.As to check whether err is already a *core.GatewayError and if so return
it unchanged, otherwise wrap it in a new core.GatewayError (preserving the
original message/cause) and return that; add errors to the imports. Ensure you
reference m.HandleRequest and core.GatewayError in the change.
There was a problem hiding this comment.
♻️ Duplicate comments (1)
internal/responsecache/responsecache.go (1)
208-214:⚠️ Potential issue | 🟠 MajorDon't expose raw internal failures in the public provider error.
This path now normalizes unknown errors to
*core.GatewayError, but usingerr.Error()as the public message still leaks cache/backend internals to clients. Log the original failure server-side and return a stable message instead;TestHandleInternalRequest_NormalizesNonGatewayErrorscan keep assertingerrors.Is(...)without pinning the public text.🩹 Minimal fix
if err != nil { var gatewayErr *core.GatewayError if errors.As(err, &gatewayErr) && gatewayErr != nil { return nil, gatewayErr } - return nil, core.NewProviderError("", http.StatusInternalServerError, err.Error(), err) + slog.Error("response cache: internal request failed", "error", err) + return nil, core.NewProviderError("", http.StatusInternalServerError, "internal cache request failed", err) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/responsecache/responsecache.go` around lines 208 - 214, The code currently returns core.NewProviderError with err.Error(), leaking internal cache/backend details; change the return to use a stable public message (e.g. "internal server error") instead of err.Error(), and log the original err server-side before returning (use the existing logger in this package or the surrounding context). Update the branch that checks errors.As(err, &core.GatewayError) to keep returning gatewayErr unchanged, but for the generic path replace core.NewProviderError("", http.StatusInternalServerError, err.Error(), err) with core.NewProviderError("", http.StatusInternalServerError, "internal server error", err) and add a server-side log of err; ensure TestHandleInternalRequest_NormalizesNonGatewayErrors still uses errors.Is(...) and does not depend on the public message.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@internal/responsecache/responsecache.go`:
- Around line 208-214: The code currently returns core.NewProviderError with
err.Error(), leaking internal cache/backend details; change the return to use a
stable public message (e.g. "internal server error") instead of err.Error(), and
log the original err server-side before returning (use the existing logger in
this package or the surrounding context). Update the branch that checks
errors.As(err, &core.GatewayError) to keep returning gatewayErr unchanged, but
for the generic path replace core.NewProviderError("",
http.StatusInternalServerError, err.Error(), err) with core.NewProviderError("",
http.StatusInternalServerError, "internal server error", err) and add a
server-side log of err; ensure
TestHandleInternalRequest_NormalizesNonGatewayErrors still uses errors.Is(...)
and does not depend on the public message.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: de6073ea-a089-484a-b6e3-d401ba3abca5
📒 Files selected for processing (2)
internal/responsecache/handle_request_test.gointernal/responsecache/responsecache.go
Summary
Testing
Summary by CodeRabbit
New Features
Admin UI
Documentation
Bug Fixes
Caching