Skip to content

feat(semantic-layer): inject a bounded provider HTTP transport - #45553

Open
mikebridge wants to merge 26 commits into
apache:masterfrom
mikebridge:sc-126317-provider-transport
Open

mikebridge wants to merge 26 commits into
apache:masterfrom
mikebridge:sc-126317-provider-transport

Conversation

@mikebridge

Copy link
Copy Markdown
Contributor

SUMMARY

TL;DR: Give semantic-layer extensions a host-provided HTTP transport with a mandatory deadline and response byte cap. Keep destination policy in Superset.

Background: Providers need HTTP during construction, schema discovery and query execution. The SDK currently has no portable HTTP capability, so provider helpers can depend on a particular host's private code.

Problem: Moving those helpers without preserving address pinning, TLS, redirect checks and absolute deadlines would weaken the existing transport. A timeout on an individual read does not bound DNS, slow framing or a full multi-request operation.

What this changes:

  • Add library-neutral SDK request-budget, response, transport and error types.
  • Require a separate transport= keyword on provider construction and both schema-discovery entry points. Update all five host call sites and in-tree providers/fixtures together; keep static read-only schema access and memo identity unchanged.
  • Resolve once, reject any unsafe answer, connect to a checked numeric address, and retain the original TLS hostname. GET follows at most three same-origin redirects. Error and refused-redirect bodies are never read.
  • Bound DNS waiting, connect, TLS, headers, framing and body reads by one monotonic deadline, using a lazy shared resolver and socket deadline scheduler. Accept identity encoding only and enforce a response-byte cap on every call.
  • Declare SEMANTIC_LAYER_ALLOW_INTERNAL_HOSTS = False. Opting in permits internal/HTTP endpoints but retains budgets, TLS verification, redirect checks and direct connections without environment proxies. Internal mode honors REQUESTS_CA_BUNDLE / CURL_CA_BUNDLE for private TLS certificates; neither mode discovers .netrc credentials.
  • Include sanitized provider display names in errors/logs, reset lazy workers after fork, remove completed deadlines lazily, and retain urllib3 socket options. Non-string provider labels use a safe fallback without coercion or leaking a URL-bearing exception chain.
  • Preserve safe transport messages in metadata API failures while keeping fixed fallback messages for other provider exceptions.
  • Add provider-author documentation and an upgrade note for the required keyword.

This prepares the unreleased SDK contract. Existing downstream provider bundles must migrate their HTTP helpers and advance their core pin together in a separate change. This PR does not claim that old bundles work against the new signatures.

BEFORE/AFTER SCREENSHOTS OR ANIMATED GIF

Not applicable: no UI change.

TESTING INSTRUCTIONS

Run from this checkout using its development environment:

PYTHONPATH=.:superset-core/src python -m pytest -q -o addopts='' \
  tests/unit_tests/semantic_layers tests/unit_tests/commands/semantic_layer \
  superset-core/tests tests/unit_tests/utils/test_network.py

The tests use local listeners and fakes, with an explicit loopback-only socket check in the transport suite. They cover all 33 original transport test functions, plus complete-answer DNS rejection, no second resolution, every stalled phase, exact/exceeded byte caps, safe logs, unread errors/redirects, and ten concurrent requests with descriptor cleanup. The connect-stall fixture blocks real socket I/O at the connect boundary; it is not a kernel SYN-blackhole experiment.

The same phase/concurrency cases are also exercised in a fresh subprocess that calls gevent.monkey.patch_all() before importing pytest. A real Apache app factory boot loads the updated local stub against disposable SQLite; no live provider, real credentials, shared database or container is used.

Final candidate validation: 1,901 semantic-layer tests passed with 100.00% coverage, 53 SDK tests passed, and 7 SDK conformance registration tests passed. Changed-file hooks including MyPy passed. Earlier transport validation also passed 16 isolated-gevent cases; those were not rerun for the dead-code removal and master merge. All 33 reference transport test functions are ported. New regression tests cover closed deadlines, shutdown errors, fork reset and invalid URLs.

Follow-up: harden the standalone ProviderRequestError constructor against string/integer subclasses that override formatting, and validate status fields. The stacked host boundary validates these fields independently; this constructor hardening is not included here.

