{"record":{"id":"e1ebd0f8cb3640fc","repo":"heygen-com/hyperframes","slug":"failurestage-attemptdetail-describefetchfa","errorCode":null,"errorMessage":"${failureStage}${attemptDetail}: ${describeFetchFailure(lastError)}","messagePattern":"\\$\\{failureStage\\}\\$\\{attemptDetail\\}: \\$\\{describeFetchFailure\\(lastError\\)\\}","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/cli/src/utils/publishProject.ts","lineNumber":243,"sourceCode":"  createInit: () => RequestInit,\n  failureStage: string,\n  attempts = 1,\n): Promise<Response> {\n  if (attempts < 1) throw new RangeError(\"Publish fetch attempts must be at least 1\");\n  let lastError: unknown;\n  let attemptsMade = 0;\n  for (let attempt = 1; attempt <= attempts; attempt += 1) {\n    attemptsMade = attempt;\n    try {\n      return await fetch(input, createInit());\n    } catch (error) {\n      lastError = error;\n      if (isRequestTimeout(error) || attempt === attempts) break;\n      await waitBeforePublishRetry();\n    }\n  }\n  const attemptDetail = attemptsMade > 1 ? ` after ${attemptsMade} attempts` : \"\";\n  throw new Error(`${failureStage}${attemptDetail}: ${describeFetchFailure(lastError)}`, {\n    cause: lastError instanceof Error ? lastError : undefined,\n  });\n}\n\nexport function uploadTimeoutMs(byteLength: number): number {\n  return Math.max(\n    PUBLISH_UPLOAD_MIN_TIMEOUT_MS,\n    Math.ceil((byteLength / PUBLISH_UPLOAD_BYTES_PER_SECOND) * 1000),\n  );\n}\n\nfunction shouldIgnoreSegment(segment: string): boolean {\n  return segment.startsWith(\".\") || IGNORED_DIRS.has(segment) || IGNORED_FILES.has(segment);\n}\n\nfunction createProjectIgnore(rootDir: string): Ignore {\n  const matcher = ignore().add(DEFAULT_PROJECT_IGNORE);\n  const ignorePath = join(rootDir, HYPERFRAMES_IGNORE_FILE);","sourceCodeStart":225,"sourceCodeEnd":261,"githubUrl":"https://github.com/heygen-com/hyperframes/blob/c2996c8626135db5253519359d8a063d3bafad8d/packages/cli/src/utils/publishProject.ts#L225-L261","documentation":"fetchForPublish is the shared retry wrapper for all publish-network calls. It retries on generic fetch exceptions (network/DNS/SSL) up to `attempts` times (default 1; staged upload paths use PUBLISH_TRANSPORT_ATTEMPTS=2), but breaks immediately on a request timeout/abort. When every attempt throws (the fetch never returned a Response at all), it throws `${failureStage}${attemptDetail}: ${describeFetchFailure(lastError)}` with the original error on the `.cause`. This is transport-level — distinct from a server HTTP error response (those are [257]/[258]/[259]).","triggerScenarios":"DNS resolution fails for the API host; the TCP connection is refused (API down); TLS handshake fails; the network is unreachable; a timeout fires (AbortSignal.timeout or DOMException TimeoutError) on the first attempt (immediate break, no retry); a proxy is configured via HTTPS_PROXY but Node fetch ignores it without NODE_USE_ENV_PROXY=1 (the error appends a proxySupportHint).","commonSituations":"Corporate proxy environment where HTTPS_PROXY is set but Node 22's fetch does not honor it without NODE_USE_ENV_PROXY=1; offline or flaky network; the publish API host is temporarily unreachable; a self-signed cert or corporate MITM breaking TLS; slow upload exceeding uploadTimeoutMs on every retry.","solutions":["If a proxy is set, retry with NODE_USE_ENV_PROXY=1 (Node 22.21+) — the error appends this hint when relevant.","Verify network connectivity to the API host: curl -I <api-base-url>.","For transient issues, simply retry the publish command (the wrapper already retries twice for staged uploads).","Check DNS resolution and TLS trust store on the host.","For slow networks, the upload timeout is auto-scaled by archive size (uploadTimeoutMs); ensure the archive is not unexpectedly huge."],"exampleFix":"# before: proxy set but ignored by Node fetch\nexport HTTPS_PROXY=http://corp-proxy:3128\nhyperframes publish .\n# after\nexport NODE_USE_ENV_PROXY=1\nhyperframes publish .","handlingStrategy":"retry","validationCode":"import { isRequestTimeout } from './publishProject'; // or replicate\n\nasync function pingApi(apiBaseUrl: string): Promise<void> {\n  try {\n    const r = await fetch(`${apiBaseUrl}/v1/hyperframes/projects`, { signal: AbortSignal.timeout(5000) });\n    if (!r.ok && r.status >= 500) throw new Error('API unhealthy');\n  } catch (e) {\n    throw new Error(`Cannot reach publish API at ${apiBaseUrl}: ${(e as Error).message}`);\n  }\n}","typeGuard":null,"tryCatchPattern":"try {\n  return await publishProjectArchive(projectDir, opts);\n} catch (err) {\n  if (err instanceof Error && /after \\d+ attempts:/.test(err.message)) {\n    // transport-level: retry with backoff, or surface proxy hint\n    if (process.env.HTTPS_PROXY && process.env.NODE_USE_ENV_PROXY !== '1') {\n      console.error('Set NODE_USE_ENV_PROXY=1 and retry (Node fetch ignores proxy vars otherwise).');\n    }\n    await new Promise(r => setTimeout(r, 1000));\n    return publishProjectArchive(projectDir, opts);\n  }\n  throw err;\n}","preventionTips":["If behind a corporate proxy, set NODE_USE_ENV_PROXY=1 (Node 22.21+) so fetch honors HTTPS_PROXY.","Run a connectivity check to the API host before publishing in CI.","Keep archives reasonably sized — upload timeout scales with byte length but very large archives are more fragile on flaky links."],"tags":["publish","network","fetch","retry","proxy","timeout"],"backgroundTag":null,"analyzedSha":"c2996c8626135db5253519359d8a063d3bafad8d","analyzedAt":"2026-08-12T22:18:56.877Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}