Skip to main content
Glama
http_client.py5.03 kB
"""HTTP client utilities shared across adapters. This module centralises retry/backoff behaviour, timeout defaults, and structured logging so that transport concerns stay consistent between adapters and higher level services. It is intentionally lightweight (requests-based) to avoid imposing additional dependencies on downstream environments. """ from __future__ import annotations import logging import random import time from collections.abc import Iterable, Mapping, MutableMapping from dataclasses import dataclass import requests logger = logging.getLogger("spice_mcp.http") @dataclass(frozen=True) class HttpClientConfig: """Configuration knobs for :class:`HttpClient`.""" timeout_seconds: float = 15.0 max_retries: int = 3 backoff_initial: float = 0.35 backoff_max: float = 5.0 jitter_range: tuple[float, float] = (1.25, 2.25) retry_statuses: tuple[int, ...] = (408, 409, 425, 429, 500, 502, 503, 504) class HttpClient: """Tiny wrapper over :mod:`requests` with consistent retry semantics.""" def __init__( self, config: HttpClientConfig, *, session: requests.Session | None = None, ) -> None: self._config = config self._session = session or requests.Session() # ------------------------------------------------------------------ public def request( self, method: str, url: str, *, headers: Mapping[str, str] | None = None, params: Mapping[str, object] | None = None, json: object | None = None, data: object | None = None, timeout: float | None = None, ok_statuses: Iterable[int] | None = None, ) -> requests.Response: """Perform an HTTP request with exponential backoff. ``ok_statuses`` may be provided to allow responses that would otherwise raise for status. Any HTTP exception that is not recoverable (non retryable status or retries exhausted) is re-raised to the caller. """ attempt = 0 backoff = self._config.backoff_initial timeout_value = timeout or self._config.timeout_seconds while True: start = time.perf_counter() try: response = self._session.request( method, url, headers=_clone_mapping(headers), params=params, json=json, data=data, timeout=timeout_value, ) except requests.RequestException as exc: # network/timeout failure logger.warning( "http_request_error", extra={ "method": method, "url": url, "attempt": attempt, "error": str(exc), }, ) if attempt >= self._config.max_retries: raise _sleep(backoff, self._config) backoff = min(self._config.backoff_max, backoff * 2) attempt += 1 continue duration_ms = int((time.perf_counter() - start) * 1000) status = response.status_code logger.debug( "http_request", extra={ "method": method, "url": url, "status": status, "attempt": attempt, "duration_ms": duration_ms, }, ) if _should_retry(status, attempt, self._config): _sleep(backoff, self._config) backoff = min(self._config.backoff_max, backoff * 2) attempt += 1 continue if ok_statuses and status in ok_statuses: return response try: response.raise_for_status() except requests.HTTPError as exc: # Non-OK terminal status logger.error( "http_request_failed", extra={ "method": method, "url": url, "status": status, "attempt": attempt, "error": str(exc), }, ) raise return response # --------------------------------------------------------------------------- def _should_retry(status: int, attempt: int, config: HttpClientConfig) -> bool: return ( status in config.retry_statuses and attempt < config.max_retries ) def _sleep(backoff: float, config: HttpClientConfig) -> None: jitter = random.uniform(*config.jitter_range) time.sleep(backoff * jitter) def _clone_mapping(mapping: Mapping[str, str] | None) -> MutableMapping[str, str] | None: if mapping is None: return None return dict(mapping)

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Evan-Kim2028/spice-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server