ADDITIONAL INFORMATION

  • Has associated issue
  • Required feature flags: existing SEMANTIC_LAYERS availability gate; provider destination opt-out is a default-off operator configuration setting.
  • Changes UI
  • Includes DB Migration
  • Introduces new feature or API: SDK ProviderTransport and required keyword.
  • Removes existing feature or API: old entry-point signatures no longer apply.

Compatibility: this is a first-release SDK gate. Update provider bundles in lockstep with the host SDK before enabling them. Rollback requires the matching host/bundle pair; this change makes no metadata database writes or migrations. OAuth and webhook transport settings are unaffected. OS DNS calls already in progress cannot be cancelled; caller waiting is bounded and the resolver uses at most eight workers. Independent security review completed; remote CI and downstream bundle/query acceptance are still pending. No live-provider acceptance is claimed.

Port coverage

All Apache targets are in tests/unit_tests/semantic_layers/transport_test.py. Proxy behavior and error wording changes below are intentional; every call is bounded.

Shell case Apache case Contract disposition
test_https_guard_ignores_background_requests test_https_guard_ignores_background_requests Same outcome; SDK adapter supplies deadline and byte cap.
test_internal_address_is_refused_before_connecting test_internal_address_is_refused_before_connecting Same outcome; SDK adapter supplies deadline and byte cap.
test_any_internal_answer_refuses_the_host test_any_internal_answer_refuses_the_host Same outcome; SDK adapter supplies deadline and byte cap.
test_translated_ipv6_answers_are_checked_as_ipv4 test_translated_ipv6_answers_are_checked_as_ipv4 Same outcome; SDK adapter supplies deadline and byte cap.
test_checked_connection_ignores_background_connections test_checked_connection_ignores_background_connections Same outcome; SDK adapter supplies deadline and byte cap.
test_public_tls_ignores_background_loopback_lookups test_public_tls_ignores_background_loopback_lookups Same outcome; SDK adapter supplies deadline and byte cap.
test_default_path_verifies_the_configured_hostname test_default_path_verifies_the_configured_hostname Same outcome; SDK adapter supplies deadline and byte cap.
test_default_path_follows_same_origin_get_redirects_only test_default_path_follows_same_origin_get_redirects_only Same outcome; SDK adapter supplies deadline and byte cap.
test_flag_allows_local_http_development_endpoints test_flag_allows_local_http_development_endpoints Same outcome; SDK adapter supplies deadline and byte cap.
test_flag_off_keeps_local_endpoints_refused test_flag_off_keeps_local_endpoints_refused Same outcome; SDK adapter supplies deadline and byte cap.
test_post_redirects_are_not_followed test_post_redirects_are_not_followed Same outcome; SDK adapter supplies deadline and byte cap.
test_redirect_chains_are_bounded test_redirect_chains_are_bounded Same outcome; SDK adapter supplies deadline and byte cap.
test_error_message_carries_status_only test_error_message_carries_provider_and_status_only Sanitized provider display name and status in errors; provider/status/category-only logs; non-2xx bodies unread.
test_transport_failure_hides_the_destination test_transport_failure_hides_the_destination Same outcome; SDK adapter supplies deadline and byte cap.
test_failure_logs_never_carry_url_secrets test_failure_logs_never_carry_url_secrets Sanitized provider display name and status in errors; provider/status/category-only logs; non-2xx bodies unread.
test_malformed_redirects_raise_the_safe_error test_malformed_redirects_raise_the_safe_error Sanitized provider display name and status in errors; provider/status/category-only logs; non-2xx bodies unread.
test_malformed_endpoint_urls_raise_the_safe_error test_malformed_endpoint_urls_raise_the_safe_error Sanitized provider display name and status in errors; provider/status/category-only logs; non-2xx bodies unread.
test_bounded_body_is_read_through_the_checked_connection test_bounded_body_is_read_through_the_checked_connection Same outcome; SDK adapter supplies deadline and byte cap.
test_bounded_body_is_refused_past_its_byte_cap test_bounded_body_is_refused_past_its_byte_cap Same outcome; SDK adapter supplies deadline and byte cap.
test_bounded_body_stops_at_the_deadline_while_dripping test_bounded_body_stops_at_the_deadline_while_dripping Same outcome; SDK adapter supplies deadline and byte cap.
test_bounded_read_after_progress_waits_only_for_the_time_left test_bounded_read_after_progress_waits_only_for_the_time_left Same outcome; SDK adapter supplies deadline and byte cap.
test_bounded_lookup_waits_no_longer_than_the_deadline test_bounded_lookup_waits_no_longer_than_the_deadline Same outcome; SDK adapter supplies deadline and byte cap.
test_bounded_body_refuses_internal_destinations test_bounded_body_refuses_internal_destinations Same outcome; SDK adapter supplies deadline and byte cap.
test_bounded_error_bodies_are_not_read_or_logged test_bounded_error_bodies_are_not_read_or_logged Sanitized provider display name and status in errors; provider/status/category-only logs; non-2xx bodies unread.
test_bounded_body_refuses_encoded_responses test_bounded_body_refuses_encoded_responses Same outcome; SDK adapter supplies deadline and byte cap.
test_bounded_post_redirects_are_not_followed test_bounded_post_redirects_are_not_followed Same outcome; SDK adapter supplies deadline and byte cap.
test_bounded_body_refuses_an_expired_deadline test_bounded_body_refuses_an_expired_deadline Same outcome; SDK adapter supplies deadline and byte cap.
test_bounded_redirect_bodies_are_never_read test_bounded_redirect_bodies_are_never_read Same outcome; SDK adapter supplies deadline and byte cap.
test_bounded_framing_obeys_the_absolute_deadline test_bounded_framing_obeys_the_absolute_deadline Same outcome; SDK adapter supplies deadline and byte cap.
test_bounded_lookup_and_connect_attempts_share_the_deadline test_bounded_lookup_and_connect_attempts_share_the_deadline Same outcome; SDK adapter supplies deadline and byte cap.
test_bounded_internal_requests_ignore_environment_proxies test_bounded_internal_requests_ignore_environment_proxies Same outcome; SDK adapter supplies deadline and byte cap.
test_unbounded_internal_requests_keep_environment_proxies test_former_unbounded_internal_requests_ignore_environment_proxies Intentional policy change: formerly unbounded opt-out also ignores environment proxies; direct loopback succeeds.
test_dns_failure_is_not_an_address_policy_refusal test_dns_failure_is_not_an_address_policy_refusal Same outcome; SDK adapter supplies deadline and byte cap.

