From 6e47cb597f68cddd040b7be7ef080c6a426b4eaf Mon Sep 17 00:00:00 2001 From: A <258483684+la14-1@users.noreply.github.com> Date: Mon, 9 Feb 2026 03:57:40 -0800 Subject: [PATCH] refactor: Extract generic_cloud_api retry logic into helper functions (#78) Split the 66-line generic_cloud_api function into focused helpers to reduce complexity and eliminate duplication: - _parse_api_response: Extracts HTTP code and response body (10 lines) - _make_api_request: Builds curl args and executes request (27 lines) - _handle_api_transient_error: Centralizes retry logic for all error types (24 lines) Main function reduced from 66 to 41 lines (38% reduction). Behavior unchanged: still retries on network errors and transient HTTP codes (429, 503), with exponential backoff. All test assertions pass. This extraction pattern makes it clearer how retry logic flows and easier to modify error handling in the future without duplicating patterns. Co-authored-by: A <6723574+louisgv@users.noreply.github.com> Co-authored-by: Claude Haiku 4.5 --- shared/common.sh | 113 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 80 insertions(+), 33 deletions(-) diff --git a/shared/common.sh b/shared/common.sh index 80a61042..93d09107 100644 --- a/shared/common.sh +++ b/shared/common.sh @@ -995,6 +995,78 @@ _update_retry_interval() { eval "${interval_var}=${current_interval}" } +# Helper to extract HTTP status code and response body from curl output +# Curl is called with "-w \n%{http_code}" so last line is the code +# Returns: http_code on stdout, response_body via global variable +_parse_api_response() { + local response="${1}" + local http_code + http_code=$(echo "${response}" | tail -1) + local response_body + response_body=$(echo "${response}" | head -n -1) + + API_HTTP_CODE="${http_code}" + API_RESPONSE_BODY="${response_body}" +} + +# Helper to handle a single API request attempt - builds curl args and executes it +# Returns: 0 on curl success, 1 on curl failure +# Sets: API_HTTP_CODE and API_RESPONSE_BODY globals +_make_api_request() { + local base_url="${1}" + local auth_token="${2}" + local method="${3}" + local endpoint="${4}" + local body="${5:-}" + + local args=( + -s + -w "\n%{http_code}" + -X "${method}" + -H "Authorization: Bearer ${auth_token}" + -H "Content-Type: application/json" + ) + + if [[ -n "${body}" ]]; then + args+=(-d "${body}") + fi + + local response + response=$(curl "${args[@]}" "${base_url}${endpoint}" 2>&1) + local curl_exit_code=$? + + _parse_api_response "${response}" + + return ${curl_exit_code} +} + +# Helper to handle transient API error and decide whether to retry +# Returns: 0 to continue, 1 to fail (response already echoed) +_handle_api_transient_error() { + local error_type="${1}" # "network" or HTTP_CODE like "429" + local attempt="${2}" + local max_retries="${3}" + local interval_var="${4}" + local max_interval_var="${5}" + local response_body="${6}" + + if [[ "${error_type}" == "network" ]]; then + if ! _api_should_retry_on_error "network" "${attempt}" "${max_retries}" "${!interval_var}" "${!max_interval_var}" "Cloud API network error"; then + log_error "Cloud API network error after ${max_retries} attempts" + return 1 + fi + else + # HTTP error code (429 or 503) + if ! _api_handle_transient_http_error "${error_type}" "${attempt}" "${max_retries}" "${!interval_var}" "${!max_interval_var}"; then + echo "${response_body}" + return 1 + fi + fi + + _update_retry_interval "${interval_var}" "${max_interval_var}" + return 0 +} + # Generic cloud API wrapper - centralized curl wrapper for all cloud providers # Includes automatic retry logic with exponential backoff for transient failures # Usage: generic_cloud_api BASE_URL AUTH_TOKEN METHOD ENDPOINT [BODY] [MAX_RETRIES] @@ -1015,52 +1087,27 @@ generic_cloud_api() { local max_interval=30 while [[ "${attempt}" -le "${max_retries}" ]]; do - local args=( - -s - -w "\n%{http_code}" - -X "${method}" - -H "Authorization: Bearer ${auth_token}" - -H "Content-Type: application/json" - ) - - if [[ -n "${body}" ]]; then - args+=(-d "${body}") - fi - - local response - response=$(curl "${args[@]}" "${base_url}${endpoint}" 2>&1) - local curl_exit_code=$? - - # Extract HTTP status code (last line) and response body (everything else) - local http_code - http_code=$(echo "${response}" | tail -1) - local response_body - response_body=$(echo "${response}" | head -n -1) - - # Check for network errors (curl exit code != 0) - if [[ ${curl_exit_code} -ne 0 ]]; then - if ! _api_should_retry_on_error "network" "${attempt}" "${max_retries}" "${interval}" "${max_interval}" "Cloud API network error"; then - log_error "Cloud API network error after ${max_retries} attempts: curl exit code ${curl_exit_code}" + # Make the API request + if ! _make_api_request "${base_url}" "${auth_token}" "${method}" "${endpoint}" "${body}"; then + # Network error - handle and retry + if ! _handle_api_transient_error "network" "${attempt}" "${max_retries}" "interval" "max_interval" ""; then return 1 fi - _update_retry_interval "interval" "max_interval" attempt=$((attempt + 1)) continue fi - # Check for transient HTTP errors that should be retried - if [[ "${http_code}" == "429" ]] || [[ "${http_code}" == "503" ]]; then - if ! _api_handle_transient_http_error "${http_code}" "${attempt}" "${max_retries}" "${interval}" "${max_interval}"; then - echo "${response_body}" + # Check for transient HTTP errors (429 or 503) + if [[ "${API_HTTP_CODE}" == "429" ]] || [[ "${API_HTTP_CODE}" == "503" ]]; then + if ! _handle_api_transient_error "${API_HTTP_CODE}" "${attempt}" "${max_retries}" "interval" "max_interval" "${API_RESPONSE_BODY}"; then return 1 fi - _update_retry_interval "interval" "max_interval" attempt=$((attempt + 1)) continue fi # Success or non-retryable error - return response body - echo "${response_body}" + echo "${API_RESPONSE_BODY}" return 0 done