Skip to content

Replace RestClient with Faraday 2.x for persistent Candlepin connections#11754

Open
pablomh wants to merge 36 commits into
Katello:masterfrom
pablomh:faraday-persistent-connections
Open

Replace RestClient with Faraday 2.x for persistent Candlepin connections#11754
pablomh wants to merge 36 commits into
Katello:masterfrom
pablomh:faraday-persistent-connections

Conversation

@pablomh

@pablomh pablomh commented May 15, 2026

Copy link
Copy Markdown
Contributor

What are the changes introduced in this pull request?

Replace the unmaintained RestClient gem with Faraday 2.x and the net_http_persistent adapter for the Candlepin HTTP layer, enabling persistent TCP+TLS connection reuse between Puma and Candlepin. This eliminates per-request connection setup overhead during concurrent host registration.

Prerequisite: theforeman/foreman_azure_rm#209 must merge first — the retired Azure SDK pins faraday < 2 via ms_rest, which blocks this upgrade.

Note: rest-client remains as a dependency — it is still used by CDN resource code (cdn.rb). Only the Candlepin HTTP layer is migrated to Faraday.

Key changes:

  • Faraday 2.x migration: Replace RestClient::Resource with Faraday::Connection, memoized at the class level for connection pooling across Puma threads. net-http-persistent v4.x provides per-thread TCP connection isolation via connection_pool internally.
  • OAuth 1.0a concern: Extract request signing into OauthRequestSigning concern, making auth a pluggable strategy (preparation for mTLS transition). Memoize OAuth::Consumer to avoid per-request allocation.
  • Unified request pipeline: All Candlepin HTTP traffic (direct, proxy, upstream) flows through issue_request with connection: (custom Faraday for upstream certs), process: (skip error raising for pass-through), and params: (query string) keywords.
  • Keyword args on HTTP verbs: get(path, headers:, params:), post(path, payload, headers:, params:) — replaces fragile positional arg parsing.
  • HttpError: Transport-neutral replacement for RestClientException with response_body for controller error rendering.
  • Upstream connection lifecycle: Per-org memoized faraday_connection for upstream Candlepin, with reset_connection! called in manifest import/refresh/delete finalize.
  • Lazy debug logging: logger.debug { } blocks skip serialization when debug is disabled.

Considerations taken when implementing this change?

  • Connection reuse impact: In A/B testing on a Satellite 6.16 environment, ActiveOpens (TCP connections created) dropped from 692K to 2.7K over a 2000-host registration run — a 99.6% reduction.
  • PQC readiness: Persistent connections amortize TLS handshake cost. With upcoming Post-Quantum Cryptography adoption (ML-KEM/ML-DSA), handshake overhead increases significantly — connection reuse prevents this from regressing registration latency.
  • Thread safety: net-http-persistent v4.x uses connection_pool internally, providing per-thread connection isolation.
  • Upstream multi-org safety: faraday_connection and upstream_owner_id are keyed by Organization.current.id to prevent cross-org credential leaks in multi-threaded Puma.
  • Backward compatibility: All 24 RestClient rescue sites updated to HttpError with status code checks. All 41+ callers updated from positional args to keyword headers: args. hash_to_query kept as alias for query_string.
  • rest-client retained: CDN code (cdn.rb) still uses RestClient exceptions and must keep the gem dependency until a separate CDN migration is done.

Gemspec changes:

Added:

  • faraday (>= 2.0, < 3.0)
  • faraday-net_http_persistent (~> 2.0)
  • faraday-multipart (~> 1.0)

Retained (still used by CDN):

  • rest-client

Packaging note: RPM packages need to add rubygem-faraday, rubygem-faraday-net_http_persistent, rubygem-faraday-multipart, and rubygem-net-http-persistent. Do NOT remove rubygem-rest-client — it is still required by CDN resource code.

What are the testing steps for this pull request?

  • Unit tests for issue_request with connection:, process: false, and params: keywords
  • Unit tests for HttpError with response_body
  • Unit tests for ping status mapping (401/410 -> UpstreamConsumerGone, 404 -> UpstreamConsumerNotFound)
  • Unit tests for query_string helper
  • Updated http_resource_test.rb for keyword args
  • Updated candlepin_test.rb for Faraday mocks
  • Updated registration_manager_test.rb for HttpError
  • Updated candlepin_proxies_controller_test.rb for Faraday response API
  • Full Katello test suite (bundle exec rake test:katello)
  • Registration workflow: register host, verify Candlepin consumer created
  • Manifest import/refresh/delete cycle — verify upstream connection reset
  • Concurrent registration (50+ hosts) — verify no connection errors
  • Verify ActiveOpens reduction with PCP (pmrep network.tcp.activeopens)

Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • OAuth signing added for outgoing HTTP requests.
    • Manifest workflows now refresh upstream backend connections after import/refresh/delete.
  • Bug Fixes

    • More consistent HTTP error handling and clearer CLI vs plain-text error responses.
    • Improved logging and safer error payload reporting for backend failures.
  • Refactor

    • Unified HTTP client and standardized upstream/resource interactions and header usage.
  • Tests

    • Expanded and updated tests to cover the new HTTP client behavior.
  • Chores

    • Updated HTTP client dependencies.

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 15, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough
📝 Walkthrough
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.68% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Replace RestClient with Faraday 2.x for persistent Candlepin connections' directly and clearly summarizes the main change in the PR, which is a comprehensive migration of HTTP libraries.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/lib/katello/resources/candlepin.rb (1)

152-154: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve the upstream port in site.

upstream_api_uri.port is discarded here, so any non-default CDN/Candlepin endpoint silently gets rewritten to 80/443.

Suggested fix
           def site
