we-promise/sure · error · Assistant::Error

External assistant returned HTTP #{response.code}.

Error message

External assistant returned HTTP #{response.code}.

What it means

Raised by Assistant::External::Client#stream_response when the upstream OpenAI-compatible endpoint answers with any non-2xx status. The response code is interpolated into the message and the first 500 bytes of the body are logged as a warning before the raise. Assistant::Error is not in TRANSIENT_ERRORS, so this is never retried.

Source

Thrown at app/models/assistant/external/client.rb:75

        sleep(RETRY_DELAY * retries)
        retry
      end
      Rails.logger.error("[External::Client] Unreachable after #{MAX_RETRIES + 1} attempts: #{e.class} - #{e.message}")
      raise Assistant::Error, "External assistant is temporarily unavailable."
    end
  end

  private

    def stream_response(http, request, &block)
      model = nil
      buffer = +""
      done = false

      http.request(request) do |response|
        unless response.is_a?(Net::HTTPSuccess)
          Rails.logger.warn("[External::Client] Upstream HTTP #{response.code}: #{response.body.to_s.truncate(500)}")
          raise Assistant::Error, "External assistant returned HTTP #{response.code}."
        end

        response.read_body do |chunk|
          break if done
          buffer << chunk

          if buffer.bytesize > MAX_SSE_BUFFER
            raise Assistant::Error, "External assistant stream exceeded maximum buffer size."
          end

          while (line_end = buffer.index("\n"))
            line = buffer.slice!(0..line_end).strip
            next if line.empty?
            next unless line.start_with?("data:")

            data = line.delete_prefix("data:")
            data = data.delete_prefix(" ") # SSE spec: strip one optional leading space

View on GitHub (pinned to e69894adb9)

Solutions

  1. Read the warn log line '[External::Client] Upstream HTTP <code>: <body>' — the truncated body names the real cause (invalid api key, model not found, quota exceeded).
  2. For 401/403, fix the token supplied to Assistant::External::Client.new (request['Authorization'] = "Bearer #{@token}") — re-issue or rotate the credential in env.
  3. For 404, correct the URL or agent id: the client POSTs to uri.request_uri with model: @agent_id, so both must match the upstream's routes.
  4. For 429/503, add caller-side backoff and retry — the client treats these as permanent for this request.

Example fix

# before
client = Assistant::External::Client.new(url:, token:)
model = client.chat(messages: msgs) { |c| print c }

# after — surface the upstream status instead of an opaque failure
begin
  model = client.chat(messages: msgs) { |c| print c }
rescue Assistant::Error => e
  if (m = e.message.match(/HTTP (\d{3})/))
    case m[1]
    when /40[13]/ then raise "External assistant auth rejected: check token"
    when "429", "503" then raise "External assistant busy, retry later"
    else raise
    end
  else
    raise
  end
end
Defensive patterns

Strategy: try-catch

Validate before calling

# Fail fast on obviously bad config before spending a request
raise ArgumentError, "token missing" if token.blank?
uri = URI(url)
raise ArgumentError, "bad URL" unless uri.host.present?

Try / catch

begin
  model = client.chat(messages: msgs) { |c| print c }
rescue Assistant::Error => e
  code = e.message[/HTTP (\d{3})/, 1]
  case code
  when "401", "403" then raise "External assistant rejected credentials"
  when "429", "503" then raise "External assistant busy — retry later"
  else raise
  end
end

Prevention

When it happens

Trigger: POST to the configured URL returns 401 (wrong/missing Bearer token passed to Client.new(url:, token:)), 404 (wrong URL path or model/@agent_id), 400 (malformed messages payload), 429 (rate limited), or 500/502/503 (upstream crash or maintenance). Any of these hits the `unless response.is_a?(Net::HTTPSuccess)` branch at app/models/assistant/external/client.rb:73-76.

Common situations: Expired or rotated API token in env; pointing the client at a base URL that already includes /v1/chat/completions or misses it; upstream deployed behind a gateway returning 502 during restarts; hitting provider rate limits under load.

Related errors


AI-assisted analysis of we-promise/sure@e69894adb9 (2026-08-21). Data as JSON: /api/errors/6cdbb7cb8baff78d. Report an issue: GitHub.