{"record":{"id":"a772f3faaac65690","repo":"we-promise/sure","slug":"external-assistant-is-temporarily-unavailable","errorCode":null,"errorMessage":"External assistant is temporarily unavailable.","messagePattern":"External assistant is temporarily unavailable\\.","errorType":"exception","errorClass":"Assistant::Error","httpStatus":null,"severity":"error","filePath":"app/models/assistant/external/client.rb","lineNumber":61,"sourceCode":"      model = stream_response(http, request) do |content|\n        streaming_started = true\n        block.call(content)\n      end\n      model\n    rescue *TRANSIENT_ERRORS => e\n      if streaming_started\n        Rails.logger.warn(\"[External::Client] Stream interrupted: #{e.class} - #{e.message}\")\n        raise Assistant::Error, \"External assistant connection was interrupted.\"\n      end\n\n      retries += 1\n      if retries <= MAX_RETRIES\n        Rails.logger.warn(\"[External::Client] Transient error (attempt #{retries}/#{MAX_RETRIES}): #{e.class} - #{e.message}\")\n        sleep(RETRY_DELAY * retries)\n        retry\n      end\n      Rails.logger.error(\"[External::Client] Unreachable after #{MAX_RETRIES + 1} attempts: #{e.class} - #{e.message}\")\n      raise Assistant::Error, \"External assistant is temporarily unavailable.\"\n    end\n  end\n\n  private\n\n    def stream_response(http, request, &block)\n      model = nil\n      buffer = +\"\"\n      done = false\n\n      http.request(request) do |response|\n        unless response.is_a?(Net::HTTPSuccess)\n          Rails.logger.warn(\"[External::Client] Upstream HTTP #{response.code}: #{response.body.to_s.truncate(500)}\")\n          raise Assistant::Error, \"External assistant returned HTTP #{response.code}.\"\n        end\n\n        response.read_body do |chunk|\n          break if done","sourceCodeStart":43,"sourceCodeEnd":79,"githubUrl":"https://github.com/we-promise/sure/blob/e69894adb92547273377398c15f45c979cd9416a/app/models/assistant/external/client.rb#L43-L79","documentation":"Raised by Assistant::External::Client#chat when a transient network error occurs BEFORE any content chunk streamed AND the built-in retry budget is exhausted. The client retries transient failures up to MAX_RETRIES = 2 extra attempts with sleep(RETRY_DELAY * retries) backoff (1s then 2s), so this error means 3 total attempts all failed at connect/first-read time.","triggerScenarios":"client.chat raises Net::OpenTimeout, Errno::ECONNREFUSED, SocketError (bad DNS), EHOSTUNREACH, or ReadTimeout before the first SSE chunk: upstream process down, wrong hostname in @url, port closed, DNS failure, or a proxy in HTTP_PROXY/HTTPS_PROXY refusing the CONNECT. streaming_started stays false through all 3 attempts, falling through to the raise at app/models/assistant/external/client.rb:60-61.","commonSituations":"External agent service stopped or crashed; wrong EXTERNAL_ASSISTANT URL in env config; DNS not resolving in the deploy environment; a corporate proxy env var pointing at a dead proxy; firewall/security group blocking egress on the target port.","solutions":["Verify the upstream is reachable from the app host: curl -v <same URL> — fix the stopped service or the URL/env configuration.","Check DNS and egress rules (security groups, firewall) for the app server, and inspect HTTP_PROXY/HTTPS_PROXY/NO_PROXY env vars the client honors.","Retry from the caller with backoff — this failure is safe to retry because nothing was streamed and no side effects occurred.","If the endpoint is flaky by nature, wrap calls in a circuit breaker so you fail fast instead of burning the full retry ladder on every request."],"exampleFix":"# before\nresult = client.chat(messages: messages) { |chunk| stream.write(chunk) }\n\n# after — safe outer retry because nothing streamed\nattempts = 0\nbegin\n  result = client.chat(messages: messages) { |chunk| stream.write(chunk) }\nrescue Assistant::Error => e\n  raise if e.message.exclude?(\"temporarily unavailable\") || (attempts += 1) >= 3\n  sleep(5 * attempts)\n  retry\nend","handlingStrategy":"retry","validationCode":"# Cheap reachability check before the call (nothing has streamed, retry is safe)\nrequire \"socket\"\nrequire \"uri\"\nuri = URI(external_assistant_url)\nSocket.tcp(uri.host, uri.port, connect_timeout: 3).close","typeGuard":null,"tryCatchPattern":"attempts = 0\nbegin\n  model = client.chat(messages: messages) { |c| stream.write(c) }\nrescue Assistant::Error => e\n  raise unless e.message.include?(\"temporarily unavailable\") && attempts < 3\n  attempts += 1\n  sleep(2**attempts)\n  retry\nend","preventionTips":["Monitor the external agent's uptime and wire a readiness check so traffic stops before the 3-attempt ladder burns request time.","Keep HTTP_PROXY/HTTPS_PROXY/NO_PROXY env vars correct for the deploy environment — the client honors them when building the connection.","Use a circuit breaker around chat so sustained outages fail fast instead of retrying on every request."],"tags":["network","retry","connect-timeout","assistant","net-http"],"backgroundTag":"service-unreachable","analyzedSha":"e69894adb92547273377398c15f45c979cd9416a","analyzedAt":"2026-08-21T18:22:41.165Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}