-            "#{upstream_api_uri.scheme}://#{upstream_api_uri.host}"
+            "#{upstream_api_uri.scheme}://#{upstream_api_uri.host}:#{upstream_api_uri.port}"
           end
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/lib/katello/resources/candlepin.rb` around lines 152 - 154, The site
method currently builds the base URL from upstream_api_uri.scheme and .host but
drops upstream_api_uri.port, causing non-default ports to be lost; update the
site method (def site) to append the port when upstream_api_uri.port is present
and not the default for the scheme (e.g., include ":#{upstream_api_uri.port}"
when upstream_api_uri.port exists and isn’t 80 for http or 443 for https) so the
returned URI preserves custom CDN/Candlepin endpoints.
🧹 Nitpick comments (3)
test/lib/http_resource_test.rb (1)

30-40: ⚡ Quick win

Add reserved-character encoding coverage in query-string tests.

These cases validate structure/order, but not escaping of values like spaces, &, or =. A small extra case would make serialization regressions much easier to catch.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/lib/http_resource_test.rb` around lines 30 - 40, Add a new unit test in
test/lib/http_resource_test.rb that calls TestHttpResource.query_string with
parameters containing reserved characters (e.g., values with spaces, ampersands,
and equals like "a b", "c&d", "e=f") to assert they are percent-encoded in the
serialized query string; create a method such as
test_query_string_reserved_characters that passes either a Hash or
array-of-tuples matching the existing ordering expectations and asserts the
expected encoded output (e.g., space -> %20, & -> %26, = -> %3D) to catch
regression in TestHttpResource.query_string encoding behavior.
app/lib/actions/candlepin/environment/set_content.rb (1)

34-47: ⚡ Quick win

Use case for status branching in the add-content rescue block.

This if/elsif pattern on a single discriminator (e.code) triggers Style/CaseLikeIf and is easier to read as case.

♻️ Suggested refactor
-              if e.code == '409'
+              case e.code
+              when '409'
                 # Candlepin raises a 409 in case it gets a duplicate content id add to an environment.
                 # Refresh the existing ids list and try again.
                 raise e if ((retries += 1) == max_retries)
                 output[:add_ids] = content_ids - existing_ids