@github-actions github-actions Bot added api Related to the REST API doc Namespace | Anything related to documentation labels Oct 21, 2026
@codecov

codecov Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 30.93923% with 250 lines in your changes missing coverage. Please review.
✅ Project coverage is 65.66%. Comparing base (7f2b04f) to head (c0e6f0f).
⚠️ Report is 6 commits behind head on master.

Files with missing lines Patch % Lines
superset/semantic_layers/transport.py 28.21% 172 Missing and 1 partial ⚠️
superset/utils/network.py 36.89% 62 Missing and 3 partials ⚠️
superset/semantic_layers/api.py 16.66% 10 Missing ⚠️
superset/semantic_layers/execution.py 66.66% 1 Missing ⚠️
superset/semantic_layers/masking.py 50.00% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           master   #45553       +/-   ##
===========================================
- Coverage   84.73%   65.66%   -19.08%     
===========================================
  Files        3069     3075        +6     
  Lines      205819   206823     +1004     
  Branches    48408    48450       +42     
===========================================
- Hits       174403   135800    -38603     
- Misses      28122    67933    +39811     
+ Partials     3294     3090      -204     
Flag Coverage Δ
hive 34.48% <30.93%> (-0.02%) ⬇️
mysql 52.53% <30.93%> (?)
postgres 52.55% <30.93%> (-0.05%) ⬇️
presto 36.20% <30.93%> (-0.03%) ⬇️
python 52.81% <30.93%> (-34.95%) ⬇️
sqlite 52.30% <30.93%> (-0.07%) ⬇️
superset-extensions-cli 91.24% <ø> (?)
unit ?

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@netlify

