Files
APIClient-Agent/app/core/http_client.py
Anand Shukla 01662f7e0e Initial release — APIClient - Agent v2.0.0
AI-first API testing desktop client built with Python + PyQt6.

Features:
- Multi-tab HTTP request editor with params/headers/body/auth/tests
- KeyValueTable with per-row enable/disable checkboxes and 36px rows
- Format JSON button, syntax highlighting, pre-request & test scripts
- Collections, environments, history, import/export (Postman v2.1, cURL)
- OpenAPI 3.x / Swagger 2.0 local parser (no AI tokens)
- EKIKA Odoo API Framework generator — JSON-API, REST JSON, GraphQL,
  Custom REST JSON with all auth types (instant, no AI tokens)
- Persistent AI chat sidebar (Claude-powered co-pilot) with streaming,
  context-aware suggestions, and one-click Apply to request editor
- AI collection generator from any docs URL or pasted spec
- WebSocket client, Mock server, Collection runner, Code generator
- Dark/light theme engine (global QSS, object-name selectors)
- SSL error detection with actionable hints
- MIT License

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 17:38:57 +05:30

162 lines
6.0 KiB
Python

"""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))