baseUrl) ->withHeaders($this->buildHeaders()) ->timeout(30) ->withOptions([ 'verify' => config('dhl.ssl.verify_peer', true), // SSL certificate verification 'http_errors' => false, // Don't throw exceptions on HTTP error codes 'curl' => [ CURLOPT_SSL_VERIFYPEER => config('dhl.ssl.verify_peer', true), CURLOPT_SSL_VERIFYHOST => config('dhl.ssl.verify_host', true) ? 2 : 0, CURLOPT_SSLVERSION => $this->getSslVersion(), CURLOPT_FOLLOWLOCATION => true, CURLOPT_MAXREDIRS => 5, CURLOPT_CONNECTTIMEOUT => config('dhl.ssl.connect_timeout', 10), CURLOPT_TIMEOUT => config('dhl.ssl.timeout', 30), CURLOPT_USERAGENT => 'acme-laravel-dhl/1.0', ], ]) ->retry(3, 300, function ($exception, $attempt) { if ($exception instanceof RequestException && $exception->response->status() === 429) { $delay = min(1000000 * $attempt, 10000000); // Max 10 seconds usleep($delay); // Microseconds return true; } return $exception instanceof ConnectionException || ($exception instanceof RequestException && in_array($exception->response->status(), [500, 502, 503, 504])); }, false); // Add authentication if provided if ($this->username && $this->password) { $request = $request->withBasicAuth($this->username, $this->password); } // Make the request $response = match (strtolower($method)) { 'get' => $request->get($uri, $query), 'post' => $request->post($uri.'?'.http_build_query($query), $payload), 'put' => $request->put($uri, $payload), 'delete' => $request->delete($uri), default => throw new Exception("Unsupported HTTP method: {$method}") }; // Handle response if ($response->failed()) { // Log additional debug info for 400 errors if ($response->status() === 400) { $this->getDhlLogger()->error('[DHL API] HTTP 400 Bad Request Details', [ 'method' => $method, 'uri' => $uri, 'request_payload' => $payload, 'response_body' => $response->body(), 'response_json' => $response->json(), 'status_code' => $response->status(), 'headers' => $response->headers(), ]); } $this->handleErrorResponse($response, $method, $uri); } return $response->json() ?? []; } catch (RequestException $e) { $this->getDhlLogger()->error('DHL API request failed', ['error' => $e->getMessage(), 'method' => $method, 'uri' => $uri]); throw new DhlApiException("DHL API request failed: {$e->getMessage()}", $e->getCode(), $e); } catch (Exception $e) { $this->getDhlLogger()->error('DHL API error', ['error' => $e->getMessage()]); // Re-throw our own exceptions if ($e instanceof DhlApiException) { throw $e; } throw new DhlApiException("DHL API error: {$e->getMessage()}", $e->getCode(), $e); } } /** * Build HTTP headers for DHL API requests */ private function buildHeaders(): array { $headers = [ 'Accept' => 'application/json', 'Content-Type' => 'application/json', 'User-Agent' => 'acme-laravel-dhl/1.0', ]; if ($this->apiKey) { $headers['dhl-api-key'] = $this->apiKey; } return $headers; } /** * Handle error responses from DHL API */ private function handleErrorResponse($response, string $method, string $uri): void { $status = $response->status(); $body = $response->json(); $errorMessage = $this->extractErrorMessage($body) ?? "HTTP {$status} error"; match (true) { $status === 401 => throw new DhlAuthenticationException("DHL API authentication failed: {$errorMessage}"), $status === 403 => throw new DhlAuthenticationException("DHL API access forbidden: {$errorMessage}"), $status === 404 => throw new DhlApiException("DHL API endpoint not found: {$method} {$uri}"), $status === 422 => throw new DhlValidationException("DHL API validation error: {$errorMessage}"), $status === 429 => throw new DhlApiException('DHL API rate limit exceeded. Please try again later.'), $status >= 500 => throw new DhlApiException("DHL API server error: {$errorMessage}"), default => throw new DhlApiException("DHL API error ({$status}): {$errorMessage}") }; } /** * Extract error message from DHL API response * * DHL API v2 returns errors in various formats: * - {status: {detail: "..."}} * - {items: [{sstatus: {detail: "..."}}]} * - {items: [{validationMessages: [{...}]}]} */ private function extractErrorMessage(?array $body): ?string { if (! $body) { return null; } // Try different possible error message fields for DHL API v2 $message = $body['message'] ?? $body['error'] ?? $body['detail'] ?? data_get($body, 'status.detail') ?? data_get($body, 'status.title') ?? data_get($body, 'errors.0.message') ?? data_get($body, 'error.message') ?? data_get($body, 'items.0.sstatus.detail') ?? data_get($body, 'items.0.sstatus.title') ?? null; // Check for validation messages in items if (! $message && isset($body['items'][0]['validationMessages'])) { $validationMessages = $body['items'][0]['validationMessages']; if (is_array($validationMessages) && ! empty($validationMessages)) { $messages = []; foreach ($validationMessages as $vm) { $vmMessage = $vm['validationMessage'] ?? $vm['message'] ?? $vm['property'] ?? null; if ($vmMessage) { $messages[] = $vmMessage; } } if (! empty($messages)) { $message = implode('; ', $messages); } } } return $message; } /** * Get SSL version constant based on configuration */ private function getSslVersion(): int { $sslVersion = config('dhl.ssl.ssl_version', 'TLSv1_2'); return match ($sslVersion) { 'TLSv1_0' => CURL_SSLVERSION_TLSv1_0, 'TLSv1_1' => CURL_SSLVERSION_TLSv1_1, 'TLSv1_2' => CURL_SSLVERSION_TLSv1_2, 'TLSv1_3' => defined('CURL_SSLVERSION_TLSv1_3') ? CURL_SSLVERSION_TLSv1_3 : CURL_SSLVERSION_TLSv1_2, default => CURL_SSLVERSION_TLSv1_2, }; } /** * Log detailed server environment information for debugging */ public function logServerEnvironment(): void { $info = [ 'php_version' => PHP_VERSION, 'curl_version' => curl_version(), 'openssl_version' => OPENSSL_VERSION_TEXT ?? 'Unknown', 'server_software' => $_SERVER['SERVER_SOFTWARE'] ?? 'Unknown', 'dhl_config' => [ 'base_url' => $this->baseUrl, 'has_api_key' => ! empty($this->apiKey), 'has_username' => ! empty($this->username), 'has_password' => ! empty($this->password), 'ssl_verify_peer' => config('dhl.ssl.verify_peer', true), 'ssl_verify_host' => config('dhl.ssl.verify_host', true), 'ssl_version' => config('dhl.ssl.ssl_version', 'TLSv1_2'), 'timeout' => config('dhl.ssl.timeout', 30), 'connect_timeout' => config('dhl.ssl.connect_timeout', 10), ], 'environment' => [ 'APP_ENV' => config('app.env'), 'APP_DEBUG' => config('app.debug'), 'APP_URL' => config('app.url'), ], ]; $this->getDhlLogger()->info('DHL Server Environment Debug Info', $info); } /** * Test connection to DHL API * * @return bool True if connection successful */ public function testConnection(): bool { // Log server environment for debugging $this->logServerEnvironment(); // Try multiple connection methods for better compatibility $methods = [ 'method1' => 'Laravel HTTP with enhanced SSL', 'method2' => 'Laravel HTTP with relaxed SSL', 'method3' => 'Direct cURL fallback', ]; foreach ($methods as $methodKey => $methodName) { try { $this->getDhlLogger()->info("DHL API connection test - trying {$methodName}", [ 'method' => $methodKey, 'base_url' => $this->baseUrl, ]); $success = $this->testConnectionWithMethod($methodKey); if ($success) { $this->getDhlLogger()->info("DHL API connection test successful with {$methodName}", [ 'method' => $methodKey, 'base_url' => $this->baseUrl, ]); return true; } } catch (Exception $e) { $this->getDhlLogger()->warning("DHL API connection test failed with {$methodName}", [ 'method' => $methodKey, 'error' => $e->getMessage(), 'base_url' => $this->baseUrl, ]); } } $this->getDhlLogger()->error('DHL API connection test failed with all methods', [ 'base_url' => $this->baseUrl, 'tried_methods' => array_keys($methods), ]); return false; } /** * Test connection with specific method */ private function testConnectionWithMethod(string $method): bool { switch ($method) { case 'method1': return $this->testConnectionEnhanced(); case 'method2': return $this->testConnectionRelaxed(); case 'method3': return $this->testConnectionCurl(); default: return false; } } /** * Enhanced SSL connection test */ private function testConnectionEnhanced(): bool { // Detect cURL version for compatibility $curlVersion = curl_version(); $isOldCurl = version_compare($curlVersion['version'], '8.0.0', '<'); $curlOptions = [ CURLOPT_SSL_VERIFYPEER => config('dhl.ssl.verify_peer', true), CURLOPT_SSL_VERIFYHOST => config('dhl.ssl.verify_host', true) ? 2 : 0, CURLOPT_SSLVERSION => $this->getSslVersion(), CURLOPT_FOLLOWLOCATION => true, CURLOPT_MAXREDIRS => 5, CURLOPT_CONNECTTIMEOUT => config('dhl.ssl.connect_timeout', 10), CURLOPT_TIMEOUT => config('dhl.ssl.connect_timeout', 10), CURLOPT_USERAGENT => 'acme-laravel-dhl/1.0', CURLOPT_VERBOSE => true, // Enable verbose output CURLOPT_STDERR => fopen('php://temp', 'w+'), // Capture verbose output ]; // Only use HTTP/2 for newer cURL versions if (! $isOldCurl && defined('CURL_HTTP_VERSION_2_0')) { $curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_2_0; } else { $curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_1; } // Additional options for older cURL versions if ($isOldCurl) { $curlOptions[CURLOPT_TCP_NODELAY] = true; $curlOptions[CURLOPT_TCP_KEEPALIVE] = 1; $curlOptions[CURLOPT_TCP_KEEPIDLE] = 10; $curlOptions[CURLOPT_TCP_KEEPINTVL] = 1; } // Log complete cURL request details $this->getDhlLogger()->info('DHL Enhanced Connection Test - Complete Request Details', [ 'method' => 'Enhanced SSL', 'url' => $this->baseUrl.'/', 'headers' => $this->buildHeaders(), 'auth' => [ 'username' => $this->username, 'password' => '***hidden***', ], 'curl_options' => $this->formatCurlOptions($curlOptions), 'timeout' => 10, 'ssl_config' => [ 'verify_peer' => config('dhl.ssl.verify_peer', true), 'verify_host' => config('dhl.ssl.verify_host', true), 'ssl_version' => config('dhl.ssl.ssl_version', 'TLSv1_2'), ], ]); $response = Http::baseUrl($this->baseUrl) ->withHeaders($this->buildHeaders()) ->withBasicAuth($this->username, $this->password) ->timeout(10) ->withOptions([ 'verify' => config('dhl.ssl.verify_peer', true), 'http_errors' => false, 'curl' => $curlOptions, ]) ->get('/'); // Log response details $this->getDhlLogger()->info('DHL Enhanced Connection Test - Response Details', [ 'status_code' => $response->status(), 'headers' => $response->headers(), 'body' => $response->body(), 'success' => $this->validateResponse($response), ]); return $this->validateResponse($response); } /** * Relaxed SSL connection test (fallback) */ private function testConnectionRelaxed(): bool { // Detect cURL version for compatibility $curlVersion = curl_version(); $isOldCurl = version_compare($curlVersion['version'], '8.0.0', '<'); $curlOptions = [ CURLOPT_SSL_VERIFYPEER => false, CURLOPT_SSL_VERIFYHOST => false, CURLOPT_SSLVERSION => CURL_SSLVERSION_DEFAULT, CURLOPT_FOLLOWLOCATION => true, CURLOPT_MAXREDIRS => 5, CURLOPT_CONNECTTIMEOUT => 15, CURLOPT_TIMEOUT => 15, CURLOPT_USERAGENT => 'acme-laravel-dhl/1.0', CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, // Force HTTP/1.1 CURLOPT_VERBOSE => true, // Enable verbose output CURLOPT_STDERR => fopen('php://temp', 'w+'), // Capture verbose output ]; // Additional options for older cURL versions if ($isOldCurl) { $curlOptions[CURLOPT_TCP_NODELAY] = true; $curlOptions[CURLOPT_TCP_KEEPALIVE] = 1; $curlOptions[CURLOPT_TCP_KEEPIDLE] = 10; $curlOptions[CURLOPT_TCP_KEEPINTVL] = 1; $curlOptions[CURLOPT_FRESH_CONNECT] = true; $curlOptions[CURLOPT_FORBID_REUSE] = true; } // Log complete cURL request details $this->getDhlLogger()->info('DHL Relaxed Connection Test - Complete Request Details', [ 'method' => 'Relaxed SSL', 'url' => $this->baseUrl.'/', 'headers' => $this->buildHeaders(), 'auth' => [ 'username' => $this->username, 'password' => '***hidden***', ], 'curl_options' => $this->formatCurlOptions($curlOptions), 'timeout' => 15, 'ssl_config' => [ 'verify_peer' => false, 'verify_host' => false, 'ssl_version' => 'DEFAULT', ], ]); $response = Http::baseUrl($this->baseUrl) ->withHeaders($this->buildHeaders()) ->withBasicAuth($this->username, $this->password) ->timeout(15) ->withOptions([ 'verify' => false, // Disable SSL verification as fallback 'http_errors' => false, 'curl' => $curlOptions, ]) ->get('/'); // Log response details $this->getDhlLogger()->info('DHL Relaxed Connection Test - Response Details', [ 'status_code' => $response->status(), 'headers' => $response->headers(), 'body' => $response->body(), 'success' => $this->validateResponse($response), ]); return $this->validateResponse($response); } /** * Direct cURL connection test (last resort) */ private function testConnectionCurl(): bool { $ch = curl_init(); $curlOptions = [ CURLOPT_URL => $this->baseUrl.'/', CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 15, CURLOPT_CONNECTTIMEOUT => 15, CURLOPT_HTTPHEADER => [ 'Accept: application/json', 'User-Agent: acme-laravel-dhl/1.0', ], CURLOPT_HTTPAUTH => CURLAUTH_BASIC, CURLOPT_USERPWD => $this->username.':'.$this->password, CURLOPT_SSL_VERIFYPEER => config('dhl.ssl.verify_peer', true), CURLOPT_SSL_VERIFYHOST => config('dhl.ssl.verify_host', true) ? 2 : 0, CURLOPT_SSLVERSION => $this->getSslVersion(), CURLOPT_FOLLOWLOCATION => true, CURLOPT_MAXREDIRS => 5, CURLOPT_VERBOSE => true, CURLOPT_STDERR => fopen('php://temp', 'w+'), ]; // Log complete cURL request details $this->getDhlLogger()->info('DHL Direct cURL Connection Test - Complete Request Details', [ 'method' => 'Direct cURL', 'url' => $this->baseUrl.'/', 'headers' => [ 'Accept: application/json', 'User-Agent: acme-laravel-dhl/1.0', ], 'auth' => [ 'username' => $this->username, 'password' => '***hidden***', 'auth_type' => 'CURLAUTH_BASIC', ], 'curl_options' => $this->formatCurlOptions($curlOptions), 'timeout' => 15, 'ssl_config' => [ 'verify_peer' => config('dhl.ssl.verify_peer', true), 'verify_host' => config('dhl.ssl.verify_host', true), 'ssl_version' => config('dhl.ssl.ssl_version', 'TLSv1_2'), ], ]); curl_setopt_array($ch, $curlOptions); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $error = curl_error($ch); $curlInfo = curl_getinfo($ch); // Get verbose output rewind($curlOptions[CURLOPT_STDERR]); $verboseOutput = stream_get_contents($curlOptions[CURLOPT_STDERR]); fclose($curlOptions[CURLOPT_STDERR]); curl_close($ch); // Log response details $this->getDhlLogger()->info('DHL Direct cURL Connection Test - Response Details', [ 'status_code' => $httpCode, 'response_body' => $response, 'curl_info' => $curlInfo, 'verbose_output' => $verboseOutput, 'curl_error' => $error, 'success' => $httpCode >= 200 && $httpCode < 500, ]); if ($error) { throw new Exception("cURL error: {$error}"); } // For cURL, we check HTTP status codes return $httpCode >= 200 && $httpCode < 500; // Accept 2xx, 3xx, 4xx (but not 5xx) } /** * Format cURL options for logging */ private function formatCurlOptions(array $curlOptions): array { $formatted = []; foreach ($curlOptions as $option => $value) { $optionName = $this->getCurlOptionName($option); // Hide sensitive values if (in_array($option, [CURLOPT_USERPWD, CURLOPT_STDERR])) { $formatted[$optionName] = '***hidden***'; } elseif (is_resource($value)) { $formatted[$optionName] = 'resource'; } elseif (is_array($value)) { $formatted[$optionName] = $value; } else { $formatted[$optionName] = $value; } } return $formatted; } /** * Get human-readable cURL option name */ private function getCurlOptionName(int $option): string { $optionNames = [ CURLOPT_URL => 'CURLOPT_URL', CURLOPT_RETURNTRANSFER => 'CURLOPT_RETURNTRANSFER', CURLOPT_TIMEOUT => 'CURLOPT_TIMEOUT', CURLOPT_CONNECTTIMEOUT => 'CURLOPT_CONNECTTIMEOUT', CURLOPT_HTTPHEADER => 'CURLOPT_HTTPHEADER', CURLOPT_HTTPAUTH => 'CURLOPT_HTTPAUTH', CURLOPT_USERPWD => 'CURLOPT_USERPWD', CURLOPT_SSL_VERIFYPEER => 'CURLOPT_SSL_VERIFYPEER', CURLOPT_SSL_VERIFYHOST => 'CURLOPT_SSL_VERIFYHOST', CURLOPT_SSLVERSION => 'CURLOPT_SSLVERSION', CURLOPT_FOLLOWLOCATION => 'CURLOPT_FOLLOWLOCATION', CURLOPT_MAXREDIRS => 'CURLOPT_MAXREDIRS', CURLOPT_VERBOSE => 'CURLOPT_VERBOSE', CURLOPT_STDERR => 'CURLOPT_STDERR', CURLOPT_USERAGENT => 'CURLOPT_USERAGENT', CURLOPT_HTTP_VERSION => 'CURLOPT_HTTP_VERSION', CURLOPT_TCP_NODELAY => 'CURLOPT_TCP_NODELAY', CURLOPT_TCP_KEEPALIVE => 'CURLOPT_TCP_KEEPALIVE', CURLOPT_TCP_KEEPIDLE => 'CURLOPT_TCP_KEEPIDLE', CURLOPT_TCP_KEEPINTVL => 'CURLOPT_TCP_KEEPINTVL', CURLOPT_FRESH_CONNECT => 'CURLOPT_FRESH_CONNECT', CURLOPT_FORBID_REUSE => 'CURLOPT_FORBID_REUSE', ]; return $optionNames[$option] ?? "CURLOPT_UNKNOWN_{$option}"; } /** * Validate HTTP response for connection test */ private function validateResponse($response): bool { if ($response->status() === 401) { $this->getDhlLogger()->error('DHL API authentication failed: Invalid username/password'); return false; } if ($response->status() === 403 && str_contains($response->body(), 'api-key')) { $this->getDhlLogger()->error('DHL API authentication failed: Invalid API key'); return false; } // Any other response code (including 404, 403 for endpoint access) means connection works return true; } }