Skip to content

Commit d02281c

Browse files
authored
Phase 25: error-body-truncation (#506)
* docs: cross-AI review for phase 25 * fix(25): revise plans based on checker feedback * docs(25): finalize verified phase plans * test(25-01): pin shared error body sanitizer contract - add shared sanitizer coverage for trim, rune-safe truncation, and exact [truncated] suffix - update Perplexity error-path tests to expect the new truncation contract - strengthen OpenRouter tests for raw fallback and structured error.message sanitization * fix(25-01): share error body sanitization across providers - add panic-safe SanitizeErrorBody in pkg/commons/http with trim, rune-limit, and [truncated] contract - route Perplexity HTTP error formatting through the shared sanitizer and remove local truncation logic - sanitize OpenRouter parsed error.message and raw fallback body text through the shared helper * docs(25-01): complete error body truncation plan - summarize the shared sanitizer contract and Perplexity/OpenRouter normalization work - advance Phase 25 planning state to plan 2 of 4 and update roadmap progress - mark ERR-01 complete while leaving ERR-02 pending for later provider migration plans * test(25-02): add failing long-body provider regressions - add OpenAI regression for truncated provider error bodies - add Baseten regression for truncated provider error bodies * fix(25-02): sanitize batch-a provider error bodies - route OpenAI and Baseten raw HTTP bodies through the shared sanitizer - sanitize Bedrock and Chroma Cloud provider error body segments - preserve provider-specific status wording while truncating oversized payloads * fix(25-03): sanitize provider error bodies in batch A - preserve Cloudflare structured errors while truncating only the raw body tail - route Cohere, Hugging Face, and Jina raw error text through SanitizeErrorBody - add a focused Cloudflare regression for the mixed structured/raw error contract * docs(25-02): add error-body-truncation summary - record the 25-02 provider migration outcome and verification evidence - capture task commits, decisions, and workflow deviations for this plan * docs(25-03): complete error body truncation plan - record the Cloudflare/Cohere/HF/Jina sanitizer slice summary - update phase 25 state after completing plan 25-03 in the forked workspace - refresh roadmap progress for the tracked phase summaries * docs(25-03): sync tracked phase progress - reflect the concurrent 25-02 summary in phase 25 progress counts - restore state and roadmap to the current tracked 3/4 completion view * fix(25-04): sanitize batch-B provider error bodies - route the remaining raw-body provider errors through chttp.SanitizeErrorBody - preserve each provider's existing status and endpoint wording - keep the migration limited to the body-derived error segment * test(25-04): add Twelve Labs truncation regressions - pin a long structured message case that requires [truncated] - pin a long raw fallback body case that rejects the full provider payload - capture the approved body-derived text policy before implementation * fix(25-04): sanitize Twelve Labs error bodies - sanitize parsed apiErr.Message values as body-derived text - sanitize the raw fallback response body path as well - keep the existing Twelve Labs error wording intact * test(25-04): verify full embedding sanitizer rollout - run go test -tags=ef ./pkg/commons/http ./pkg/embeddings/... successfully - confirm make lint is clean after the final provider migrations - capture the green phase gate as the completion point for Task 3 * docs(25-04): complete error-body-truncation plan - complete the batch-B and Twelve Labs sanitizer rollout summary - update state, roadmap, and requirements for Phase 25 completion - record verification evidence for the full embedding sweep and lint gate * fix(25-03): sanitize non-json Cloudflare error bodies - fall back to the shared sanitizer on non-JSON error responses\n- add a focused regression for plain-text gateway failures\n- keep the JSON structured-error path unchanged * docs(25): add code review report - capture the post-fix Phase 25 review findings\n- record the remaining non-blocking Cohere default-model warning * docs(phase-25): complete phase execution - add the Phase 25 verification report\n- record passed goal verification on the current codebase * docs(phase-25): finalize state after verification - mark Phase 25 as verified complete in STATE.md\n- advance the current focus to Phase 30\n- normalize the current-position fields after the partial phase-complete update * test(25): complete UAT - 8 passed, 0 issues * docs(25): add code review report * fix(25): WR-03 restore cohere default model * fix(25): WR-02 sanitize cloudflare structured errors * fix(25): bound sanitizer allocations * fix(25): WR-01 remove stale sanitizer path * test(25): strengthen cloudflare structured error regression * docs(25): add code review fix report * Preserve structured embedding API error details * docs(25): ship phase 25 — PR #506 * fix(25): sanitize parsed error fields and harden panic-recovery seam Address PR #506 review feedback for error-body truncation: - chromacloud, chromacloudsplade: wrap parsed embResp.Error with SanitizeErrorBody so JSON-side error strings can't bypass the 512-rune display cap. - openrouter: sanitize apiErr.Error.Code (typed any) by routing through SanitizeErrorBody after %v formatting. - twelvelabs: sanitize apiErr.Code in the structured-error branch. - pkg/commons/http: replace mutable sanitizeErrorBodyFunc package var with a private sanitizeErrorBodyWith(body, fn) helper; the panic recovery test now injects via parameter and is parallel-safe. Each provider fix ships with an httptest-based regression test that fails before the fix and passes after. * fix(25): restore ReadRespBody empty-string contract on read error Commit 6407cc3 changed ReadRespBody to return a bracketed sentinel ("[failed to read response body: ...]") on ReadLimitedBody failure. The seven v2-client callers in pkg/api/v2/client_http.go feed the result into strconv.Atoi / json.Unmarshal / NewTenantFromJSON / strings.Contains, so the sentinel surfaced as confusing parse errors that obscured the real failure (oversize body or transport error). Revert the error path to "", matching the historical 2024-era contract that the v2 callers were written against. Keep the ReadLimitedBody size cap (the legitimate defense from 6407cc3). Add a brief WHY comment so the sentinel does not get re-introduced. Replace the obsolete TestReadRespBodyReportsReadErrors with two parallel-safe contract tests (read error + oversize body). A proper (string, error) refactor remains owed as future work but is out of Phase 25 scope.
1 parent 5cab223 commit d02281c

47 files changed

Lines changed: 2597 additions & 83 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.planning/REQUIREMENTS.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@
2020

2121
### Error Handling
2222

23-
- [ ] **ERR-01**: Shared `SanitizeErrorBody` utility truncates HTTP error bodies to a safe display length with `[truncated]` suffix
24-
- [ ] **ERR-02**: All embedding providers use `SanitizeErrorBody` for error message construction instead of raw `string(respData)`
23+
- [x] **ERR-01**: Shared `SanitizeErrorBody` utility truncates HTTP error bodies to a safe display length with `[truncated]` suffix
24+
- [x] **ERR-02**: All embedding providers use `SanitizeErrorBody` for error message construction instead of raw `string(respData)`
2525

2626
### Provider Enhancement
2727

@@ -67,8 +67,8 @@
6767
| EFL-01 | Phase 23 | Complete |
6868
| EFL-02 | Phase 24 | Pending |
6969
| EFL-03 | Phase 24 | Pending |
70-
| ERR-01 | Phase 25 | Pending |
71-
| ERR-02 | Phase 25 | Pending |
70+
| ERR-01 | Phase 25 | Complete |
71+
| ERR-02 | Phase 25 | Complete |
7272
| TLA-01 | Phase 26 | Pending |
7373
| TLA-02 | Phase 26 | Pending |
7474
| TLA-03 | Phase 26 | Pending |
@@ -84,4 +84,4 @@
8484

8585
---
8686
*Requirements defined: 2026-04-08*
87-
*Last updated: 2026-04-11 after Phase 23 completion*
87+
*Last updated: 2026-04-13 after Phase 25 completion*

.planning/ROADMAP.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ See: [v0.4.1 Archived Roadmap](milestones/v0.4.1-ROADMAP.md)
2626
- [x] **Phase 22: WithGroupBy Validation** - WithGroupBy(nil) returns an error instead of silently skipping grouping (completed 2026-04-10)
2727
- [x] **Phase 23: ORT EF Leak Fix** - Default ORT EF is properly closed when CreateCollection finds an existing collection (completed 2026-04-11)
2828
- [x] **Phase 24: GetOrCreateCollection EF Safety** - GetOrCreateCollection does not pass closed EFs to CreateCollection fallback (completed 2026-04-12)
29-
- [ ] **Phase 25: Error Body Truncation** - Embedding provider error messages truncate raw HTTP bodies to safe display lengths
29+
- [x] **Phase 25: Error Body Truncation** - Embedding provider error messages truncate raw HTTP bodies to safe display lengths (completed 2026-04-13)
3030
- [ ] **Phase 26: Twelve Labs Async Embedding** - Twelve Labs provider handles async task responses for long-running media
3131
- [ ] **Phase 27: Download Stack Consolidation** - default_ef download code uses shared downloadutil instead of its own HTTP implementation
3232
- [ ] **Phase 28: Morph Test Fix** - Morph EF integration test handles upstream 404 gracefully
@@ -109,10 +109,13 @@ Plans:
109109
1. A shared SanitizeErrorBody utility exists that truncates bodies exceeding a safe display length and appends a `[truncated]` suffix
110110
2. All embedding providers use SanitizeErrorBody when constructing error messages from HTTP responses
111111
3. Error messages from providers with large error bodies are readable (not multi-KB dumps)
112-
**Plans**: TBD
112+
**Plans**: 4 plans
113113

114114
Plans:
115-
- [ ] 25-01: TBD
115+
- [x] 25-01-PLAN.md — Create the shared sanitizer contract with panic recovery and normalize Perplexity/OpenRouter
116+
- [x] 25-02-PLAN.md — Migrate the representative raw-body providers and add OpenAI/Baseten regressions
117+
- [x] 25-03-PLAN.md — Finish batch A with Cloudflare/Cohere/HF/Jina and preserve Cloudflare mixed formatting
118+
- [x] 25-04-PLAN.md — Finish batch B, sanitize Twelve Labs structured errors, and run the full embedding sweep/lint
116119

117120
### Phase 26: Twelve Labs Async Embedding
118121
**Goal**: Twelve Labs provider handles async task responses for long-running audio and video embeddings
@@ -196,7 +199,7 @@ Phase 24 depends on Phase 23. Phase 26 depends on Phase 25. Phase 29 depends on
196199
| 22. WithGroupBy Validation | v0.4.2 | 1/1 | Complete | 2026-04-10 |
197200
| 23. ORT EF Leak Fix | v0.4.2 | 1/1 | Complete | 2026-04-11 |
198201
| 24. GetOrCreateCollection EF Safety | v0.4.2 | 1/1 | Complete | 2026-04-12 |
199-
| 25. Error Body Truncation | v0.4.2 | 0/0 | Not started | - |
202+
| 25. Error Body Truncation | v0.4.2 | 4/4 | Complete | 2026-04-13 |
200203
| 26. Twelve Labs Async Embedding | v0.4.2 | 0/0 | Not started | - |
201204
| 27. Download Stack Consolidation | v0.4.2 | 0/0 | Not started | - |
202205
| 28. Morph Test Fix | v0.4.2 | 0/0 | Not started | - |

.planning/STATE.md

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@
22
gsd_state_version: 1.0
33
milestone: v0.4.2
44
milestone_name: Bug Fixes and Robustness
5-
status: planning
6-
stopped_at: Phase 24 completed
7-
last_updated: "2026-04-12T15:44:20.760Z"
8-
last_activity: 2026-04-12
5+
status: ready
6+
stopped_at: Phase 25 shipped — PR #506
7+
last_updated: "2026-04-13T18:25:33.287Z"
8+
last_activity: "2026-04-13 -- Phase 25 shipped — PR #506"
99
progress:
1010
total_phases: 11
11-
completed_phases: 5
12-
total_plans: 6
13-
completed_plans: 6
11+
completed_phases: 6
12+
total_plans: 10
13+
completed_plans: 10
1414
percent: 100
1515
---
1616

@@ -27,25 +27,38 @@ See: .planning/PROJECT.md (updated 2026-04-10)
2727

2828
Phase: 30
2929
Plan: Not started
30-
Status: Ready for Phase 30 planning
31-
Last activity: 2026-04-12
30+
Status: Phase 25 shipped — PR #506
31+
Last activity: 2026-04-13 -- Phase 25 shipped — PR #506
3232

3333
Progress: [██████████] 100%
3434

3535
## Performance Metrics
3636

3737
**Velocity:**
3838

39-
- Total plans completed: 6
40-
- Average duration: --
41-
- Total execution time: 0 hours
39+
- Total plans completed: 10
40+
- Average duration: 23 min
41+
- Total execution time: 23 min
4242

4343
## Accumulated Context
4444

4545
### Decisions
4646

4747
Decisions are logged in PROJECT.md Key Decisions table.
4848

49+
- [Phase 25]: Kept ReadLimitedBody and MaxResponseBodySize unchanged so transport safety and display safety stay separate concerns.
50+
- [Phase 25]: Sanitized OpenRouter's parsed error.message as body-derived text instead of trusting structured JSON fields to remain short.
51+
- [Phase 25]: Left ERR-02 pending because 25-01 only normalizes Perplexity/OpenRouter; later Phase 25 plans still migrate the remaining providers.
52+
- [Phase 25]: Kept provider-specific error wording intact and changed only raw-body segments to use SanitizeErrorBody(...)
53+
- [Phase 25]: Used OpenAI and Baseten as the representative long-body regressions for the first raw-body provider batch
54+
- [Phase 25]: Left ERR-02 pending because the remaining provider batches still belong to Plans 25-03 and 25-04
55+
- [Phase 25]: Cloudflare keeps parsed embeddings.Errors intact while sanitizing only the appended raw-body tail.
56+
- [Phase 25]: Cloudflare's mixed-format contract is enforced with a focused httptest regression instead of a source-only check.
57+
- [Phase 25]: Cohere's default embed model moved to embed-english-v3.0 after the retired v2.0 default blocked live ef verification on April 13, 2026.
58+
- [Phase 25]: Kept the batch-B provider edits mechanical by changing only the body-derived error segment and preserving existing status and endpoint wording. — This completed ERR-02 without widening the rollout into provider-specific wording changes.
59+
- [Phase 25]: Treated Twelve Labs parsed apiErr.Message values as body-derived text and sanitized them the same way as the raw fallback path. — The review-approved policy for parsed provider error text needed to stay consistent across OpenRouter and Twelve Labs.
60+
- [Phase 25]: Used a temporary authless DOCKER_CONFIG and a one-time ollama/ollama:latest pre-pull to unblock the required ollama ef verification. — The host Docker credsStore helper timed out on public-image pulls; isolating Docker config restored the intended verification path without repository changes.
61+
4962
### Roadmap Evolution
5063

5164
- Phase 21.1 inserted after Phase 21: RRF cloud integration test coverage including arithmetic compositions (URGENT) — post-fix cloud coverage gap for Phase 21 arithmetic methods
@@ -57,5 +70,5 @@ Decisions are logged in PROJECT.md Key Decisions table.
5770

5871
## Session
5972

60-
**Last Date:** 2026-04-12T14:47:20.000Z
61-
**Stopped At:** Phase 24 completed
73+
**Last Date:** 2026-04-13T08:13:19.127Z
74+
**Stopped At:** Phase 25 shipped — PR #506
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
---
2+
phase: 25-error-body-truncation
3+
plan: 01
4+
type: execute
5+
wave: 1
6+
depends_on: []
7+
files_modified:
8+
- pkg/commons/http/utils.go
9+
- pkg/commons/http/utils_test.go
10+
- pkg/embeddings/perplexity/perplexity.go
11+
- pkg/embeddings/perplexity/perplexity_test.go
12+
- pkg/embeddings/openrouter/openrouter.go
13+
- pkg/embeddings/openrouter/openrouter_test.go
14+
autonomous: true
15+
requirements:
16+
- ERR-01
17+
- ERR-02
18+
19+
must_haves:
20+
truths:
21+
- "`pkg/commons/http/utils.go` defines `SanitizeErrorBody(body []byte)` as the shared display-layer sanitizer, with whitespace trimming, rune-safe truncation, and the exact `[truncated]` suffix."
22+
- "`SanitizeErrorBody(...)` follows `CLAUDE.md` panic-prevention guidance: it recovers from internal panics and returns the best sanitized result available instead of propagating a panic or surfacing an unsanitized full body."
23+
- "`MaxResponseBodySize`, `ReadLimitedBody(...)`, and other transport guards remain unchanged so this plan affects display safety only."
24+
- "`pkg/embeddings/perplexity/perplexity.go` and `pkg/embeddings/openrouter/openrouter.go` stop owning local truncation behavior and delegate to `chttp.SanitizeErrorBody(...)`."
25+
- "OpenRouter treats parsed `error.message` as body-derived text and sanitizes it before returning it."
26+
- "Focused tests in `pkg/commons/http/utils_test.go`, `pkg/embeddings/perplexity/perplexity_test.go`, and `pkg/embeddings/openrouter/openrouter_test.go` pin UTF-8-safe truncation and the `[truncated]` contract."
27+
artifacts:
28+
- path: "pkg/commons/http/utils.go"
29+
provides: "Shared panic-safe error-body sanitizer"
30+
contains: "func SanitizeErrorBody("
31+
- path: "pkg/commons/http/utils_test.go"
32+
provides: "Shared helper contract coverage for trim, rune-safe truncation, and suffix policy"
33+
contains: "TestSanitizeErrorBody"
34+
- path: "pkg/embeddings/openrouter/openrouter.go"
35+
provides: "OpenRouter structured-error parsing routed through the shared sanitizer"
36+
contains: "SanitizeErrorBody([]byte(apiErr.Error.Message))"
37+
- path: "pkg/embeddings/perplexity/perplexity.go"
38+
provides: "Perplexity HTTP error formatting normalized onto the shared sanitizer"
39+
contains: "SanitizeErrorBody(respData)"
40+
key_links:
41+
- from: "pkg/commons/http/utils.go:SanitizeErrorBody"
42+
to: "pkg/embeddings/perplexity/perplexity.go and pkg/embeddings/openrouter/openrouter.go"
43+
via: "Shared display sanitizer replaces provider-local truncation helpers"
44+
pattern: "SanitizeErrorBody"
45+
- from: "pkg/commons/http/utils_test.go"
46+
to: "pkg/embeddings/perplexity/perplexity_test.go and pkg/embeddings/openrouter/openrouter_test.go"
47+
via: "Focused regressions keep the `[truncated]` contract aligned across the shared helper and both local precedents"
48+
pattern: "[truncated]"
49+
---
50+
51+
<objective>
52+
Establish the shared `SanitizeErrorBody(...)` contract in `pkg/commons/http` and normalize the two existing local precedents (`Perplexity` and `OpenRouter`) onto it.
53+
54+
Purpose: this plan isolates the risky semantics change first: one shared helper, one shared suffix contract, one explicit policy for parsed OpenRouter message strings, and one CLAUDE.md-compliant panic-recovery rule. The later provider-audit plans can then be mechanical migrations instead of contract design work.
55+
56+
Output: shared panic-safe sanitizer, updated helper/provider tests, and normalized Perplexity/OpenRouter error formatting.
57+
</objective>
58+
59+
<execution_context>
60+
@/Users/tazarov/.codex/get-shit-done/workflows/execute-plan.md
61+
@/Users/tazarov/.codex/get-shit-done/templates/summary.md
62+
</execution_context>
63+
64+
<context>
65+
@.planning/PROJECT.md
66+
@.planning/ROADMAP.md
67+
@.planning/REQUIREMENTS.md
68+
@.planning/STATE.md
69+
@.planning/phases/25-error-body-truncation/25-RESEARCH.md
70+
@.planning/phases/25-error-body-truncation/25-REVIEWS.md
71+
@.planning/phases/25-error-body-truncation/25-VALIDATION.md
72+
@pkg/commons/http/utils.go
73+
@pkg/commons/http/utils_test.go
74+
@pkg/embeddings/perplexity/perplexity.go
75+
@pkg/embeddings/perplexity/perplexity_test.go
76+
@pkg/embeddings/openrouter/openrouter.go
77+
@pkg/embeddings/openrouter/openrouter_test.go
78+
@CLAUDE.md
79+
80+
<interfaces>
81+
From `pkg/commons/http/utils.go`:
82+
```go
83+
const MaxResponseBodySize = 200 * 1024 * 1024
84+
85+
func ReadLimitedBody(r io.Reader) ([]byte, error) { ... }
86+
```
87+
88+
From `pkg/embeddings/perplexity/perplexity.go`:
89+
```go
90+
const maxErrorBodyChars = 512
91+
92+
func sanitizeErrorBody(body []byte) string { ... }
93+
```
94+
95+
From `pkg/embeddings/openrouter/openrouter.go`:
96+
```go
97+
func parseAPIError(body []byte) string {
98+
var apiErr apiErrorResponse
99+
if err := json.Unmarshal(body, &apiErr); err == nil && apiErr.Error.Message != "" {
100+
return apiErr.Error.Message
101+
}
102+
...
103+
}
104+
```
105+
</interfaces>
106+
</context>
107+
108+
<tasks>
109+
110+
<task type="auto" tdd="true">
111+
<name>Task 1: Pin the shared sanitizer contract in tests before changing production code</name>
112+
<files>pkg/commons/http/utils_test.go, pkg/embeddings/perplexity/perplexity_test.go, pkg/embeddings/openrouter/openrouter_test.go</files>
113+
<behavior>
114+
- `SanitizeErrorBody(nil)` and `SanitizeErrorBody([]byte(""))` return `""`
115+
- leading/trailing whitespace is trimmed before truncation checks
116+
- long values truncate by rune count, not bytes
117+
- truncated values append the exact suffix `[truncated]`
118+
- OpenRouter's parsed `error.message` path is treated as body-derived text and is expected to surface the shared sanitized contract
119+
- compile-fail RED is acceptable for the brand-new helper because `SanitizeErrorBody(...)` does not exist yet
120+
</behavior>
121+
<action>
122+
Add or update focused tests first:
123+
124+
- in `pkg/commons/http/utils_test.go`, add a `TestSanitizeErrorBody` table that covers nil/empty input, trim behavior, short-body passthrough, long ASCII truncation, and UTF-8-safe truncation
125+
- in `pkg/embeddings/perplexity/perplexity_test.go`, replace old `...(truncated)` expectations with `[truncated]`
126+
- in `pkg/embeddings/openrouter/openrouter_test.go`, update the raw fallback truncation assertion and add/strengthen a structured JSON case so an overlong `error.message` is expected to be sanitized too
127+
128+
Use the focused command below as the RED/GREEN loop. The first RED run may fail to compile because the shared helper is not defined yet; that compile-fail is acceptable and should not be worked around by weakening the tests.
129+
</action>
130+
<verify>
131+
<automated>cd /Users/tazarov/GolandProjects/chroma-go && go test -tags=ef ./pkg/commons/http ./pkg/embeddings/openrouter ./pkg/embeddings/perplexity</automated>
132+
</verify>
133+
<done>`utils_test.go`, `perplexity_test.go`, and `openrouter_test.go` all pin the exact `[truncated]` contract and the new structured-message policy before implementation changes begin.</done>
134+
</task>
135+
136+
<task type="auto" tdd="true">
137+
<name>Task 2: Implement the shared helper with panic recovery and normalize Perplexity/OpenRouter onto it</name>
138+
<files>pkg/commons/http/utils.go, pkg/embeddings/perplexity/perplexity.go, pkg/embeddings/openrouter/openrouter.go</files>
139+
<behavior>
140+
- `SanitizeErrorBody(...)` is the single shared display sanitizer used by both providers
141+
- `ReadLimitedBody(...)` and `MaxResponseBodySize` remain unchanged
142+
- the helper recovers from panics and returns the best-effort sanitized result accumulated so far; if panic happens before the final value is assigned, the fallback path still enforces the same trim + rune-limit + `[truncated]` contract
143+
- OpenRouter sanitizes both parsed `error.message` and raw fallback body text because both originate from the HTTP body
144+
</behavior>
145+
<action>
146+
In `pkg/commons/http/utils.go`, add `func SanitizeErrorBody(body []byte) (result string)` with this exact contract:
147+
148+
- trim whitespace
149+
- truncate by rune count at the shared 512-rune display limit
150+
- append the exact suffix `[truncated]` when truncation occurs
151+
- include a `defer` panic-recovery block per `CLAUDE.md`; recovery must return the best `result` accumulated so far and, if `result` is still empty, it must re-apply the same trim + rune-limit + `[truncated]` contract to the fallback string rather than returning `strings.TrimSpace(string(body))` raw
152+
153+
Then normalize the local precedents:
154+
155+
- remove Perplexity's bespoke truncation helper and route the HTTP error path through `chttp.SanitizeErrorBody(respData)`
156+
- keep OpenRouter's JSON parse, but sanitize `apiErr.Error.Message` via `chttp.SanitizeErrorBody([]byte(apiErr.Error.Message))`; the raw fallback path must also use the shared helper
157+
158+
Do not change transport-level helpers or widen scope into other providers in this plan.
159+
</action>
160+
<verify>
161+
<automated>cd /Users/tazarov/GolandProjects/chroma-go && go test -tags=ef ./pkg/commons/http ./pkg/embeddings/openrouter ./pkg/embeddings/perplexity && make lint</automated>
162+
</verify>
163+
<done>The shared helper exists, follows the panic-recovery rule, Perplexity/OpenRouter delegate to it, and the focused helper/provider test set passes green.</done>
164+
</task>
165+
166+
</tasks>
167+
168+
<threat_model>
169+
## Trust Boundaries
170+
171+
| Boundary | Description |
172+
|----------|-------------|
173+
| Provider HTTP response body -> returned Go error | Third-party APIs control both body size and body content; the SDK decides how much is surfaced to callers |
174+
175+
## STRIDE Threat Register
176+
177+
| Threat ID | Category | Component | Disposition | Mitigation Plan |
178+
|-----------|----------|-----------|-------------|-----------------|
179+
| T-25-01 | I (Information Disclosure) | `pkg/commons/http/utils.go` and provider error formatting | mitigate | Centralize body sanitization and stop returning provider bodies verbatim |
180+
| T-25-02 | T (Tampering) | local provider truncation helpers | mitigate | Replace Perplexity/OpenRouter local implementations with one shared helper |
181+
| T-25-03 | D (Denial of Service) | error/log readability | mitigate | Keep transport guard separate and add rune-safe display truncation with `[truncated]` |
182+
| T-25-04 | D (Denial of Service) | sanitizer runtime behavior | mitigate | Add panic recovery so sanitization failures degrade to partial results instead of crashing library callers |
183+
</threat_model>
184+
185+
<verification>
186+
1. `go test -tags=ef ./pkg/commons/http ./pkg/embeddings/openrouter ./pkg/embeddings/perplexity` passes
187+
2. `pkg/commons/http/utils.go` contains `func SanitizeErrorBody(`
188+
3. `pkg/commons/http/utils.go` contains a `recover()` path for best-effort partial return behavior
189+
4. `pkg/embeddings/perplexity/perplexity.go` and `pkg/embeddings/openrouter/openrouter.go` both contain `SanitizeErrorBody`
190+
</verification>
191+
192+
<success_criteria>
193+
- Phase 25 has a shared sanitizer contract anchored in `pkg/commons/http`
194+
- The helper follows the repo's no-panic rule and degrades to partial output on panic
195+
- Perplexity and OpenRouter no longer drift on suffix or truncation behavior
196+
- The shared helper and both local precedents are covered by focused green tests
197+
</success_criteria>
198+
199+
<output>
200+
After completion, create `.planning/phases/25-error-body-truncation/25-01-SUMMARY.md`
201+
</output>

0 commit comments

Comments
 (0)