Skip to content

Commit 8814ef6

Browse files
committed
fix: handle HTTP 404/410 from remote CAR gateways
wrap HTTP error responses from remote CAR fetcher with ErrorStatusCode to ensure proper error classification in gateway handlers - 404 responses are now recognized by isErrNotFound() - 410 responses are now recognized by isErrContentBlocked() - limit error message reading to 512 bytes to prevent memory exhaustion - properly close/drain response body to avoid connection leaks - fixes directory listing failures with remote-car-backend
1 parent 5de9351 commit 8814ef6

File tree

2 files changed

+34
-4
lines changed

2 files changed

+34
-4
lines changed

gateway/backend_car_fetcher.go

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,15 +66,33 @@ func (ps *remoteCarFetcher) Fetch(ctx context.Context, path path.ImmutablePath,
6666
}
6767

6868
if resp.StatusCode != http.StatusOK {
69-
errData, err := io.ReadAll(resp.Body)
69+
defer resp.Body.Close() // Ensure body is closed and drained
70+
71+
// Limit error message reading to prevent memory exhaustion
72+
// 512 bytes is enough for one sentence of error description
73+
const maxErrorSize = 512
74+
limitedReader := io.LimitReader(resp.Body, maxErrorSize)
75+
errData, err := io.ReadAll(limitedReader)
76+
var errMsg string
7077
if err != nil {
71-
err = fmt.Errorf("could not read error message: %w", err)
78+
errMsg = fmt.Sprintf("could not read error message: %v", err)
7279
} else {
73-
err = fmt.Errorf("%q", string(errData))
80+
errMsg = string(errData)
81+
// Add ellipsis if we hit the limit
82+
if len(errData) == maxErrorSize {
83+
errMsg = errMsg + "..."
84+
}
7485
}
75-
return fmt.Errorf("http error from car gateway: %s: %w", resp.Status, err)
86+
87+
// Wrap with appropriate status code for proper handling downstream
88+
return NewErrorStatusCode(
89+
fmt.Errorf("car gateway responded with %s: %q", resp.Status, errMsg),
90+
resp.StatusCode,
91+
)
7692
}
7793

94+
// For successful responses, callback is responsible for reading
95+
// and closing the body
7896
err = cb(path, resp.Body)
7997
if err != nil {
8098
resp.Body.Close()

gateway/errors.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,12 @@ func webError(w http.ResponseWriter, r *http.Request, c *Config, err error, defa
253253
// isErrNotFound returns true for IPLD errors that should return 4xx errors (e.g. the path doesn't exist, the data is
254254
// the wrong type, etc.), rather than issues with just finding and retrieving the data.
255255
func isErrNotFound(err error) bool {
256+
// Check for ErrorStatusCode with 404
257+
var statusErr *ErrorStatusCode
258+
if errors.As(err, &statusErr) && statusErr.StatusCode == http.StatusNotFound {
259+
return true
260+
}
261+
256262
if errors.Is(err, &resolver.ErrNoLink{}) || errors.Is(err, schema.ErrNoSuchField{}) {
257263
return true
258264
}
@@ -295,6 +301,12 @@ func isErrNotFound(err error) bool {
295301
// TODO: When nopfs becomes a direct dependency, replace this string matching with proper
296302
// type assertion or errors.Is() for more robust error detection.
297303
func isErrContentBlocked(err error) bool {
304+
// Check for ErrorStatusCode with 410
305+
var statusErr *ErrorStatusCode
306+
if errors.As(err, &statusErr) && statusErr.StatusCode == http.StatusGone {
307+
return true
308+
}
309+
298310
// The nopfs StatusError.Error() returns messages in the format:
299311
// - "{cid} is blocked and cannot be provided" for blocked CIDs
300312
// - "{path} is blocked and cannot be provided" for blocked paths

0 commit comments

Comments
 (0)