// Copyright 2025 Woodpecker Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package httputil import ( "context" "crypto/x509" "errors" "fmt" "net" "net/url" "os" "strings" "syscall" ) // EnhanceHTTPError adds detailed context to HTTP errors to help with debugging. func EnhanceHTTPError(err error, method, endpoint string) error { if err == nil { return nil } // parse url to get host information parsedURL, parseErr := url.Parse(endpoint) var host string if parseErr == nil { host = parsedURL.Host } else { host = endpoint } // base error message baseMsg := fmt.Sprintf("%s %q", method, endpoint) // check for context errors // timeout if errors.Is(err, context.DeadlineExceeded) { if strings.Contains(err.Error(), "Client.Timeout") { return fmt.Errorf("connection timeout: %s: %w (the remote server at %s did not respond within the configured timeout)", baseMsg, err, host) } return fmt.Errorf("request timeout: %s: %w (operation took too long time)", baseMsg, err) } // cancellation if errors.Is(err, context.Canceled) { return fmt.Errorf("request canceled: %s: %w (the operation was canceled before completion)", baseMsg, err) } // check for net package errors // dns error handling var dnsErr *net.DNSError if errors.As(err, &dnsErr) { if dnsErr.IsNotFound { return fmt.Errorf("DNS resolution failed: %s: %w (hostname %s does not exist or cannot be resolved)", baseMsg, err, host) } if dnsErr.IsTimeout { return fmt.Errorf("DNS timeout: %s: %w (DNS server did not respond in time)", baseMsg, err) } return fmt.Errorf("DNS error: %s: %w", baseMsg, err) } // op error handling var opErr *net.OpError if errors.As(err, &opErr) { // connection refused if errors.Is(opErr.Err, syscall.ECONNREFUSED) { return fmt.Errorf("connection refused: %s: %w (server at %s is not accepting connections - is it running?)", baseMsg, err, host) } // connection reset if errors.Is(opErr.Err, syscall.ECONNRESET) { return fmt.Errorf("connection reset: %s: %w (server at %s closed the connection unexpectedly)", baseMsg, err, host) } // network unreachable if errors.Is(opErr.Err, syscall.ENETUNREACH) { return fmt.Errorf("network unreachable: %s: %w (cannot reach %s - check network connectivity)", baseMsg, err, host) } // host unreachable if errors.Is(opErr.Err, syscall.EHOSTUNREACH) { return fmt.Errorf("host unreachable: %s: %w (cannot reach %s - host may be down or firewall blocking)", baseMsg, err, host) } // timeout during operation if opErr.Timeout() { return fmt.Errorf("network timeout during %s: %s: %w (operation at %s took too long)", opErr.Op, baseMsg, err, host) } return fmt.Errorf("network error during %s: %s: %w", opErr.Op, baseMsg, err) } // check for url parsing errors var urlErr *url.Error if errors.As(err, &urlErr) { return fmt.Errorf("URL error: %s: %w (check if the endpoint URL is correctly formatted)", baseMsg, err) } // check for TLS/certificate errors var certErr *x509.CertificateInvalidError if errors.As(err, &certErr) { return fmt.Errorf("TLS certificate invalid: %s: %w (certificate validation failed for %s)", baseMsg, err, host) } var unknownAuthErr *x509.UnknownAuthorityError if errors.As(err, &unknownAuthErr) { return fmt.Errorf("TLS certificate verification failed: %s: %w (certificate signed by unknown authority for %s)", baseMsg, err, host) } var hostErr *x509.HostnameError if errors.As(err, &hostErr) { return fmt.Errorf("TLS hostname mismatch: %s: %w (certificate is not valid for %s)", baseMsg, err, host) } // check for os errors if errors.Is(err, os.ErrInvalid) { return fmt.Errorf("invalid argument: %s: %w", baseMsg, err) } if errors.Is(err, os.ErrPermission) { return fmt.Errorf("permission denied: %s: %w", baseMsg, err) } if errors.Is(err, os.ErrExist) { return fmt.Errorf("file already exists: %s: %w", baseMsg, err) } if errors.Is(err, os.ErrNotExist) { return fmt.Errorf("file does not exist: %s: %w", baseMsg, err) } if errors.Is(err, os.ErrClosed) { return fmt.Errorf("file already closed: %s: %w", baseMsg, err) } if errors.Is(err, os.ErrNoDeadline) { return fmt.Errorf("file type does not support deadline: %s: %w", baseMsg, err) } if errors.Is(err, os.ErrDeadlineExceeded) { return fmt.Errorf("i/o timeout: %s: %w", baseMsg, err) } // check for EOF specifically if err.Error() == "EOF" || strings.Contains(err.Error(), "EOF") { return fmt.Errorf("unexpected connection closure: %s: %w (server at %s closed connection prematurely - possible causes: server crash, request too large, incompatible protocol, or server-side timeout)", baseMsg, err, host) } // check for "connection reset by peer" if strings.Contains(err.Error(), "connection reset by peer") { return fmt.Errorf("connection reset by peer: %s: %w (server at %s forcibly closed the connection)", baseMsg, err, host) } // generic error return fmt.Errorf("HTTP request failed: %s: %w", baseMsg, err) }