netlify Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for superset-docs-preview ready!

Name Link
🔨 Latest commit c0e6f0f
🔍 Latest deploy log https://app.netlify.com/projects/superset-docs-preview/deploys/6ae559f99a73472565d9deb8
😎 Deploy Preview https://deploy-preview-45553--superset-docs-preview.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@github-actions github-actions Bot added the i18n Namespace | Anything related to localization label Oct 21, 2026
@github-actions github-actions Bot added the requires:rebase Requires rebasing on top of current master label Oct 21, 2026
Keep bulk-validation uniqueness assertions and transport error coverage together. Move the transport contract into master's consolidated provider guide and update its isolated schema example.
@github-actions github-actions Bot removed the requires:rebase Requires rebasing on top of current master label Oct 23, 2026
@mikebridge
mikebridge marked this pull request as ready for review October 23, 2026 23:35
Comment thread superset/utils/network.py Outdated
Comment on lines +296 to +304
if _RESOLVER is None:
_RESOLVER = ThreadPoolExecutor(8, "provider-dns")
lookup: Future[Any] = _RESOLVER.submit(
socket.getaddrinfo, host, port, socket.AF_UNSPEC, socket.SOCK_STREAM
)
try:
answers: Any = lookup.result(timeout=timeout)
finally:
lookup.cancel() # Queued work is disposable; running system DNS cannot stop.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggestion: When eight system DNS calls stall, Future.cancel() cannot stop running lookups; the shared pool stays exhausted, so otherwise valid provider requests queue and time out.

Assessment: 🟠 Major · 🔁 Occurrence: Rarely · 🏷️ Performance

Use CodeAnt Skill Fix in Cursor Fix in VSCode Claude

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** superset/utils/network.py
**Line:** 296:304
**Comment:**
	*Performance: When eight system DNS calls stall, `Future.cancel()` cannot stop running lookups; the shared pool stays exhausted, so otherwise valid provider requests queue and time out.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

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—Future.cancel() on a not-yet-running lookup only marks it cancelled; the work item stays in the executor queue until a worker pops it. With the eight workers stuck, 108 timed-out calls left 100 cancelled items queued (observed), so memory grows even though every caller has already failed. Would a bounded submission count, or a shared in-flight limit that fails fast, address it?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Yes. Bound all outstanding resolver submissions—both running lookups and queued work—and wait for a slot only until the request deadline. If capacity is exhausted, fail the request instead of adding another item to the executor’s unbounded queue.

The accounting matters: don’t release a slot merely because the caller timed out or Future.cancel() succeeded. A cancelled queued future can remain in the queue, and cancellation may mark it done immediately. Either leave timed-out futures submitted and release their slots only when the worker actually completes them, or use a bounded queue that can reliably account for dequeue/removal.

This prevents unbounded queue growth and makes overload fail fast. It won’t restore DNS availability while all resolver threads are stuck in uncancellable system lookups; recovering from that would require stronger isolation, such as resolver processes.

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.

Good catch. DNS admission now bounds outstanding submissions as well as workers. A timed-out request keeps its slot until the submitted lookup actually finishes, and a saturated pool fails immediately instead of queueing. A blocked-worker test covers queued work and slot recovery afterwards, in e791fe9.

