diff --git a/freerelay/control_plane/learner/outcome_consumer.py b/freerelay/control_plane/learner/outcome_consumer.py index d5f1448..de3339f 100644 --- a/freerelay/control_plane/learner/outcome_consumer.py +++ b/freerelay/control_plane/learner/outcome_consumer.py @@ -136,7 +136,7 @@ async def ensure_group(self) -> None: logger.info( "consumer_group_created stream=%s group=%s", self._stream, self._group ) - except Exception as exc: + except redis.asyncio.ResponseError as exc: # BUSYGROUP means the group already exists — that's fine if "BUSYGROUP" in str(exc): logger.debug( @@ -167,7 +167,7 @@ async def consume_batch( count=batch_size, block=block_ms, ) - except Exception: + except redis.asyncio.ResponseError: logger.exception("consume_batch_read_error") return [] @@ -177,7 +177,7 @@ async def consume_batch( try: record = OutcomeRecord.from_stream(msg_id, fields) records.append(record) - except Exception: + except (json.JSONDecodeError, ValueError, TypeError): logger.exception("consume_batch_parse_error id=%s", msg_id) if records: @@ -195,7 +195,7 @@ async def acknowledge(self, message_ids: list[str]) -> int: count = await self._redis.xack(self._stream, self._group, *message_ids) logger.debug("acked_outcomes count=%d", count) return count - except Exception: + except redis.asyncio.ResponseError: logger.exception("acknowledge_error") return 0 @@ -224,7 +224,7 @@ async def pending_info(self) -> dict[str, Any]: "max_id": info.get("max"), "consumers": info.get("consumers", []), } - except Exception: + except redis.asyncio.ResponseError: logger.exception("pending_info_error") return {"pending_count": 0} @@ -274,6 +274,6 @@ async def claim_stale( logger.info("claimed_stale_outcomes count=%d", len(records)) return records - except Exception: + except redis.asyncio.ResponseError: logger.exception("claim_stale_error") return [] diff --git a/freerelay/control_plane/learner/policy_publisher.py b/freerelay/control_plane/learner/policy_publisher.py index 1928893..5187bf6 100644 --- a/freerelay/control_plane/learner/policy_publisher.py +++ b/freerelay/control_plane/learner/policy_publisher.py @@ -85,7 +85,7 @@ async def publish( ) return version - except Exception: + except redis.asyncio.ResponseError: logger.exception("policy_publish_error") raise @@ -96,7 +96,7 @@ async def load_current(self) -> dict[str, Any] | None: if raw is None: return None return json.loads(raw) - except (json.JSONDecodeError, Exception): + except (json.JSONDecodeError, redis.asyncio.ResponseError): logger.exception("policy_load_error") return None @@ -105,7 +105,7 @@ async def get_version_history(self, count: int = 10) -> list[dict[str, Any]]: try: entries = await self._redis.lrange(POLICY_HISTORY_KEY, 0, count - 1) return [json.loads(e) for e in entries] - except Exception: + except redis.asyncio.ResponseError: logger.exception("policy_history_error") return [] @@ -127,7 +127,7 @@ async def rollback(self, target_version: str) -> bool: logger.info("policy_rollback version=%s", target_version) return True - except Exception: + except redis.asyncio.ResponseError: logger.exception("rollback_error") return False @@ -143,7 +143,7 @@ async def snapshot_version(self, version: str) -> None: versioned_key = f"freerelay:policy:versions:{version}" await self._redis.set(versioned_key, raw, ex=86400 * 7) # 7 day TTL logger.debug("policy_version_snapshot version=%s", version) - except Exception: + except redis.asyncio.ResponseError: logger.exception("snapshot_version_error") async def subscribe_to_updates(self, callback: Any) -> None: @@ -162,9 +162,9 @@ async def subscribe_to_updates(self, callback: Any) -> None: try: policy = json.loads(message["data"]) await callback(policy) - except Exception: + except (json.JSONDecodeError, ValueError, TypeError): logger.exception("policy_callback_error") - except Exception: + except redis.asyncio.ResponseError: logger.exception("policy_subscribe_error") finally: await pubsub.unsubscribe(POLICY_CHANNEL) @@ -177,6 +177,6 @@ async def delete_policy(self) -> bool: if deleted: logger.info("policy_deleted") return bool(deleted) - except Exception: + except redis.asyncio.ResponseError: logger.exception("policy_delete_error") return False diff --git a/freerelay/core/execution/executor.py b/freerelay/core/execution/executor.py index 085e053..83ad4b3 100644 --- a/freerelay/core/execution/executor.py +++ b/freerelay/core/execution/executor.py @@ -156,7 +156,7 @@ async def execute_hedged( for _name, circuit in circuits.items(): await circuit.record_success() return response - except Exception as e: + except (ProviderError, ValueError, TypeError) as e: # Record failure on all involved circuit breakers for _name, circuit in circuits.items(): status = e.status_code if isinstance(e, ProviderError) else None diff --git a/freerelay/core/intelligence/cache.py b/freerelay/core/intelligence/cache.py index 3cdc32a..060dbff 100644 --- a/freerelay/core/intelligence/cache.py +++ b/freerelay/core/intelligence/cache.py @@ -133,7 +133,7 @@ def lookup(self, request: ChatCompletionRequest) -> ChatCompletionResponse | Non if entry and (time.time() - entry.created_at) < entry.ttl: try: return ChatCompletionResponse.model_validate_json(entry.response_json) - except Exception: + except (ValueError, TypeError): del self._entries[key] return None @@ -153,9 +153,9 @@ def lookup(self, request: ChatCompletionRequest) -> ChatCompletionResponse | Non return ChatCompletionResponse.model_validate_json( candidate.response_json ) - except Exception: + except (ValueError, TypeError): continue - except Exception: + except (TypeError, ValueError, KeyError): pass return None diff --git a/freerelay/core/observability/supabase_logger.py b/freerelay/core/observability/supabase_logger.py index 73540aa..9ec7cf5 100644 --- a/freerelay/core/observability/supabase_logger.py +++ b/freerelay/core/observability/supabase_logger.py @@ -29,5 +29,5 @@ def log(self, record: OutcomeRecord) -> None: "schema_pass": record.schema_pass, "notes": record.notes }).execute() - except Exception as e: + except (OSError, ValueError, TypeError) as e: logger.error(f"Failed to log usage to Supabase: {e}") diff --git a/freerelay/core/routing/engine.py b/freerelay/core/routing/engine.py index 2a17456..aed44aa 100644 --- a/freerelay/core/routing/engine.py +++ b/freerelay/core/routing/engine.py @@ -163,7 +163,7 @@ def _load_capability_matrix(self, settings: Settings) -> CapabilityMatrix | None matrix = CapabilityMatrix.model_validate(data) logger.info("Loaded capability matrix: %d models", len(matrix.models)) return matrix - except Exception: + except (OSError, yaml.YAMLError, ValueError): logger.exception("Failed to load capability matrix: %s", path) return None diff --git a/freerelay/core/routing/policy.py b/freerelay/core/routing/policy.py index dadb661..9efb249 100644 --- a/freerelay/core/routing/policy.py +++ b/freerelay/core/routing/policy.py @@ -21,7 +21,7 @@ def _eval_condition_context(condition: str, context: dict[str, Any]) -> bool: return False try: return bool(eval(condition, {"__builtins__": {}}, context)) - except Exception: + except (SyntaxError, NameError, TypeError, ValueError): logger.warning("Failed to evaluate condition: %s", condition) return False diff --git a/freerelay/dashboard/ws.py b/freerelay/dashboard/ws.py index 2b30577..2074d0f 100644 --- a/freerelay/dashboard/ws.py +++ b/freerelay/dashboard/ws.py @@ -47,7 +47,7 @@ async def broadcast(self, data: dict[str, object]) -> None: for client in self._clients: try: await client.send_text(message) - except Exception: + except (RuntimeError, ConnectionResetError): disconnected.append(client) for client in disconnected: @@ -75,7 +75,7 @@ async def start_broadcasting(self, get_metrics: Any) -> None: "data": metrics, } ) - except Exception as e: + except (RuntimeError, ValueError) as e: logger.error("Metrics broadcast error: %s", e) await asyncio.sleep(self.interval) diff --git a/freerelay/data_plane/execution/cancellation.py b/freerelay/data_plane/execution/cancellation.py index 8df2515..1254879 100644 --- a/freerelay/data_plane/execution/cancellation.py +++ b/freerelay/data_plane/execution/cancellation.py @@ -14,6 +14,8 @@ from enum import StrEnum from typing import Any +import httpx + logger = logging.getLogger("freerelay.data_plane.cancellation") @@ -112,10 +114,11 @@ async def cancel( if self._httpx_request is not None and self._httpx_client is not None: try: await self._httpx_client.cancel_request(self._httpx_request) - except Exception: + except (OSError, httpx.RequestError) as err: logger.debug( - "Failed to cancel httpx request %s", + "Failed to cancel httpx request %s: %s", self._state.request_id, + err, ) # Cancel timeout task diff --git a/freerelay/data_plane/ingress/auth.py b/freerelay/data_plane/ingress/auth.py index ec90d0e..228ac41 100644 --- a/freerelay/data_plane/ingress/auth.py +++ b/freerelay/data_plane/ingress/auth.py @@ -14,6 +14,8 @@ from dataclasses import dataclass from typing import TYPE_CHECKING +import redis.asyncio + if TYPE_CHECKING: from redis.asyncio import Redis @@ -101,7 +103,7 @@ async def _lookup_redis(self, key_hash: str) -> str | None: result.decode("utf-8") if isinstance(result, bytes) else str(result) ) return None - except Exception: + except redis.asyncio.RedisError as err: logger.exception("Redis key lookup failed for hash %s", key_hash[:8]) return None diff --git a/freerelay/data_plane/ingress/idempotency.py b/freerelay/data_plane/ingress/idempotency.py index 45a1951..fb161e7 100644 --- a/freerelay/data_plane/ingress/idempotency.py +++ b/freerelay/data_plane/ingress/idempotency.py @@ -12,6 +12,8 @@ from dataclasses import dataclass from typing import TYPE_CHECKING, Any +import redis.asyncio + if TYPE_CHECKING: from redis.asyncio import Redis @@ -69,7 +71,7 @@ async def check(self, request_id: str) -> dict[str, Any] | None: if self._redis is not None: try: return await self._check_redis(request_id) - except Exception: + except redis.asyncio.RedisError: logger.exception("Redis idempotency check failed, using fallback") return self._check_memory(request_id) @@ -123,7 +125,7 @@ async def store(self, request_id: str, response: dict[str, Any]) -> bool: if self._redis is not None: try: return await self._store_redis(request_id, data) - except Exception: + except redis.asyncio.RedisError: logger.exception("Redis idempotency store failed, using fallback") return self._store_memory(request_id, data) diff --git a/freerelay/data_plane/ingress/rate_limit.py b/freerelay/data_plane/ingress/rate_limit.py index 500aa71..b4dbec9 100644 --- a/freerelay/data_plane/ingress/rate_limit.py +++ b/freerelay/data_plane/ingress/rate_limit.py @@ -12,6 +12,8 @@ from dataclasses import dataclass from typing import TYPE_CHECKING +import redis.asyncio + if TYPE_CHECKING: from redis.asyncio import Redis @@ -138,7 +140,7 @@ async def check_rate_limit( if self._redis is not None: try: return await self._check_redis(namespace, limit) - except Exception: + except redis.asyncio.RedisError: logger.exception("Redis rate limit check failed, using fallback") return self._fallback.check(namespace, limit, self._window) diff --git a/freerelay/main.py b/freerelay/main.py index 29c1e35..23f3423 100644 --- a/freerelay/main.py +++ b/freerelay/main.py @@ -94,7 +94,7 @@ def create_app() -> FastAPI: from fastapi.responses import ORJSONResponse default_response_class: type[Response] = ORJSONResponse - except Exception: + except ImportError: default_response_class = JSONResponse app = FastAPI( @@ -202,7 +202,7 @@ async def chat_completions(request: Request) -> Response: try: body = await request.body() - except Exception: + except OSError: return JSONResponse( status_code=400, content=ChatCompletionResponse.error_body("Invalid JSON body", 400), @@ -210,7 +210,7 @@ async def chat_completions(request: Request) -> Response: try: req = ChatCompletionRequest.model_validate_json(body) - except Exception as e: + except (ValueError, TypeError) as e: return JSONResponse( status_code=400, content=ChatCompletionResponse.error_body(f"Invalid request: {e}", 400), diff --git a/freerelay/middleware/auth.py b/freerelay/middleware/auth.py index 319d0e8..1539fff 100644 --- a/freerelay/middleware/auth.py +++ b/freerelay/middleware/auth.py @@ -67,7 +67,7 @@ def _verify_token_supabase(token_hash: str) -> dict[str, str] | None: _AUTH_CACHE[token_hash] = (user_info, now + _CACHE_TTL) return user_info return None - except Exception as e: + except (OSError, KeyError, TypeError, IndexError) as e: logger.error(f"Supabase auth error: {e}") return None diff --git a/freerelay/middleware/idempotency.py b/freerelay/middleware/idempotency.py index 40b21fc..4149604 100644 --- a/freerelay/middleware/idempotency.py +++ b/freerelay/middleware/idempotency.py @@ -102,7 +102,7 @@ async def dispatch( if len(self._cache) >= _MAX_CACHE_SIZE: self._cache.popitem(last=False) self._cache[idempotency_key] = (time.time(), body, response.status_code) - except Exception: + except (ValueError, TypeError): pass return response diff --git a/freerelay/providers/codex.py b/freerelay/providers/codex.py index 1789796..e513768 100644 --- a/freerelay/providers/codex.py +++ b/freerelay/providers/codex.py @@ -244,7 +244,7 @@ def get_codex_token_status() -> dict[str, object]: else "Token expired. Re-authenticate with 'openclaw configure'." ), } - except Exception: + except (TypeError, ValueError): pass return { diff --git a/freerelay/providers/opencode.py b/freerelay/providers/opencode.py index 73b1861..6bedbbf 100644 --- a/freerelay/providers/opencode.py +++ b/freerelay/providers/opencode.py @@ -166,7 +166,11 @@ async def fetch_opencode_models(api_key: str = "") -> list[dict[str, str]]: } ) return models - except Exception: + except httpx.RequestError: + pass + except (ValueError, TypeError): + pass + except AttributeError: pass # Fallback: return known free model diff --git a/freerelay/shared/security/crypto.py b/freerelay/shared/security/crypto.py index b225d8c..fab034c 100644 --- a/freerelay/shared/security/crypto.py +++ b/freerelay/shared/security/crypto.py @@ -68,7 +68,7 @@ def decrypt_key(ciphertext: str, secret: bytes) -> str: try: bundle = base64.b64decode(ciphertext) - except Exception as exc: + except (ValueError, TypeError) as exc: raise ValueError("Invalid base64 ciphertext") from exc if len(bundle) < 28: # 12 (nonce) + 16 (tag) minimum @@ -118,7 +118,7 @@ def _aes_gcm_decrypt(ciphertext: bytes, key: bytes, nonce: bytes, tag: bytes) -> "cryptography package required for AES-256-GCM. " "Install with: pip install cryptography" ) from e - except Exception as exc: + except ValueError as exc: raise ValueError("Decryption failed: invalid key or corrupted data") from exc diff --git a/freerelay/shared/tenancy/audit.py b/freerelay/shared/tenancy/audit.py index 3a247bd..c32f633 100644 --- a/freerelay/shared/tenancy/audit.py +++ b/freerelay/shared/tenancy/audit.py @@ -221,7 +221,7 @@ async def purge_expired(self, namespace: str) -> int: namespace, ) return len(ids) - except Exception as exc: + except redis.asyncio.ResponseError as exc: logger.error("Failed to purge audit records: %s", exc) return 0 @@ -252,7 +252,7 @@ async def _persist(self, namespace: str, record: AuditRecord) -> None: maxlen=100_000, # Cap stream size ) return - except Exception as exc: + except redis.asyncio.ResponseError as exc: logger.error("Redis audit write failed, falling back to memory: %s", exc) # In-memory fallback @@ -289,7 +289,7 @@ async def _get_from_redis( # Return newest first records.reverse() return records - except Exception as exc: + except redis.asyncio.ResponseError as exc: logger.error("Redis audit read failed: %s", exc) return []