Skip to content

feat(fetch_azure): adapt fetch HttpClient to Azure's HttpClient#494

Draft
martintmk wants to merge 22 commits into
mainfrom
user/martintomka/20260612-fetch-azure-crate
Draft

feat(fetch_azure): adapt fetch HttpClient to Azure's HttpClient#494
martintmk wants to merge 22 commits into
mainfrom
user/martintomka/20260612-fetch-azure-crate

Conversation

@martintmk

@martintmk martintmk commented Jun 12, 2026

Copy link
Copy Markdown
Member

Summary

Adds two crates that adapt Oxidizer building blocks to the Azure SDK abstractions, built directly on typespec_client_core 1.0.0 (the crate that defines the HttpClient and AsyncRuntime traits that azure_core re-exports):

  • fetch_azureAzureHttpClient implements typespec_client_core::http::HttpClient on top of a fetch::HttpClient transport.
  • anyspawn_azureRuntime implements typespec_client_core::async_runtime::AsyncRuntime on top of an anyspawn::Spawner (spawning) and a tick::Clock (sleeping). With the optional azure-identity feature it also implements azure_identity::Executor, running developer-credential commands on the spawner.

fetch is depended on without enabling any of its features.

Details

  • AzureHttpClient converts a typespec Request (method, URI, headers, and bytes or seekable-stream body) into a fetch request, executes it, and maps the fetch response back into an AsyncRawResponse with a streamed body. Empty bodies and byte bodies avoid copies; non-UTF-8 response headers are skipped (matching the built-in reqwest transport). Build errors map to DataConversion; transport errors to Io.
  • Conversions: From<fetch::HttpClient> and From<AzureHttpClient> into Arc<dyn HttpClient>; From<Runtime> into Arc<dyn AsyncRuntime>.
  • Runtime spawns tasks wrapped in futures::future::Abortable so cancellation wakes pending waiters; sleep uses tick::Clock::delay.
  • Examples: azure_transport (transport round-trip) and blob_list (transport + anyspawn_azure executor + DeveloperToolsCredential, listing blobs on tokio).
  • Integration tests + doctests; client.rs and runtime.rs are each at 100% line/region coverage.

This mirrors the in-tree reqwest adapter shipped by typespec_client_core.

Add a new fetch_azure crate providing FetchHttpClient, an adapter that
implements typespec_client_core::http::HttpClient on top of a
fetch::HttpClient. This lets the Azure SDK for Rust use fetch as its HTTP
transport.

The adapter converts a typespec Request into a fetch request (method, uri,
headers, and bytes or seekable-stream body), executes it through the fetch
client, and maps the fetch response back into an AsyncRawResponse with a
streamed body. A new_http_client helper returns an Arc<dyn HttpClient> for
convenience.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@codecov

codecov Bot commented Jun 12, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.0%. Comparing base (963e015) to head (3438550).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##            main     #494    +/-   ##
=======================================
  Coverage   99.9%   100.0%            
=======================================
  Files        336      338     +2     
  Lines      24829    24932   +103     
=======================================
+ Hits       24818    24932   +114     
+ Misses        11        0    -11     

☔ 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.

@github-actions

github-actions Bot commented Jun 12, 2026

Copy link
Copy Markdown

⚠️ Breaking Changes Detected

error: failed to retrieve local crate data from git revision

Caused by:
    0: failed to retrieve manifest file from git revision source
    1: possibly due to errors: [
         failed to parse /home/runner/work/oxidizer/oxidizer/target/semver-checks/git-origin_main/1f0b5970b95b02ee08d19ac3d38733fabb2e06f6/Cargo.toml: no `package` table,
         failed when reading /home/runner/work/oxidizer/oxidizer/target/semver-checks/git-origin_main/1f0b5970b95b02ee08d19ac3d38733fabb2e06f6/scripts/crate-template/Cargo.toml: TOML parse error at line 9, column 26
         |
       9 | keywords = ["oxidizer", {{CRATE_KEYWORDS}}]
         |                          ^
       missing key for inline table element, expected key
       : TOML parse error at line 9, column 26
         |
       9 | keywords = ["oxidizer", {{CRATE_KEYWORDS}}]
         |                          ^
       missing key for inline table element, expected key
       ,
       ]
    2: package `anyspawn_azure` not found in /home/runner/work/oxidizer/oxidizer/target/semver-checks/git-origin_main/1f0b5970b95b02ee08d19ac3d38733fabb2e06f6