-              elsif e.code == '404'
+              when '404'
                 # Set a higher limit for retries just in case the missing content is not being parsed correctly.
                 # If the content is not found after the retries, assume it is gone and continue.
                 raise e if ((retries += 1) == 1_000)
                 missing_content = e.message.split(' ')[-1].gsub(/[".]/, '')
                 Rails.logger.debug "Content #{missing_content} not found in the environment. Removing it from the add_ids list."
                 output[:add_ids].delete(missing_content)
               else
                 raise
               end
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/lib/actions/candlepin/environment/set_content.rb` around lines 34 - 47,
Replace the if/elsif chain that branches on e.code inside the add-content rescue
block with a case statement (case e.code; when '409' ... when '404' ... else ...
end) to satisfy Style/CaseLikeIf while preserving existing behavior: on '409'
keep the retries increment and raise when (retries += 1) == max_retries and
refresh output[:add_ids] = content_ids - existing_ids; on '404' keep the higher
retry threshold (raise when (retries += 1) == 1_000), compute missing_content
the same way, log the debug message, and remove it from output[:add_ids];
otherwise re-raise the exception. Ensure variables and side-effects (retries,
max_retries, output[:add_ids], content_ids, existing_ids, and the
Rails.logger.debug call) remain unchanged.
app/controllers/katello/api/registry/registry_proxies_controller.rb (1)

48-50: ⚡ Quick win

Keep BLOB_UNKNOWN 404s out of error logs.

The RestClient::Exception branch right above intentionally downgrades 404 BLOB_UNKNOWN lookups to info because podman push triggers them routinely. This new HttpError path logs every such miss as error, so once registry requests come through HttpError the logs will start filling with expected failures again.

Proposed fix
 rescue_from HttpResource::HttpError do |e|
-  Rails.logger.error(pp_exception(e, with_backtrace: false))
+  if e.code.to_i == 404
+    parsed = JSON.parse(e.response_body.to_s) rescue {}
+    if parsed["errors"]&.any? { |error| error["code"] == "BLOB_UNKNOWN" }
+      Rails.logger.info(pp_exception(e, with_backtrace: false))
+    else
+      Rails.logger.error(pp_exception(e, with_backtrace: false))
+    end
+  else
+    Rails.logger.error(pp_exception(e, with_backtrace: false))
+  end
   body = e.response_body || e.message
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/controllers/katello/api/registry/registry_proxies_controller.rb` around
lines 48 - 50, The HttpResource::HttpError rescue handler (rescue_from
HttpResource::HttpError do |e|) currently logs all misses as errors; change it
to detect a 404 response (e.response_code == 404 or e.response_body includes
"BLOB_UNKNOWN") and log those cases with Rails.logger.info (or downgrade the log
level) instead of Rails.logger.error, preserving the existing body =
e.response_body || e.message assignment and only calling Rails.logger.error for
non-404/ non-BLOB_UNKNOWN errors.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/controllers/katello/api/registry/registry_proxies_controller.rb`:
- Around line 50-54: Replace the current fallback logic that sets body =
e.response_body || e.message with a presence-aware check so empty strings from
Candlepin don't override the fallback: use e.response_body.presence || e.message
when building body (in the RegistryProxiesController error handling block where
body is assigned and rendered for request_from_katello_cli? / render plain
paths) so blank response_body values fall back to e.message.

In `@app/lib/actions/middleware/propagate_candlepin_errors.rb`:
- Around line 20-26: The rescue currently converts every HttpResource::HttpError
into Katello::Errors::CandlepinError, losing upstream-specific types; change the
re-raise to preserve upstream error classes: after extracting display_message
(using JSON.parse of e.response_body as done), check if
e.is_a?(Katello::Errors::UpstreamCandlepinError) (or whatever upstream subclass
lives under Katello::Errors) and if so re-raise an instance of e.class with the
display_message (e.g. raise e.class, display_message, e.backtrace) so downstream
rescues still match, otherwise raise ::Katello::Errors::CandlepinError with
display_message as today.

In `@app/lib/katello/http_resource.rb`:
- Around line 64-77: The rescue block in http_resource.rb currently converts
parse failures for many HTTP error statuses into NetworkException which breaks
callers expecting HttpError; adjust the rescue so that when resp is present
(i.e. an HTTP response was received) you always raise HttpError with the
existing payload ({:message => message_or_error, :service_code => service_code,
:code => status_code, :response_body => resp.body}, caller) instead of raising
NetworkException for non-JSON bodies—preserve the existing special-case logging
for 404/500/502/503/504 if desired, but ensure 401/409/410 and other HTTP error
statuses still raise HttpError; use the existing locals resp.status, error (from
rescue), message, service_code and status_code to build the HttpError.

In `@app/lib/katello/resources/candlepin.rb`:
- Around line 167-174: The method upstream_owner_id is bypassing normal HTTP
error handling by passing process: false to
Katello::Resources::Candlepin::UpstreamConsumer.issue_request; remove the
process: false option so issue_request will raise the appropriate HTTP
exceptions on 401/404/410 instead of returning a raw response, then parse
JSON.parse(response.body)['owner']['key'] only after a successful call; update
the call site in upstream_owner_id (and any similar callers) to rely on the
default processing behavior of issue_request.

In `@app/lib/katello/resources/candlepin/activation_key.rb`:
- Around line 55-67: The JSON.parse call is using the response object itself
instead of its body after the RestClient→Faraday change; update the tail of the
method so it parses the response body when result is a Faraday/response object
(e.g. if result.respond_to?(:body) use result.body, otherwise fall back to
result or '{}'), then pass the parsed value into
::Katello::Util::Data.array_with_indifferent_access; adjust references around
Candlepin::CandlepinResource.put and Candlepin::CandlepinResource.issue_request
to use the chosen result-body extraction.

In `@app/lib/katello/resources/candlepin/consumer.rb`:
- Around line 47-52: In async_hypervisors, self.post now returns an HTTP
response wrapper, so JSON.parse is being called on the response object instead
of the body; update the method (async_hypervisors) to extract the response body
(e.g., response.body) and parse that, then call with_indifferent_access on the
parsed object; also consider handling a nil/empty body before parsing to avoid
exceptions.
- Around line 138-150: The code currently calls JSON.parse(result) but result is
the HTTP response object returned by
Candlepin::CandlepinResource.put/issue_request; parse the response body string
instead (e.g., result.body or result.read_body depending on the client) before
passing it to JSON.parse and then to
::Katello::Util::Data.array_with_indifferent_access, and ensure result is set
(or handle nil) when neither attrs_to_update nor attrs_to_delete runs; update
references to Candlepin::CandlepinResource.put, issue_request, the local result
variable, and the final ::Katello::Util::Data.array_with_indifferent_access call
accordingly.

In `@app/lib/katello/resources/candlepin/owner.rb`:
- Around line 80-86: The destroy_imports method is parsing Faraday response
objects directly (response_json = self.delete(...) and response_json =
self.get(...)) which causes JSON.parse errors; change the code to extract the
response body (use response.body) before calling JSON.parse in both the delete
and get calls so that JSON.parse receives a string; update the variables
referenced in destroy_imports (response_json and response) accordingly and
preserve the existing with_indifferent_access handling.

In `@app/lib/katello/resources/candlepin/upstream_pool.rb`:
- Around line 8-17: The get method in upstream_pool.rb currently calls
issue_request with process: false and only checks for response.status == 410,
which lets other non-2xx responses slip through; update the get method (the
issue_request call and its immediate response handling) to explicitly guard
non-2xx statuses before returning: if response.status == 410 raise
Katello::Errors::UpstreamConsumerGone, if response.status == 401 (if your
upstream-consumer contract requires) map to the same or another appropriate
Katello::Errors class, and for any other non-2xx status raise a
generic/error-wrapping exception (include status and response body) so
downstream parsing won’t receive raw error responses. Ensure you still pass
default_headers and params when reusing issue_request and only return the
response for 2xx statuses.

In `@app/services/katello/registration_manager.rb`:
- Around line 294-298: Replace the if/elsif chain that checks e.code with a case
statement: change the conditional starting with "if e.code == '404'" (which
warns about missing consumer) and the "elsif e.code == '410'" branch (which
warns about already removed consumer) to "case e.code; when '404' ... when '410'
... else ... end", keeping the same Rails.logger.warn(...) calls and the
host_uuid reference so behavior is unchanged while satisfying Style/CaseLikeIf.

In `@test/lib/http_resource_test.rb`:
- Around line 47-49: The test currently only asserts that `@mock_conn.send` was
invoked and yields a permissive stub_request, which lets regressions in
header/body assignment slip by; update the tests (e.g., the case using
`@mock_conn.expects`(:send).with(:get,
'/path').yields(stub_request).returns(`@mock_response`) and the other similar
cases) to set stricter expectations on the yielded stub_request object
itself—expect calls like
stub_request.expects(:headers=).with(hash_including('headerOne' =>
'headerOneValue')) and/or stub_request.expects(:body=).with(expected_body) (or
use assert_received/assert_called equivalents) before invoking
TestHttpResource.get to ensure headers/body are actually assigned rather than
only verifying send was called.

In `@test/support/scenario_support.rb`:
- Around line 75-76: In the rescue block inside exists? that currently does
"rescue HttpResource::HttpError => e; raise unless e.code == '404'", change the
comparison to a numeric one so both string and integer codes match (e.g. use
e.code.to_i == 404 or Integer(e.code) == 404 rescue nil) and re-raise only when
the numeric comparison is not 404; keep the rescue clause (rescue
HttpResource::HttpError => e) and ensure you still re-raise for non-404
responses.

---

Outside diff comments:
In `@app/lib/katello/resources/candlepin.rb`:
- Around line 152-154: The site method currently builds the base URL from
upstream_api_uri.scheme and .host but drops upstream_api_uri.port, causing
non-default ports to be lost; update the site method (def site) to append the
port when upstream_api_uri.port is present and not the default for the scheme
(e.g., include ":#{upstream_api_uri.port}" when upstream_api_uri.port exists and
isn’t 80 for http or 443 for https) so the returned URI preserves custom
CDN/Candlepin endpoints.

---

Nitpick comments:
In `@app/controllers/katello/api/registry/registry_proxies_controller.rb`:
- Around line 48-50: The HttpResource::HttpError rescue handler (rescue_from
HttpResource::HttpError do |e|) currently logs all misses as errors; change it
to detect a 404 response (e.response_code == 404 or e.response_body includes
"BLOB_UNKNOWN") and log those cases with Rails.logger.info (or downgrade the log
level) instead of Rails.logger.error, preserving the existing body =
e.response_body || e.message assignment and only calling Rails.logger.error for
non-404/ non-BLOB_UNKNOWN errors.

In `@app/lib/actions/candlepin/environment/set_content.rb`:
- Around line 34-47: Replace the if/elsif chain that branches on e.code inside
the add-content rescue block with a case statement (case e.code; when '409' ...
when '404' ... else ... end) to satisfy Style/CaseLikeIf while preserving
existing behavior: on '409' keep the retries increment and raise when (retries
+= 1) == max_retries and refresh output[:add_ids] = content_ids - existing_ids;
on '404' keep the higher retry threshold (raise when (retries += 1) == 1_000),
compute missing_content the same way, log the debug message, and remove it from
output[:add_ids]; otherwise re-raise the exception. Ensure variables and
side-effects (retries, max_retries, output[:add_ids], content_ids, existing_ids,
and the Rails.logger.debug call) remain unchanged.

In `@test/lib/http_resource_test.rb`:
- Around line 30-40: Add a new unit test in test/lib/http_resource_test.rb that
calls TestHttpResource.query_string with parameters containing reserved
characters (e.g., values with spaces, ampersands, and equals like "a b", "c&d",
"e=f") to assert they are percent-encoded in the serialized query string; create
a method such as test_query_string_reserved_characters that passes either a Hash
or array-of-tuples matching the existing ordering expectations and asserts the
expected encoded output (e.g., space -> %20, & -> %26, = -> %3D) to catch
regression in TestHttpResource.query_string encoding behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c4161b5b-ef78-4794-9ed0-2c091b557af6

📥 Commits

Reviewing files that changed from the base of the PR and between c0e96be and 35dc4fdea5ae2931d82183a9a6901923960acfe4.

📒 Files selected for processing (44)
  • app/controllers/katello/api/registry/registry_proxies_controller.rb
  • app/controllers/katello/api/rhsm/candlepin_dynflow_proxy_controller.rb
  • app/controllers/katello/api/rhsm/candlepin_proxies_controller.rb
  • app/lib/actions/candlepin/consumer/clean_backend_objects.rb
  • app/lib/actions/candlepin/environment/add_content_to_environment.rb
  • app/lib/actions/candlepin/environment/set_content.rb
  • app/lib/actions/katello/organization/manifest_delete.rb
  • app/lib/actions/katello/organization/manifest_import.rb
  • app/lib/actions/katello/organization/manifest_refresh.rb
  • app/lib/actions/middleware/propagate_candlepin_errors.rb
  • app/lib/katello/api/v2/error_handling.rb
  • app/lib/katello/concerns/oauth_request_signing.rb
  • app/lib/katello/http_resource.rb
  • app/lib/katello/resources/candlepin.rb
  • app/lib/katello/resources/candlepin/activation_key.rb
  • app/lib/katello/resources/candlepin/c_p_user.rb
  • app/lib/katello/resources/candlepin/consumer.rb
  • app/lib/katello/resources/candlepin/content.rb
  • app/lib/katello/resources/candlepin/environment.rb
  • app/lib/katello/resources/candlepin/job.rb
  • app/lib/katello/resources/candlepin/owner.rb
  • app/lib/katello/resources/candlepin/pool.rb
  • app/lib/katello/resources/candlepin/product.rb
  • app/lib/katello/resources/candlepin/proxy.rb
  • app/lib/katello/resources/candlepin/subscription.rb
  • app/lib/katello/resources/candlepin/upstream_consumer.rb
  • app/lib/katello/resources/candlepin/upstream_entitlement.rb
  • app/lib/katello/resources/candlepin/upstream_pool.rb
  • app/lib/katello/util/candlepin_repository_checker.rb
  • app/models/katello/glue/candlepin/environment.rb
  • app/models/katello/glue/candlepin/owner.rb
  • app/models/katello/upstream_pool.rb
  • app/services/katello/product_content_importer.rb
  • app/services/katello/registration_manager.rb
  • katello.gemspec
  • test/controllers/api/rhsm/candlepin_proxies_controller_test.rb
  • test/lib/http_resource_test.rb
  • test/lib/resources/candlepin_test.rb
  • test/lib/tasks/upgrades/4.21/cleanup_python_publications_test.rb
  • test/models/upstream_pool_test.rb
  • test/scenarios/scenario_test.rb
  • test/services/katello/registration_manager_test.rb
  • test/services/katello/ui_notifications/subscriptions/manifest_expired_warning_test.rb
  • test/support/scenario_support.rb

Comment thread app/controllers/katello/api/registry/registry_proxies_controller.rb Outdated
Comment on lines +20 to +26
rescue HttpResource::HttpError => e
display_message = e.message
if e.response_body.present?
parsed = JSON.parse(e.response_body) rescue {}
display_message = parsed['displayMessage'] || display_message
end
raise ::Katello::Errors::CandlepinError, display_message

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve upstream-vs-local error typing here.

This now rewrites every HttpResource::HttpError into Katello::Errors::CandlepinError, so upstream failures lose their specialized class and any downstream rescue logic for UpstreamCandlepinError will stop matching. Please keep the service-specific mapping when re-raising instead of collapsing everything into the local Candlepin type.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/lib/actions/middleware/propagate_candlepin_errors.rb` around lines 20 -
26, The rescue currently converts every HttpResource::HttpError into
Katello::Errors::CandlepinError, losing upstream-specific types; change the
re-raise to preserve upstream error classes: after extracting display_message
(using JSON.parse of e.response_body as done), check if
e.is_a?(Katello::Errors::UpstreamCandlepinError) (or whatever upstream subclass
lives under Katello::Errors) and if so re-raise an instance of e.class with the
display_message (e.g. raise e.class, display_message, e.backtrace) so downstream
rescues still match, otherwise raise ::Katello::Errors::CandlepinError with
display_message as today.

Comment thread app/lib/katello/http_resource.rb Outdated
Comment thread app/lib/katello/resources/candlepin.rb
Comment thread app/lib/katello/resources/candlepin/activation_key.rb Outdated
Comment thread app/lib/katello/resources/candlepin/owner.rb Outdated
Comment thread app/lib/katello/resources/candlepin/upstream_pool.rb Outdated
Comment thread app/services/katello/registration_manager.rb Outdated
Comment on lines +47 to 49
@mock_conn.expects(:send).with(:get, '/path').yields(stub_request).returns(@mock_response)
TestHttpResource.get('/path', headers: { 'headerOne' => 'headerOneValue' })
end

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Assert request mutations, not just send invocation.

Line [113] makes stub_request permissive (headers/body=), so Lines [47], [62], [67], and [72] can pass even if header/body assignment regresses.

Proposed tightening
 def test_get
-  `@mock_conn.expects`(:send).with(:get, '/path').yields(stub_request).returns(`@mock_response`)
+  request = stub_request
+  `@mock_conn.expects`(:send).with(:get, '/path').yields(request).returns(`@mock_response`)
   TestHttpResource.get('/path', headers: { 'headerOne' => 'headerOneValue' })
+  assert_equal({ 'headerOne' => 'headerOneValue' }, request.headers)
 end

 def test_put
-  `@mock_conn.expects`(:send).with(:put, '/path').yields(stub_request).returns(`@mock_response`)
+  request = stub_request(expected_body: { payloadKey: 'payloadValue' })
+  `@mock_conn.expects`(:send).with(:put, '/path').yields(request).returns(`@mock_response`)
   TestHttpResource.put('/path', { payloadKey: 'payloadValue' }, headers: { 'headerOne' => 'headerOneValue' })
+  assert_equal({ 'headerOne' => 'headerOneValue' }, request.headers)
 end

 private

-def stub_request
+def stub_request(expected_body: :any)
   req = stub
   req.stubs(:headers).returns({})
-  req.stubs(:body=)
+  expected_body == :any ? req.stubs(:body=) : req.expects(:body=).with(expected_body)
   req
 end

Also applies to: 62-64, 67-74, 113-117

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/lib/http_resource_test.rb` around lines 47 - 49, The test currently only
asserts that `@mock_conn.send` was invoked and yields a permissive stub_request,
which lets regressions in header/body assignment slip by; update the tests
(e.g., the case using `@mock_conn.expects`(:send).with(:get,
'/path').yields(stub_request).returns(`@mock_response`) and the other similar
cases) to set stricter expectations on the yielded stub_request object
itself—expect calls like
stub_request.expects(:headers=).with(hash_including('headerOne' =>
'headerOneValue')) and/or stub_request.expects(:body=).with(expected_body) (or
use assert_received/assert_called equivalents) before invoking
TestHttpResource.get to ensure headers/body are actually assigned rather than
only verifying send was called.

Comment thread test/support/scenario_support.rb Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
app/lib/katello/resources/candlepin/activation_key.rb (1)

69-69: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard empty-body parsing in update_content_overrides.

At Line 69, result&.body || '{}' still passes '' through, which can raise JSON::ParserError on empty delete responses. Treat blank body as {} before parsing.

Proposed fix
-            ::Katello::Util::Data.array_with_indifferent_access(JSON.parse(result&.body || '{}'))
+            body = result&.body.presence || '{}'
+            ::Katello::Util::Data.array_with_indifferent_access(JSON.parse(body))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/lib/katello/resources/candlepin/activation_key.rb` at line 69, The
JSON.parse call in update_content_overrides uses result&.body || '{}' which
still lets an empty string get parsed and raise JSON::ParserError; before
calling JSON.parse (the expression currently written as
::Katello::Util::Data.array_with_indifferent_access(JSON.parse(result&.body ||
'{}'))), normalize the body by converting result&.body to a string and treating
blank/whitespace-only bodies as '{}' (e.g. use result&.body.to_s.strip.empty? ?
'{}' : result.body) so JSON.parse always receives a valid JSON string.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/lib/katello/resources/candlepin/upstream_consumer.rb`:
- Around line 14-15: The two exception signals in upstream_consumer.rb should
use fail instead of raise to satisfy Style/SignalException (semantic) — replace
the initial uses of raise ::Katello::Errors::UpstreamConsumerGone and raise
::Katello::Errors::UpstreamConsumerNotFound (the branches checking
response.status for [401, 410] and 404) with fail so the UpstreamConsumer error
signaling conforms to the lint rule; keep the same exception classes and
conditions (response.status checks) unchanged.

---

Duplicate comments:
In `@app/lib/katello/resources/candlepin/activation_key.rb`:
- Line 69: The JSON.parse call in update_content_overrides uses result&.body ||
'{}' which still lets an empty string get parsed and raise JSON::ParserError;
before calling JSON.parse (the expression currently written as
::Katello::Util::Data.array_with_indifferent_access(JSON.parse(result&.body ||
'{}'))), normalize the body by converting result&.body to a string and treating
blank/whitespace-only bodies as '{}' (e.g. use result&.body.to_s.strip.empty? ?
'{}' : result.body) so JSON.parse always receives a valid JSON string.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 762acb98-d805-442b-90b3-e63d26f431d6

📥 Commits

Reviewing files that changed from the base of the PR and between 35dc4fdea5ae2931d82183a9a6901923960acfe4 and 35986c571dfa9db6044701c09e3f74cc9225ce50.

📒 Files selected for processing (6)
  • app/lib/katello/resources/candlepin/activation_key.rb
  • app/lib/katello/resources/candlepin/consumer.rb
  • app/lib/katello/resources/candlepin/owner.rb
  • app/lib/katello/resources/candlepin/upstream_consumer.rb
  • app/lib/katello/resources/candlepin/upstream_job.rb
  • test/lib/resources/candlepin_test.rb
🚧 Files skipped from review as they are similar to previous changes (2)
  • app/lib/katello/resources/candlepin/consumer.rb
  • app/lib/katello/resources/candlepin/owner.rb

Comment thread app/lib/katello/resources/candlepin/upstream_consumer.rb Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/lib/katello/resources/candlepin.rb`:
- Around line 154-165: The code disables SSL verification when ca_file is nil
which makes the subsequent proxy.cacert-based cert_store (used via
Foreman::Util.add_ca_bundle_to_store and assigned to f.ssl.cert_store)
ineffective; change the logic so that when proxy&.cacert is present you enable
peer verification (set f.ssl.verify = true) and populate f.ssl.cert_store with
the cert_store, falling back to disabling verification only if neither ca_file
nor proxy.cacert are available—update the conditional flow around ca_file,
proxy.cacert, f.ssl.verify and f.ssl.cert_store in the Candlepin Faraday setup
to reflect this.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 59ecebca-290e-417c-b926-24498e830a8a

📥 Commits

Reviewing files that changed from the base of the PR and between 35986c571dfa9db6044701c09e3f74cc9225ce50 and 832c215cc74e46365d7af3a0f64a6b571705229b.

📒 Files selected for processing (6)
  • app/lib/katello/http_resource.rb
  • app/lib/katello/resources/candlepin.rb
  • app/lib/katello/resources/candlepin/activation_key.rb
  • app/lib/katello/resources/candlepin/consumer.rb
  • app/lib/katello/resources/candlepin/upstream_consumer.rb
  • test/lib/resources/candlepin_test.rb
🚧 Files skipped from review as they are similar to previous changes (1)
  • test/lib/resources/candlepin_test.rb

Comment thread app/lib/katello/resources/candlepin.rb Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (2)
app/lib/actions/middleware/propagate_candlepin_errors.rb (1)

22-28: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't collapse upstream HTTP failures into CandlepinError.

UpstreamPool.get now routes non-401/410 upstream failures through process_response, so an upstream 5xx reaches this rescue as HttpResource::HttpError and gets re-raised here as plain ::Katello::Errors::CandlepinError. That drops the upstream/local distinction and can break downstream rescues keyed on upstream failures.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/lib/actions/middleware/propagate_candlepin_errors.rb` around lines 22 -
28, The rescue currently converts every HttpResource::HttpError into
::Katello::Errors::CandlepinError, losing upstream-vs-local distinction; update
the rescue in propagate_candlepin_errors.rb so that if the caught
HttpResource::HttpError represents an upstream failure (e.g. response code in
the 5xx range or otherwise coming from UpstreamPool/process_response), re-raise
the original HttpResource::HttpError (raise e) to preserve its type, otherwise
continue to extract display_message from e.response_body and raise
::Katello::Errors::CandlepinError with that message; reference the
HttpResource::HttpError rescue block and ::Katello::Errors::CandlepinError and
consider using e.response_code or any upstream marker set by
UpstreamPool.get/process_response to decide which path to take.
app/lib/katello/http_resource.rb (1)

68-71: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Preserve plain-text error bodies in HttpError#message.

When JSON parsing fails, Line 71 replaces the upstream body with the parser exception text. PropagateCandlepinErrors seeds its display message from e.message, so plain-text Candlepin failures will surface as JSON parse noise instead of the actual response body. Prefer resp.body.presence || error.to_s here.

Suggested fix
-          fail HttpError, {:message => error.to_s, :service_code => service_code, :code => status_code, :response_body => resp.body}, caller
+          fail HttpError, {:message => resp.body.presence || error.to_s, :service_code => service_code, :code => status_code, :response_body => resp.body}, caller
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/lib/katello/http_resource.rb` around lines 68 - 71, In the rescue block
that raises HttpError (the rescue => error in app/lib/katello/http_resource.rb),
preserve the original response body when building the HttpError message instead
of always using the parser exception; change the error payload so :message is
resp.body.presence || error.to_s (or resp.body || error.to_s if presence isn't
available), keeping the rest of the HttpError arguments the same so
PropagateCandlepinErrors can surface plain-text Candlepin responses.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@app/lib/actions/middleware/propagate_candlepin_errors.rb`:
- Around line 22-28: The rescue currently converts every HttpResource::HttpError
into ::Katello::Errors::CandlepinError, losing upstream-vs-local distinction;
update the rescue in propagate_candlepin_errors.rb so that if the caught
HttpResource::HttpError represents an upstream failure (e.g. response code in
the 5xx range or otherwise coming from UpstreamPool/process_response), re-raise
the original HttpResource::HttpError (raise e) to preserve its type, otherwise
continue to extract display_message from e.response_body and raise
::Katello::Errors::CandlepinError with that message; reference the
HttpResource::HttpError rescue block and ::Katello::Errors::CandlepinError and
consider using e.response_code or any upstream marker set by
UpstreamPool.get/process_response to decide which path to take.

In `@app/lib/katello/http_resource.rb`:
- Around line 68-71: In the rescue block that raises HttpError (the rescue =>
error in app/lib/katello/http_resource.rb), preserve the original response body
when building the HttpError message instead of always using the parser
exception; change the error payload so :message is resp.body.presence ||
error.to_s (or resp.body || error.to_s if presence isn't available), keeping the
rest of the HttpError arguments the same so PropagateCandlepinErrors can surface
plain-text Candlepin responses.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 861316c2-49f2-4c75-aadd-a353fd4e065c

📥 Commits

Reviewing files that changed from the base of the PR and between 832c215cc74e46365d7af3a0f64a6b571705229b and ac34a649a6d3c63217a4211624fea02096623ed9.

📒 Files selected for processing (7)
  • app/controllers/katello/api/registry/registry_proxies_controller.rb
  • app/lib/actions/middleware/propagate_candlepin_errors.rb
  • app/lib/katello/http_resource.rb
  • app/lib/katello/resources/candlepin.rb
  • app/lib/katello/resources/candlepin/upstream_consumer.rb
  • app/lib/katello/resources/candlepin/upstream_pool.rb
  • test/support/scenario_support.rb
🚧 Files skipped from review as they are similar to previous changes (4)
  • app/controllers/katello/api/registry/registry_proxies_controller.rb
  • test/support/scenario_support.rb
  • app/lib/katello/resources/candlepin/upstream_consumer.rb
  • app/lib/katello/resources/candlepin.rb

@pablomh pablomh force-pushed the faraday-persistent-connections branch from ac34a64 to f3669c4 Compare May 19, 2026 00:14

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/lib/katello/concerns/oauth_request_signing.rb`:
- Line 31: Remove the trailing comma from the hash literal that includes the
:ca_file => self.ssl_ca_file entry in the OAuthRequestSigning concern; locate
the options/hash construction (references: :ca_file key and ssl_ca_file method)
and delete the final comma so the argument list/hard-coded hash is valid for
RuboCop.
- Around line 17-19: The guard in sign_request currently only rejects nil/false
and allows empty strings; update the check in sign_request to reject blank
credentials by verifying consumer_key and consumer_secret are not
nil/empty/whitespace (e.g. use consumer_key.to_s.strip.empty? /
consumer_secret.to_s.strip.empty? or consumer_key.blank? if ActiveSupport is
available) and raise the same failure message if either is blank before calling
build_oauth_header so misconfiguration surfaces immediately.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 80bf860c-e16d-4c92-9310-5a75a8b37033

📥 Commits

Reviewing files that changed from the base of the PR and between ac34a649a6d3c63217a4211624fea02096623ed9 and f3669c459387fb24a852ff11d37402a54d60f714.

📒 Files selected for processing (11)
  • app/controllers/katello/api/registry/registry_proxies_controller.rb
  • app/lib/actions/candlepin/environment/set_content.rb
  • app/lib/actions/middleware/propagate_candlepin_errors.rb
  • app/lib/katello/api/v2/error_handling.rb
  • app/lib/katello/concerns/oauth_request_signing.rb
  • app/lib/katello/http_resource.rb
  • app/lib/katello/resources/candlepin.rb
  • app/lib/katello/resources/candlepin/upstream_consumer.rb
  • app/lib/katello/resources/candlepin/upstream_pool.rb
  • app/services/katello/registration_manager.rb
  • test/support/scenario_support.rb
🚧 Files skipped from review as they are similar to previous changes (8)
  • app/lib/katello/resources/candlepin/upstream_pool.rb
  • app/services/katello/registration_manager.rb
  • app/lib/actions/middleware/propagate_candlepin_errors.rb
  • app/lib/katello/api/v2/error_handling.rb
  • app/lib/katello/resources/candlepin/upstream_consumer.rb
  • app/lib/actions/candlepin/environment/set_content.rb
  • app/lib/katello/resources/candlepin.rb
  • app/lib/katello/http_resource.rb

Comment thread app/lib/katello/concerns/oauth_request_signing.rb
Comment thread app/lib/katello/concerns/oauth_request_signing.rb Outdated
@pablomh pablomh force-pushed the faraday-persistent-connections branch from bcc28f1 to f899012 Compare May 19, 2026 10:02
@pablomh pablomh force-pushed the faraday-persistent-connections branch 2 times, most recently from c5bcdbd to 26bd7d5 Compare May 19, 2026 13:26
@ekohl

ekohl commented May 19, 2026

Copy link
Copy Markdown
Member

Prerequisite: theforeman/foreman_azure_rm#209 must merge first — the retired Azure SDK pins faraday < 2 via ms_rest, which blocks this upgrade.

Don't forget https://github.com/theforeman/foreman_google that also pins to Faraday < 2.

Rails.logger.error(pp_exception(e, with_backtrace: false))
end
body = e.response_body.presence || e.message
if request_from_katello_cli?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm surprised by this mechanism. I know it was already there, but Rails normally uses respond_to. I think this would be the Rails native mechanism, assuming the CLI sets Accept: application/json as a header:

respond_to do |format|
  format.json { render json: { errors: [body] }, status: e.code }
  format.any { render plain: body, status: e.code }
end

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I checked the current Hammer stack and it does reliably send Accept: application/json;version=... via apipie-bindings, so respond_to should work for the maintained CLI path. You're also right that request_from_katello_cli? appears to be legacy behavior tied to the old Python-based Katello CLI rather than modern Hammer. My only hesitation is calling it fully dead code without scoping that claim — the registry endpoints are more likely hit by container tooling (podman, skopeo) and scripts than by Hammer itself. If we're comfortable aligning this with maintained clients rather than preserving the old User-Agent behavior, I think respond_to is reasonable — but I'd do it as a follow-up that updates all the sibling controllers consistently rather than just this one handler.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed it belongs in a separate PR.

Replace the unmaintained RestClient gem with Faraday 2.x and the
net_http_persistent adapter to enable persistent TCP+TLS connection
reuse between Puma and Candlepin, eliminating per-request connection
setup overhead during concurrent host registration.

Key changes:

- Replace RestClient::Resource with Faraday::Connection, memoized at
  the class level for connection pooling across Puma threads
- Extract OAuth 1.0a request signing into OauthRequestSigning concern,
  making the auth mechanism a pluggable strategy (preparation for
  CANDLEPIN-222 mTLS transition)
- Add sign_request hook on HttpResource as extension point
- Memoize OAuth::Consumer object to avoid per-request allocation
- Fix Proxy class to sign requests via concern
- Fix CandlepinProxiesController to use Faraday response API
- Fix DELETE-with-body paths to include OAuth signing
- Fix UpstreamEntitlement bracket notation (RestClient-era API)
- Add defensive stringify_headers helper for Net::HTTP compatibility
- Update gemspec: faraday >= 2.0, faraday-net_http_persistent,
  faraday-multipart

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
pablomh and others added 29 commits June 23, 2026 15:54
- Add process: false option to issue_request for pass-through callers
  that need raw Faraday::Response without error raising
- Rewrite Proxy to delegate to CandlepinResource.issue_request instead
  of duplicating connection, signing, and header logic
- Route UpstreamPool.get through issue_request for consistent logging
  and connection handling
- Memoize UpstreamCandlepinResource.faraday_connection for connection
  reuse on upstream manifest operations

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- rescue_from_exception_with_response now uses the actual HTTP status
  code from HttpError instead of hardcoding :bad_request (resolves
  long-standing TODO)
- upstream_owner_id routes through issue_request instead of raw
  conn.get with rescue Faraday::Error
- Add comment on resource vs faraday_connection lifecycle in
  UpstreamCandlepinResource

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Generated get/delete now accept (path, headers:, params:).
Generated post/put/patch now accept (path, payload, headers:, params:).

Eliminates fragile positional arg parsing and makes params: available
to all callers without dropping to issue_request. All 41 call sites
and super calls updated.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- rescue_from_exception_with_response falls back to :bad_request when
  code.to_i yields 0 (empty or missing code)
- Add comment on Owner#import explaining why multipart bypasses
  issue_request

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- UpstreamConsumer#ping uses issue_request instead of resource + conn.head
- Consumer and ActivationKey DELETE-with-body use issue_request instead
  of raw conn.delete with manual signing and headers
- UpstreamEntitlement#update uses issue_request instead of resource + conn.put

Only Owner#import (multipart, documented) and three UpstreamConsumer
methods (custom URL/certs per call) remain outside issue_request.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add connection: parameter to issue_request so callers with custom
Faraday connections (different URL/certs) still go through the unified
request pipeline (logging, signing, headers, error handling).

All Candlepin HTTP traffic now flows through issue_request. Only
Owner#import (multipart with separate Faraday stack) remains outside.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When connection: is provided, the path alone is insufficient for
debugging since the host varies per call. Now logs the full URL
(url_prefix + path) for custom connections, plain path otherwise.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- UpstreamConsumer#ping: check response.status directly instead of
  relying on HttpError rescue (process: false never raises HttpError)
- OAuth signing URL now derived from conn.url_prefix when connection:
  is provided, ensuring correct signing for any future OAuth-backed
  custom connection

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Test issue_request with custom connection: parameter
- Test process: false returns raw Faraday::Response
- Test UpstreamConsumer#ping maps 401/410 to UpstreamConsumerGone
  and 404 to UpstreamConsumerNotFound
- Verify no positional header calls in engines/

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Use logger.debug { ... } blocks in issue_request and process_response
  so serialization (to_json, filter_sensitive_data) is skipped when
  debug logging is disabled
- Consolidate duplicate url/sign_url computation into single full_url
- Memoize upstream_owner_id to avoid repeated GET on every upstream
  path resolution (UpstreamPool.path, UpstreamOwner.path)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Memoized faraday_connection and upstream_owner_id can become stale
after manifest refresh changes the upstream identity cert. Callers
can invoke reset_connection! to clear cached state.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Clears memoized faraday_connection and upstream_owner_id after
manifest operations that may change the upstream identity cert,
preventing stale credentials on subsequent upstream pool listings.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Both faraday_connection and upstream_owner_id are now keyed by
Organization.current.id, preventing cross-org data leaks in
multi-threaded Puma serving multiple organizations concurrently.

reset_connection! clears only the current org's cached entries.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Dynflow workers may not have Organization.current set during manifest
finalize, so per-org deletion would miss the target. Full clear is
safe since manifest operations are rare and rebuild cost is trivial.
Also prevents unbounded hash growth in long-running processes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…tion

- Owner.destroy_imports, Owner.imports: parse response.body not response
- Consumer.async_hypervisors: parse response.body
- Consumer.update_content_overrides: parse result.body, early return on empty
- ActivationKey.update_content_overrides: parse result&.body, early return on empty
- UpstreamConsumer.get: fix ?/& separator for include_only with empty params
- UpstreamJob.get: parse response.body from start_upstream_export
- Consumer.get: safe query string construction matching UpstreamConsumer pattern
- Owner.import: add open_timeout for consistency with other Faraday connections
- Add tests for Consumer.get URL composition and empty content-override edge cases

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…des_for

- build_path: safe URL composition for base path + params + includes +
  pagination, eliminating manual ?/& assembly in Consumer.get and
  UpstreamConsumer.get
- parse_json: shared JSON response parsing with optional array mode,
  available for gradual adoption across resource classes
- update_content_overrides_for: deduplicate identical logic between
  Consumer and ActivationKey, preventing fix drift
- Dup params before mutating with .delete(:include_only) to avoid
  leaking side effects to callers

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Style/MutableConstant: freeze TOTAL_COUNT_HEADER
- Style/SignalException: use fail instead of raise
- Style/CaseLikeIf: convert if-elsif to case-when in http_resource
- Lint/UnusedMethodArgument: prefix unused id with underscore
- Minitest/AssertEmptyLiteral: use assert_empty instead of assert_equal []

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add trailing comma after last item in multiline keyword args in
update_content_overrides_for issue_request call.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- registry_proxies_controller: use .presence for empty body fallback
- propagate_candlepin_errors: re-raise CandlepinError subclasses to
  preserve upstream error typing
- http_resource: keep non-JSON error responses as HttpError instead of
  downgrading to NetworkException
- upstream_owner_id: remove process: false so HTTP errors are handled
  normally instead of silently returning nil
- upstream_pool: guard non-2xx responses, map 401 to UpstreamConsumerGone,
  delegate other errors to process_response
- scenario_support: normalize code comparison with .to_s
- upstream resource: set ssl.verify based on whether trust anchors
  (ca_file or proxy cacert) are available, not unconditionally false

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Align exception signaling and case-style handling with the project RuboCop rules so CI passes without changing the Faraday migration behavior.

Co-authored-by: Cursor <cursoragent@cursor.com>
Clarify the HTTP-versus-network failure boundary and harden the shared Candlepin helpers so the Faraday migration preserves upstream messages, custom ports, and cleanup safety.

Co-authored-by: Cursor <cursoragent@cursor.com>
Lock in the new transport, upstream URL, empty-body parsing, and cleanup restoration behavior so the Faraday follow-up remains reviewable and stable.

Co-authored-by: Cursor <cursoragent@cursor.com>
- UpstreamPoolTest: stub response with body: and status: instead of
  to_str: (Faraday response API, not RestClient)
- CoreTest: replace test_with_excon with test_with_net_http_persistent
  (excon adapter not available in Faraday 2.x)
- ManifestExpiredWarningTest: stub cdn_inaccessible? directly instead
  of raising HttpError from Net::HTTP (CDN uses RestClient, not Faraday)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The Pulp3 publications API path uses PulpcoreClient (which depends on
RestClient), not our Faraday-based HttpResource. The production code at
cleanup_python_publications.rake:147 rescues RestClient::NotFound, so
the test must raise that same exception type.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Update RHSM routing expectations and permission coverage so the new compliance endpoints match their dedicated actions in CI.

Co-authored-by: Cursor <cursoragent@cursor.com>
- Revert routing test and access_permissions_test changes that belong
  to PR Katello#11731, not this branch (compliance routes unchanged here)
- Fix UpstreamPoolTest header stub: use string key 'x-total-count'
  instead of symbol x_total_count (Faraday doesn't auto-normalize)
- Fix trailing slash in ConsumerResource/OwnerResource path when id
  is nil: /consumers/ -> /consumers (Faraday doesn't strip it)
- Add :net_http_persistent to pulp3_ssl_configuration case statement
  alongside :net_http (same SSL cert types)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Use the same pessimistic Faraday 2.x dependency constraint as the matching smart-proxy transport change for consistency across the related migrations.

Co-authored-by: Cursor <cursoragent@cursor.com>
@pablomh pablomh force-pushed the faraday-persistent-connections branch from 3d91add to 7de46e8 Compare June 23, 2026 18:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants