opf/openproject · error · Import::JiraClient::ConnectionError

Failed to connect to Jira server: %{message}

Error message

Failed to connect to Jira server: %{message}

What it means

In download_attachment, any SsrfFilter::Error other than PrivateIPAddress is wrapped into Import::JiraClient::ConnectionError with this message. SsrfFilter::Error is the parent of InvalidUriScheme, UnresolvedHostname, CRLFInjection and TooManyRedirects, so this is the generic failure of the SSRF-guarded HTTP fetch of a Jira attachment (the original gem message is preserved in %{message}).

Source

Thrown at app/services/import/jira_client.rb:271

      tempfile = nil
      OpenProject::SsrfProtection.get(content_url, headers: @headers, http_options: HTTP_OPTIONS, max_redirects: 1) do |response|
        case response
        when Net::HTTPSuccess
          tempfile = Tempfile.create(filename, binmode: true)
          response.read_body do |chunk|
            tempfile.write chunk
          end
          yield tempfile
        else
          status = response.code.to_i
          raise ApiError.new(I18n.t("admin.jira.client.api_error", status:), status:, response_body: response.body)
        end
      end
      nil
    rescue SsrfFilter::PrivateIPAddress
      raise SsrfError, I18n.t("admin.jira.client.ssrf_blocked")
    rescue SsrfFilter::Error => e
      raise ConnectionError, I18n.t("admin.jira.client.connection_error", message: e.message)
    rescue OpenSSL::SSL::SSLError => e
      raise ConnectionError, I18n.t("admin.jira.client.ssl_error", message: e.message)
    rescue Timeout::Error => e
      raise ConnectionError, I18n.t("admin.jira.client.connection_timeout", message: e.message)
    ensure
      File.unlink(tempfile) if tempfile
    end

    private

    def get(path, params: {})
      response = get_response(path, params:)
      handle_response(response)
    end

    def get_response(path, params: {})
      OpenProject::SsrfProtection.get(
        "#{@url}#{path}",

View on GitHub (pinned to d9742c43f3)

Solutions

  1. Read %{message} to identify the subclass: 'hostname' wording means DNS failure, 'redirect' means the redirect limit, 'scheme' means a non-http(s) URL
  2. Verify DNS resolution and scheme of the failing content_url from the OpenProject host (curl -sIL '<content_url>')
  3. If an SSO/proxy redirect chain is the cause, bypass it for Jira REST/attachment paths or make the final hop resolvable in one redirect
  4. Validate the content_url is an absolute http(s) URI with a host before attempting the download

Example fix

# before
client.download_attachment(content_url, filename) { |tf| attach(tf) }

# after — reject unfetchable URLs before the SSRF-guarded request
uri = URI.parse(content_url)
unless uri.is_a?(URI::HTTP) && uri.host.present?
  raise ArgumentError, "Not a fetchable attachment URL: #{content_url}"
end
client.download_attachment(content_url, filename) { |tf| attach(tf) }
Defensive patterns

Strategy: try-catch

Validate before calling

uri = URI.parse(content_url)
raise ArgumentError, "unfetchable attachment URL #{content_url}" unless uri.is_a?(URI::HTTP) && uri.host.present?
client.download_attachment(content_url, filename) { |tf| attach(tf) }

Try / catch

begin
  client.download_attachment(content_url, filename) { |tf| attach(tf) }
rescue Import::JiraClient::ConnectionError => e
  # e.message embeds the original SsrfFilter reason (DNS, redirects, scheme)
  Rails.logger.warn("Jira attachment download failed: #{e.message}")
  mark_attachment_skipped(content_url, reason: e.message) # continue the import, record the gap
end

Prevention

When it happens

Trigger: client.download_attachment called with a content_url that is not http/https (InvalidUriScheme), whose hostname does not resolve (UnresolvedHostname), that contains CR/LF characters (CRLFInjection), or whose download follows more than one redirect (max_redirects: 1 is hardcoded, so a second hop raises TooManyRedirects).

Common situations: Attachment URLs shortened/rewritten by an SSO proxy that chains two redirects; internal hostnames not present in the OpenProject container's DNS (docker-compose without the corporate DNS); copy-pasted attachment URLs with a wrong scheme; reverse proxies redirecting http→https→internal-host.

Related errors


AI-assisted analysis of opf/openproject@d9742c43f3 (2026-08-21). Data as JSON: /api/errors/c26b134220f01064. Report an issue: GitHub.