From f0690aa9810d083e8ca8c22782a7e7d5af93796b Mon Sep 17 00:00:00 2001 From: chiehwangs Date: Sun, 17 May 2026 11:17:52 +0800 Subject: [PATCH] fix: Fix installer retries for HTTP/2 download failures (#1910) --- .github/workflows/installer-smoke.yml | 2 + docs/RELEASE-MANUAL-SMOKE.md | 4 ++ scripts/install.sh | 48 ++++++++++++++++--- scripts/test_install.sh | 67 +++++++++++++++++++++++++++ 4 files changed, 115 insertions(+), 6 deletions(-) diff --git a/.github/workflows/installer-smoke.yml b/.github/workflows/installer-smoke.yml index 61dbe9266..6882371ff 100644 --- a/.github/workflows/installer-smoke.yml +++ b/.github/workflows/installer-smoke.yml @@ -20,6 +20,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v5 + - name: Unit tests (install.sh helpers) + run: bash scripts/test_install.sh - name: Run installer dry-run run: bash scripts/install.sh --dry-run --verbose diff --git a/docs/RELEASE-MANUAL-SMOKE.md b/docs/RELEASE-MANUAL-SMOKE.md index ef29648ad..65ef87a08 100644 --- a/docs/RELEASE-MANUAL-SMOKE.md +++ b/docs/RELEASE-MANUAL-SMOKE.md @@ -21,6 +21,10 @@ This is the **only** acceptable substitute for a `🚫` row in [`TEST-COVERAGE-M Applies to every release, all platforms. +### Public installer script + +- [ ] **`scripts/install.sh` downloads the latest asset on a proxy/VPN network** — From a clean checkout, run `bash scripts/install.sh --dry-run --verbose`, then run the public `curl -fsSL https://raw.githubusercontent.com/tinyhumansai/openhuman/main/scripts/install.sh | bash` flow on one macOS or Linux host. Expected: release metadata resolves, the asset downloads successfully, and transient GitHub/CDN HTTP/2 failures retry over HTTP/1.1 instead of surfacing `curl: (16) Error in the HTTP2 framing layer`. + ### macOS - [ ] **Gatekeeper accepts the signed `.app` on first launch** — Double-click the `.app` from a fresh download (Quarantine attribute set). Expected: app opens without `"OpenHuman" cannot be opened because the developer cannot be verified` dialog. If it appears, the build is unsigned or the notarization stapler is missing. diff --git a/scripts/install.sh b/scripts/install.sh index e8150649e..88acc2777 100644 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -200,11 +200,47 @@ PY printf '%s\n' "$url" } +# curl can fail on GitHub/CDN HTTP/2 framing issues on some networks while the +# same URL succeeds over HTTP/1.1. Try the normal path first, then a +# compatibility retry before surfacing the failure. +curl_get_file() { + local url="$1" output="$2" rc + if curl -fsSL "$url" -o "$output"; then + return 0 + else + rc=$? + fi + log_warn "Request failed (curl rc=${rc}); retrying with HTTP/1.1." + curl --http1.1 -fsSL "$url" -o "$output" +} + +curl_download_file() { + local url="$1" output="$2" rc + if curl -fL "$url" -o "$output"; then + return 0 + else + rc=$? + fi + log_warn "Download failed (curl rc=${rc}); retrying with HTTP/1.1." + curl --http1.1 -fL "$url" -o "$output" +} + +curl_head_with_http_fallback() { + local url="$1" rc + if curl -fsSI --max-time 10 "$url" >/dev/null 2>&1; then + return 0 + else + rc=$? + fi + log_warn "Reachability check failed (curl rc=${rc}); retrying with HTTP/1.1." + curl --http1.1 -fsSI --max-time 10 "$url" >/dev/null 2>&1 +} + # Retries an HTTP HEAD on the asset URL, fails loudly with the URL. verify_asset_reachable() { local url="$1" max_attempts=5 delay=2 for i in $(seq 1 $max_attempts); do - if curl -fsSI --max-time 10 "$url" >/dev/null 2>&1; then + if curl_head_with_http_fallback "$url"; then return 0 fi if [[ $i -lt $max_attempts ]]; then @@ -217,7 +253,7 @@ verify_asset_reachable() { } resolve_from_latest_json() { - if ! curl -fsSL "${LATEST_JSON_URL}" -o "${LATEST_JSON_PATH}"; then + if ! curl_get_file "${LATEST_JSON_URL}" "${LATEST_JSON_PATH}"; then return 1 fi @@ -252,7 +288,7 @@ print(d.get('version', '')) } resolve_from_release_api() { - if ! curl -fsSL "${LATEST_RELEASE_API_URL}" -o "${RELEASE_JSON_PATH}"; then + if ! curl_get_file "${LATEST_RELEASE_API_URL}" "${RELEASE_JSON_PATH}"; then return 1 fi @@ -333,7 +369,7 @@ resolve_release_digest() { return 0 fi if [ ! -s "${RELEASE_JSON_PATH}" ]; then - if ! curl -fsSL "${LATEST_RELEASE_API_URL}" -o "${RELEASE_JSON_PATH}"; then + if ! curl_get_file "${LATEST_RELEASE_API_URL}" "${RELEASE_JSON_PATH}"; then return 0 fi fi @@ -417,9 +453,9 @@ fi DOWNLOAD_PATH="${TMP_DIR}/${ASSET_NAME}" log_info "Downloading ${ASSET_NAME}" if [ "${DRY_RUN}" = true ]; then - echo "DRY RUN: curl -fL ${ASSET_URL} -o ${DOWNLOAD_PATH}" + echo "DRY RUN: curl -fL ${ASSET_URL} -o ${DOWNLOAD_PATH} (retrying with --http1.1 on failure)" else - curl -fL "${ASSET_URL}" -o "${DOWNLOAD_PATH}" + curl_download_file "${ASSET_URL}" "${DOWNLOAD_PATH}" fi compute_sha256() { diff --git a/scripts/test_install.sh b/scripts/test_install.sh index e5746bab4..526f6cde2 100755 --- a/scripts/test_install.sh +++ b/scripts/test_install.sh @@ -30,4 +30,71 @@ if [[ "$missing_platform_rc" -ne 3 ]]; then exit 1 fi +assert_retry_shape() { + local calls="$1" label="$2" + local _ first second extra + IFS='|' read -r _ first second extra <<<"${calls}" + + if [[ -z "${first:-}" || -z "${second:-}" || -n "${extra:-}" ]]; then + echo "FAIL: ${label} should issue exactly 2 curl calls (base + HTTP/1.1 retry)" + exit 1 + fi + + if [[ "${first}" == *"--http1.1"* || "${second}" != *"--http1.1"* ]]; then + echo "FAIL: ${label} should retry with --http1.1 only on the second call" + exit 1 + fi +} + +( + CURL_CALLS="" + curl() { + CURL_CALLS="${CURL_CALLS}|$*" + case " $* " in + *" --http1.1 "*) return 0 ;; + *) return 16 ;; + esac + } + + if ! curl_head_with_http_fallback "https://example.invalid/OpenHuman.app.tar.gz"; then + echo "FAIL: reachability fallback should succeed when HTTP/1.1 retry succeeds" + exit 1 + fi + assert_retry_shape "${CURL_CALLS}" "reachability check" +) + +( + CURL_CALLS="" + curl() { + CURL_CALLS="${CURL_CALLS}|$*" + case " $* " in + *" --http1.1 "*) return 0 ;; + *) return 16 ;; + esac + } + + if ! curl_get_file "https://example.invalid/latest.json" "/tmp/openhuman-test-latest.json"; then + echo "FAIL: metadata fetch fallback should succeed when HTTP/1.1 retry succeeds" + exit 1 + fi + assert_retry_shape "${CURL_CALLS}" "metadata fetch" +) + +( + CURL_CALLS="" + curl() { + CURL_CALLS="${CURL_CALLS}|$*" + case " $* " in + *" --http1.1 "*) return 0 ;; + *) return 16 ;; + esac + } + + if ! curl_download_file "https://example.invalid/OpenHuman.app.tar.gz" "/tmp/openhuman-test-download"; then + echo "FAIL: download fallback should succeed when HTTP/1.1 retry succeeds" + exit 1 + fi + assert_retry_shape "${CURL_CALLS}" "download" +) + echo "PASS"