Stack backtrace:
   0: anyhow::error::<impl anyhow::Error>::msg
   1: cargo_semver_checks::rustdoc_gen::RustdocFromProjectRoot::get_crate_source
   2: cargo_semver_checks::rustdoc_gen::StatefulRustdocGenerator<cargo_semver_checks::rustdoc_gen::CoupledState>::prepare_generator
   3: cargo_semver_checks::Check::check_release::{{closure}}
   4: cargo_semver_checks::Check::check_release
   5: cargo_semver_checks::exit_on_error
   6: cargo_semver_checks::main
   7: std::sys::backtrace::__rust_begin_short_backtrace
   8: main

If the breaking changes are intentional then everything is fine - this message is merely informative.

Remember to apply a version number bump with the correct severity when publishing a version with breaking changes (1.x.x -> 2.x.x or 0.1.x -> 0.2.x).

martintmk and others added 7 commits June 12, 2026 10:56
Avoid the possessive form of the SDK acronym, which Hunspell flags, in the
to_headers doc comment.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Incorporate the best ideas from the internal azure_core adapter to reduce
allocations and improve error classification:

- split request building (mapped to DataConversion errors) from execution
  (mapped to Io errors) via layered::Service, instead of the combined
  builder .fetch() path
- add an empty-body fast path that reuses fetch's shared empty body instead
  of allocating
- stream the response body via HttpBody::into_stream, dropping the
  http-body-util dependency
- forward seekable request-stream read errors with a descriptive message

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add examples/azure_transport.rs showing how to adapt a Tokio-based fetch
client into an Arc<dyn HttpClient> Azure SDK transport and issue a request
through the typespec HttpClient trait. Enable the tokio and rustls features
on the fetch dev-dependency so the example can build a real client.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add integration tests for the previously-uncovered branches so the crate
reaches full line coverage:

- request build failure maps to a DataConversion error
- non-UTF8 response header values are skipped
- a failing seekable request-body stream surfaces through the body error map
- a failing response body surfaces through the response error map

Adds async-trait and futures dev-dependencies for the erroring SeekableStream
helper.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Bundle a runtime abstraction alongside the transport: SpawnerRuntime
implements azure_core::async_runtime::AsyncRuntime on top of an
anyspawn::Spawner (spawn via the spawner, sleep on its blocking pool, and
yield), with a new_async_runtime helper returning Arc<dyn AsyncRuntime>.

Switch the crate from typespec_client_core to azure_core (which re-exports
the same typespec http and async_runtime traits) so both abstractions come
from one Azure SDK crate. Add anyspawn and azure_core dependencies and drop
the direct typespec_client_core dependency.

Add integration tests for spawn, abort, sleep, yield, the dyn-runtime
helper, and the From/inner round trip; lib.rs remains at 100% coverage.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
SpawnerRuntime now holds a tick::Clock alongside the spawner and implements
AsyncRuntime::sleep via Clock::delay, instead of blocking a spawner thread
with std::thread::sleep. Constructors and new_async_runtime take the clock;
spawner() and clock() accessors replace inner()/into_inner(), and From now
accepts a (Spawner, Clock) tuple.

lib.rs remains at 100% coverage.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The adapter implements azure_core::http::HttpClient, so AzureHttpClient
reads more naturally than FetchHttpClient. Pure rename; no behavior change.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment thread crates/fetch_azure/src/lib.rs Outdated
Comment thread crates/fetch_azure/src/lib.rs Outdated
Comment thread crates/fetch_azure/src/lib.rs Outdated
martintmk and others added 2 commits June 12, 2026 12:07
Drop the new_http_client free function in favor of From impls:
From<fetch::HttpClient> for AzureHttpClient (unchanged) and a new
From<AzureHttpClient> for Arc<dyn HttpClient>, so callers write
AzureHttpClient::from(client).into(). A direct
From<fetch::HttpClient> for Arc<dyn HttpClient> is not possible under the
orphan rules, so the Arc conversion goes through the local type.

lib.rs remains at 100% coverage.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Address PR review feedback:
- move AzureHttpClient into a client module
- move the runtime into a runtime module and rename SpawnerRuntime to Runtime
- fix cancellation: spawn tasks wrapped in futures::future::Abortable and
  abort via AbortHandle so aborting wakes pending waiters, instead of the
  flag-based approach that left parked awaiters hanging

lib.rs is now a thin module root; client.rs and runtime.rs are each at 100%
coverage.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment thread crates/fetch_azure/src/client.rs Outdated
Comment thread crates/fetch_azure/src/client.rs Outdated
Comment thread crates/fetch_azure/src/runtime.rs Outdated
Comment thread crates/fetch_azure/tests/client.rs
martintmk and others added 6 commits June 12, 2026 12:45
Address PR review feedback:
- drop AzureHttpClient::inner and into_inner (not needed publicly)
- drop the new_async_runtime free function; add From<Runtime> for
  Arc<dyn AsyncRuntime> to mirror the client's From-based Arc conversion
