Skip to content

Commit 4bc3800

Browse files
committed
fix(vector): abort builds on auth-status encode errors instead of skipping
1 parent 75df6f3 commit 4bc3800

2 files changed

Lines changed: 39 additions & 3 deletions

File tree

internal/vector/encoder.go

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,20 @@ func (e *HTTPStatusError) Error() string {
7070

7171
// Permanent reports whether the embeddings endpoint's response indicates a
7272
// rejection of this specific input that will never succeed on retry: any
73-
// 4xx status except 429, which is rate-limiting rather than a content
74-
// rejection and is itself retryable.
73+
// 4xx status except 429 (rate-limiting, retryable) and except the auth
74+
// statuses 401/403/407, which describe the caller's credentials rather
75+
// than the input — skip-stamping documents on an expired token would
76+
// silently mark an entire corpus embedded-with-no-vectors, so auth
77+
// failures must abort the build instead.
7578
func (e *HTTPStatusError) Permanent() bool {
76-
return e.Status >= 400 && e.Status < 500 && e.Status != http.StatusTooManyRequests
79+
switch e.Status {
80+
case http.StatusTooManyRequests,
81+
http.StatusUnauthorized,
82+
http.StatusForbidden,
83+
http.StatusProxyAuthRequired:
84+
return false
85+
}
86+
return e.Status >= 400 && e.Status < 500
7787
}
7888

7989
// embeddingsRequestBody is the OpenAI-compatible embeddings request.

internal/vector/encoder_test.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,32 @@ func TestEncoder429ReturnsNonPermanentHTTPStatusError(t *testing.T) {
311311
assert.False(t, statusErr.Permanent(), "429 is transient rate-limiting, not a content rejection")
312312
}
313313

314+
// TestHTTPStatusErrorPermanentClassification pins the skip-vs-abort
315+
// classification: auth statuses describe the caller's credentials, not the
316+
// input, so treating them as permanent would skip-stamp an entire corpus
317+
// on an expired token instead of aborting the build.
318+
func TestHTTPStatusErrorPermanentClassification(t *testing.T) {
319+
cases := []struct {
320+
status int
321+
permanent bool
322+
}{
323+
{http.StatusBadRequest, true},
324+
{http.StatusNotFound, true},
325+
{http.StatusUnprocessableEntity, true},
326+
{http.StatusUnauthorized, false},
327+
{http.StatusForbidden, false},
328+
{http.StatusProxyAuthRequired, false},
329+
{http.StatusTooManyRequests, false},
330+
{http.StatusInternalServerError, false},
331+
{http.StatusBadGateway, false},
332+
}
333+
for _, tc := range cases {
334+
err := &HTTPStatusError{Status: tc.status}
335+
assert.Equalf(t, tc.permanent, err.Permanent(),
336+
"status %d: Permanent() classification", tc.status)
337+
}
338+
}
339+
314340
// TestEncoderDecodeErrorIsRetried covers fix 2: a decoding failure almost
315341
// always means the connection died mid-stream, not that the endpoint sent
316342
// a deliberately malformed response, so it must be retried rather than

0 commit comments

Comments
 (0)