# http_client.py # # Copyright 2025 Pavel Baksy # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # SPDX-License-Identifier: GPL-3.0-or-later import subprocess import time from typing import Tuple, Optional from .models import HttpRequest, HttpResponse class HttpClient: """HTTP client using HTTPie as backend.""" def execute_request(self, request: HttpRequest) -> Tuple[Optional[HttpResponse], Optional[str]]: """ Execute HTTP request using HTTPie. Args: request: HttpRequest object with method, url, headers, body Returns: Tuple of (response, error_message) If successful: (HttpResponse, None) If failed: (None, error_message) """ # Build command cmd = ['http', '--print=HhBb', request.method, request.url] # Add headers for key, value in request.headers.items(): if key and value: # Skip empty headers cmd.append(f'{key}:{value}') # Prepare stdin for body (if POST/PUT/DELETE and body exists) stdin_data = None if request.method in ['POST', 'PUT', 'DELETE'] and request.body.strip(): stdin_data = request.body.encode('utf-8') else: # Only use --ignore-stdin when there's no body cmd.insert(2, '--ignore-stdin') # Execute with timing start_time = time.time() try: result = subprocess.run( cmd, input=stdin_data, capture_output=True, timeout=30, # 30 second timeout text=False # We'll decode manually ) end_time = time.time() response_time = (end_time - start_time) * 1000 # Convert to ms # Parse output output = result.stdout.decode('utf-8', errors='replace') if result.returncode != 0: error_msg = result.stderr.decode('utf-8', errors='replace') return None, error_msg # Parse HTTPie output response = self._parse_httpie_output(output, response_time) return response, None except subprocess.TimeoutExpired: return None, "Request timeout (30 seconds)" except FileNotFoundError: return None, "HTTPie not installed. Please install with: pip install httpie" except Exception as e: return None, f"Error: {str(e)}" def _parse_httpie_output(self, output: str, response_time: float) -> HttpResponse: """ Parse HTTPie --print=HhBb output format. Format: REQUEST HEADERS REQUEST BODY RESPONSE HEADERS (starts with HTTP/1.1 200 OK) RESPONSE BODY """ # Split output into lines to manually find response section lines = output.split('\n') # Find where response headers start (line starting with HTTP/) response_header_start = 0 for i, line in enumerate(lines): if line.startswith('HTTP/'): response_header_start = i break # Parse status line status_line = lines[response_header_start] parts = status_line.split(' ', 2) if len(parts) < 2: raise ValueError(f"Invalid status line: {status_line}") status_code = int(parts[1]) status_text = parts[2] if len(parts) > 2 else '' # Find where response headers end (first empty line after response starts) response_body_start = response_header_start + 1 for i in range(response_header_start + 1, len(lines)): if lines[i].strip() == '': response_body_start = i + 1 break # Extract response headers (from HTTP/ line to first blank line) response_headers = '\n'.join(lines[response_header_start:response_body_start - 1]) # Extract response body (everything after the blank line) response_body = '\n'.join(lines[response_body_start:]) return HttpResponse( status_code=status_code, status_text=status_text, headers=response_headers, body=response_body.strip(), response_time_ms=response_time )