- split integration tests into tests/client.rs and tests/runtime.rs

client.rs and runtime.rs remain at 100% coverage.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add an optional �zure-identity feature under which Runtime implements
�zure_identity::Executor, running developer-credential commands on the
spawner's blocking pool. Add a tokio-based blob-listing example that wires
the transport, executor, and DeveloperToolsCredential together.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The CI examples runner executes every example and requires exit 0. The
blob_list example needs a live Storage account and developer sign-in, so
it now skips gracefully (printing a message and returning Ok) when
\AZURE_STORAGE_SERVICE_ENDPOINT\ is unset, while still running the full
flow when configured.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Mark to_fetch_body with mutants::skip: the empty-body fast path is
observationally equivalent to the general bytes path (both yield a
zero-length body), so the is_empty() guard is an equivalent mutant.

Bound the abort cancellation test with a timeout so a no-op abort fails
fast (caught) instead of hanging until the mutation-test timeout.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Extract the AsyncRuntime adapter (Runtime) and the azure_identity::Executor
impl out of fetch_azure into a dedicated anyspawn_azure crate. fetch_azure
now provides only the AzureHttpClient transport; anyspawn_azure owns the
spawner/clock-backed runtime and the optional azure-identity executor.

Also drop the From<(Spawner, Clock)> for Runtime conversion (callers use
Runtime::new) and keep the blob_list example in fetch_azure, now dev-depending
on anyspawn_azure for the executor. Register both crates in the root README
and CHANGELOG.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
fetch_azure no longer depends on anyspawn after the runtime extraction, so
the crate-doc reference to it must be a plain code span rather than an
intra-doc link (which fails the docs build under -D warnings). Regenerate
the README accordingly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment thread crates/fetch_azure/Cargo.toml Outdated
Both crate libraries used azure_core only to reach the HttpClient and
AsyncRuntime traits, which azure_core re-exports from typespec_client_core
(the allowed_external_types lists already targeted typespec_client_core).
Depend on typespec_client_core directly in both libraries (fetch_azure
enables its \http\ feature). azure_core is retained only as a fetch_azure
dev-dependency for the blob_list example, which uses Azure-specific
ClientOptions/Transport/credentials.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment thread crates/fetch_azure/src/client.rs Outdated
Comment thread crates/anyspawn_azure/src/runtime.rs
martintmk and others added 2 commits June 12, 2026 16:19
Per review, rename the transport adapter to \etch_azure::HttpClient\ so it
reads naturally alongside \�nyspawn_azure::Runtime\. Since the struct now
shares its name with the \	ypespec_client_core::http::HttpClient\ trait it
implements, the trait is imported under the \HttpClientTrait\ alias where it
must be named (impl site, \Arc<dyn ...>\). Updates the lib/struct docs,
examples, and tests accordingly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Mirror the existing \From<Runtime> for Arc<dyn AsyncRuntime>\ with a
feature-gated \From<Runtime> for Arc<dyn azure_identity::Executor>\ so a
Runtime can be handed to credentials as a boxed executor, with a test
covering the conversion.

Also enable the \http\ feature on anyspawn_azure's typespec_client_core
dependency: typespec_client_core's always-compiled \stream\ module imports
the http-gated \crate::http\, so the crate does not build without it.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment thread crates/fetch_azure/src/lib.rs
Add \#![cfg_attr(docsrs, feature(doc_cfg))]\ to fetch's crate root so
feature-gated items render with their feature requirements on docs.rs,
matching fetch_azure and anyspawn_azure. The attribute is gated on the
\docsrs\ cfg, so it is inert on stable builds.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
martintmk and others added 2 commits June 12, 2026 17:23
Miri cannot run tokio's runtime or spawn OS processes, so the runtime and
transport integration tests fail under \cargo miri test\. Gate both test
files with \#![cfg(not(miri))]\ (matching anyspawn's tokio test files);
neither crate has unsafe code, so Miri loses no UB coverage. Also fix a
stale doc reference to fetch_azure::Runtime in the moved runtime tests.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Under Miri the cfg(not(miri)) guard strips each test crate's contents,
including its module doc, leaving an empty crate that trips the denied
missing_docs lint. Add an allow(missing_docs) attribute before the cfg
guard (matching anyspawn's tokio test files) so the empty Miri build
compiles cleanly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant