"""APIClient - Agent — HTTP client engine.""" import re import base64 from copy import deepcopy import httpx from app.models import HttpRequest, HttpResponse def resolve_variables(text: str, variables: dict) -> str: """Replace {{variable}} placeholders with environment values.""" if not text or not variables: return text def replacer(m): key = m.group(1).strip() return str(variables.get(key, m.group(0))) return re.sub(r"\{\{(.+?)\}\}", replacer, text) def apply_variables(req: HttpRequest, variables: dict) -> HttpRequest: """Return a deep copy of the request with all variables resolved.""" r = deepcopy(req) r.url = resolve_variables(r.url, variables) r.body = resolve_variables(r.body, variables) r.headers = {k: resolve_variables(v, variables) for k, v in r.headers.items()} r.params = {k: resolve_variables(v, variables) for k, v in r.params.items()} if r.auth_data: r.auth_data = {k: resolve_variables(str(v), variables) for k, v in r.auth_data.items()} return r def _build_auth_headers(req: HttpRequest) -> dict: headers = {} if req.auth_type == "bearer": token = req.auth_data.get("token", "") if token: headers["Authorization"] = f"Bearer {token}" elif req.auth_type == "basic": user = req.auth_data.get("username", "") pwd = req.auth_data.get("password", "") encoded = base64.b64encode(f"{user}:{pwd}".encode()).decode() headers["Authorization"] = f"Basic {encoded}" elif req.auth_type == "apikey": key = req.auth_data.get("key", "") value = req.auth_data.get("value", "") location = req.auth_data.get("in", "header") if location == "header" and key: headers[key] = value return headers def send_request(req: HttpRequest, variables: dict = None) -> HttpResponse: r = req # will be overwritten with resolved copy; kept here for exception handlers try: r = apply_variables(req, variables or {}) # Check for unresolved variables unresolved = re.findall(r"\{\{(.+?)\}\}", r.url) if unresolved: return HttpResponse( error=f"Unresolved variable(s): {', '.join(unresolved)}. " "Go to Tools → Environments to define them." ) headers = {**r.headers, **_build_auth_headers(r)} # Query params (merge URL params dict + API-key-in-query) params = r.params.copy() if r.auth_type == "apikey" and r.auth_data.get("in") == "query": params[r.auth_data.get("key", "")] = r.auth_data.get("value", "") # Build request body content = None data = None files = None if r.body_type == "raw" and r.body: content = r.body.encode() # Auto Content-Type: honour explicit override, then try to detect JSON if r.content_type: headers.setdefault("Content-Type", r.content_type) elif "Content-Type" not in headers: stripped = r.body.lstrip() if stripped.startswith(("{", "[")): headers["Content-Type"] = "application/json" else: headers["Content-Type"] = "text/plain" elif r.body_type == "urlencoded" and r.body: pairs = {} for line in r.body.splitlines(): if "=" in line: k, _, v = line.partition("=") pairs[k.strip()] = v.strip() data = pairs elif r.body_type == "form-data" and r.body: # Expect "key=value" lines; values starting with "@" treated as file paths pairs = {} for line in r.body.splitlines(): if "=" in line: k, _, v = line.partition("=") pairs[k.strip()] = v.strip() data = pairs timeout = httpx.Timeout( connect=10.0, read=float(r.timeout), write=float(r.timeout), pool=5.0, ) with httpx.Client( follow_redirects=True, timeout=timeout, verify=r.ssl_verify, ) as client: response = client.request( method=r.method, url=r.url, headers=headers, params=params or None, content=content, data=data, files=files, ) body = response.text size_bytes = len(response.content) return HttpResponse( status=response.status_code, reason=response.reason_phrase, headers=dict(response.headers), body=body, elapsed_ms=response.elapsed.total_seconds() * 1000, size_bytes=size_bytes, ) except httpx.InvalidURL: return HttpResponse(error=f"Invalid URL: {r.url}") except httpx.ConnectError as e: detail = str(e) if "CERTIFICATE_VERIFY_FAILED" in detail or "certificate" in detail.lower() or "SSL" in detail: return HttpResponse(error=( f"SSL certificate error — could not connect to {r.url}\n\n" f"The server's certificate is not trusted or doesn't match the hostname.\n" f"Tip: disable SSL verification in the request Settings tab." )) return HttpResponse(error=f"Connection refused — could not reach {r.url}") except httpx.ConnectTimeout: return HttpResponse(error=f"Connection timed out after {req.timeout}s") except httpx.ReadTimeout: return HttpResponse(error=f"Read timed out — server took too long to respond") except httpx.SSLError as e: return HttpResponse(error=f"SSL error: {e}. Disable SSL verification if using a self-signed cert.") except httpx.TooManyRedirects: return HttpResponse(error="Too many redirects — possible redirect loop") except Exception as e: return HttpResponse(error=str(e))