Comment thread superset/semantic_layers/transport.py Resolved
@github-actions github-actions Bot removed the i18n Namespace | Anything related to localization label Oct 26, 2026
Comment thread superset/semantic_layers/transport.py Resolved
@github-actions github-actions Bot removed the requires:rebase Requires rebasing on top of current master label Oct 27, 2026
@github-actions github-actions Bot added the requires:rebase Requires rebasing on top of current master label Oct 27, 2026
Comment thread superset/utils/network.py Resolved
Merge apache master while retaining provider error normalization and transport injection. Address Joe's r4394124322 by submitting DNS work outside the native lock and creating the scheduler lock after gevent patching; publish only after successful startup. Preserve timeout classification through HTTP wrappers. Address r4394124328 with real-response body tripwires and set the test certificate context's TLS minimum.
Guard only lazy lock publication so native threads share one scheduler while preload gevent startup remains outside the native guard. Reset the guard after fork. Keep host transport construction outside provider fault attribution. Add widened-window race and host-fault regression coverage for the two review LOWs.
@github-actions github-actions Bot removed the requires:rebase Requires rebasing on top of current master label Oct 28, 2026
Comment thread superset/semantic_layers/transport.py Resolved
Close each abandoned response and its deadline-owned duplicate descriptors before following the next hop, so single-threaded keep-alive providers can accept it. Preserve the original deadline across hops. Add the real-server regression and strengthen refused-body and non-default-port tests for Joe's three redirect review threads.
Exercise the no-scheduler release path without initializing deadline infrastructure, closing the final branch-coverage gap while keeping production behavior unchanged.
@github-actions github-actions Bot added the requires:rebase Requires rebasing on top of current master label Oct 28, 2026
Comment thread superset/semantic_layers/transport.py Resolved
@github-actions github-actions Bot removed the requires:rebase Requires rebasing on top of current master label Oct 29, 2026
session.mount("http://", adapter)
hop: int
for hop in range(MAX_REDIRECTS + 1):
with session.request(

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.

With LOG_LEVEL=DEBUG, urllib3's connection-pool logger writes the request path and query string for this call, so a provider URL like https://provider.example/catalog?token=SECRET puts the token in the application log on every successful request. The sanitized warning in this module doesn't cover those records. Should the transport redact or suppress urllib3.connectionpool debug output for provider calls?

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.

Good catch, fixed in b54ff69. The transport now filters urllib3.connectionpool records only while a provider request is active. A DEBUG loopback test fails without the filter (the query token shows up in the log) and checks that unrelated concurrent HTTP still logs.

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.

This only covers the urllib3.connectionpool logger. With the pinned urllib3 2.8.0, a header-parse failure is logged at WARNING by urllib3.connection as Failed to parse headers (url=...), and that URL includes the query string. A provider reply of HTTP/1.1 200 OK, Content-Length: 2, then a line like Not-A-Header would put ?token=SECRET in the application log at default levels, before the transport sanitizes the error. Could the filter also cover the urllib3.connection logger?

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.

Good catch. The header-parse warning from urllib3.connection carried the full URL. The request-scoped filter now covers that logger too (transport.py:62-71). test_malformed_provider_headers_do_not_log_query_tokens sends a reply with a malformed header line and checks that ?token=SECRET never reaches a log record. It fails without the change.

Comment thread tests/unit_tests/utils/test_network.py Resolved
Comment thread UPDATING.md Resolved
with (
ThreadPoolExecutor(1) as worker,
_framing_server(public_tls[0], mode) as (port, ready, release),
_internal_hosts(allow_internal),

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.

_internal_hosts(allow_internal) enters its Flask app context in the pytest thread, but provider_body calls build_provider_transport() inside the ThreadPoolExecutor worker, which has no app context. Both parameter values therefore build a transport with allow_internal_hosts=False, and the allow_internal=True cases never reach the internal-host connection path. For example, changing _PinnedHTTPConnection._new_conn to call bound.watch(sock) only when public_only is set would still pass these framing-deadline cases. Could the transport be built inside the app context before it is submitted to the worker, or the context pushed in the worker?

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.

You were right: the worker thread had no app context, so both parameters built a public-only transport. The test now builds the transport inside the app context and only sends the request from the worker. The internal cases use plain HTTP on 127.0.0.1, so they reach the internal-host connection path. Moving construction back into the worker makes the allow_internal=True cases fail again.

@github-actions github-actions Bot added the requires:rebase Requires rebasing on top of current master label Oct 29, 2026
Joe review threads r4415570656 and r4415570062 identified a urllib3.connection header warning that could log provider query tokens and a framing test whose worker lost the Flask internal-host context. Filter the warning during provider requests and build the test transport inside that context so the internal path is exercised.
@github-actions github-actions Bot added requires:rebase Requires rebasing on top of current master and removed requires:rebase Requires rebasing on top of current master labels Oct 30, 2026
@github-actions github-actions Bot removed the requires:rebase Requires rebasing on top of current master label Oct 31, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api Related to the REST API doc Namespace | Anything related to documentation size/XXL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants