<p align="center">
<b>optional provider-neutral reqwest boundary for cloud-sdk.</b><br>
Provider crates, explicit API domains, security-first release gates, and transport-free core types.
</p>
<div align="center">
<a href="https://crates.io/crates/cloud-sdk">cloud-sdk crate</a>
|
<a href="https://docs.rs/cloud-sdk-reqwest">Docs.rs</a>
|
<a href="https://github.com/valkyoth/cloud-sdk/blob/main/docs/RELEASE_PLAN.md">Release Plan</a>
|
<a href="https://github.com/valkyoth/cloud-sdk/blob/main/docs/threat-model.md">Threat Model</a>
|
<a href="https://github.com/valkyoth/cloud-sdk/blob/main/SECURITY.md">Security</a>
</div>
<br>
<p align="center">
<a href="https://github.com/valkyoth/cloud-sdk">
<img src="https://raw.githubusercontent.com/valkyoth/cloud-sdk/main/.github/images/cloud-sdk.webp" alt="cloud-sdk Rust crate overview">
</a>
</p>
# cloud-sdk-reqwest
Optional provider-neutral transport adapter for the main
[`cloud-sdk`](https://github.com/valkyoth/cloud-sdk) workspace and
[`cloud-sdk`](https://crates.io/crates/cloud-sdk) crate.
The crate remains no_std and transport-free by default. Its non-default
`blocking-rustls`, `blocking-rustls-webpki-roots`, and `async-rustls` features
provide reviewed HTTPS implementations for every
provider without adding transport dependencies to provider crates.
Transport features are supported on Linux, Windows, macOS, and FreeBSD.
Android, iOS, WASM, and bare-metal builds fail with an explicit diagnostic;
use a target-native implementation of the `cloud-sdk` transport traits there.
The default and `std`-only graphs remain portable and transport-free.
## Install
```toml
[dependencies]
cloud-sdk = "=0.100.0"
cloud-sdk-reqwest = { version = "=0.37.0", features = ["blocking-rustls"] }
```
The examples use Hetzner as a concrete endpoint, but the adapter contains no
provider-specific routing, authentication, or response logic.
Response metadata changes from the previous release are listed in the
[v0.29 migration guide](https://github.com/valkyoth/cloud-sdk/blob/main/docs/MIGRATION.md#v0290).
The adapters transmit every method admitted by `cloud-sdk 0.33`, including
bounded provider extensions. Method validation and migration details are in
the
[v0.33 migration guide](https://github.com/valkyoth/cloud-sdk/blob/main/docs/MIGRATION.md#v0330).
Endpoint trust construction changed in v0.34. Prefer
`HttpsEndpoint::new_with_policy` with a provider-owned fixed, official-set, or
regional policy. `new_custom` now requires
`CustomEndpointAcknowledgement::trusted_operator_configuration()` so a custom
credential destination cannot be selected accidentally. See the
[v0.34 migration guide](https://github.com/valkyoth/cloud-sdk/blob/main/docs/MIGRATION.md#v0340).
Raw endpoint input is bounded by `MAX_CONFIGURED_ENDPOINT_BYTES` before URL
parsing. Base paths must already be exact printable ASCII and cannot contain
backslashes, percent escapes, controls, whitespace, non-ASCII bytes, repeated
slashes, or dot segments.
Request paths and queries are validated once by `cloud-sdk`; this adapter
preserves their exact bytes and does not apply a second encoding dialect. See
the [v0.35 migration guide](https://github.com/valkyoth/cloud-sdk/blob/main/docs/MIGRATION.md#v0350).
Request headers are now complete bounded core values rather than adapter
defaults. Response headers are retained in bounded owned metadata. See the
[v0.36 migration guide](https://github.com/valkyoth/cloud-sdk/blob/main/docs/MIGRATION.md#v0360).
Response provenance migration is listed in the
[v0.37 migration guide](https://github.com/valkyoth/cloud-sdk/blob/main/docs/MIGRATION.md#v0370).
Mandatory response cleanup migration is listed in the
[v0.38 migration guide](https://github.com/valkyoth/cloud-sdk/blob/main/docs/MIGRATION.md#v0380).
Raw bounded execution and delivery-phase migration are listed in the
[v0.40 migration guide](https://github.com/valkyoth/cloud-sdk/blob/main/docs/MIGRATION.md#v0400).
Mandatory bearer scope, rotation, and refresh migration are listed in the
[v0.41 migration guide](https://github.com/valkyoth/cloud-sdk/blob/main/docs/MIGRATION.md#v0410).
Basic credential and client additions are listed in the
[v0.42 migration guide](https://github.com/valkyoth/cloud-sdk/blob/main/docs/MIGRATION.md#v0420).
Authenticated raw-wire execution and delivery-phase changes are listed in the
[v0.43 migration guide](https://github.com/valkyoth/cloud-sdk/blob/main/docs/MIGRATION.md#v0430).
The v0.46 package change is dependency-only. Retry ownership remains in the
provider-neutral caller policy described by the
[retry and idempotency guide](https://github.com/valkyoth/cloud-sdk/blob/main/docs/RETRY_AND_IDEMPOTENCY.md);
each adapter call still performs exactly one attempt.
The v0.47 package change is also dependency-only. Reqwest's `Send` futures
automatically satisfy the local async traits, but the adapter still requires
Tokio and does not become a browser-WASM or embedded transport. See the
[local async guide](https://github.com/valkyoth/cloud-sdk/blob/main/docs/LOCAL_ASYNC.md).
## Link-Local Metadata Executor
`LinkLocalHttpEndpoint` is the only admitted HTTP exception. It requires a
provider-owned fixed policy, an IPv4 link-local literal, port 80, and the exact
base path. `RawLinkLocalBlockingClientBuilder` and
`RawLinkLocalAsyncClientBuilder` are separate from the HTTPS-only raw builders,
have no credential parameter, use the direct Hyper connector, follow no
redirects, consult no proxy environment, and perform one attempt:
```rust,no_run
# #[cfg(feature = "blocking-rustls")]
# fn main() -> Result<(), Box<dyn std::error::Error>> {
use std::time::Duration;
use cloud_sdk::transport::{EndpointIdentity, EndpointPolicy, EndpointScheme};
use cloud_sdk_reqwest::blocking::{
LinkLocalHttpEndpoint, RawLinkLocalBlockingClientBuilder, RequestTimeouts,
UserAgent,
};
let identity = EndpointIdentity::new(
EndpointScheme::Http,
"169.254.169.254",
80,
"/",
)?;
let endpoint = LinkLocalHttpEndpoint::new_with_policy(
"http://169.254.169.254",
EndpointPolicy::fixed(identity),
)?;
let user_agent = UserAgent::new("metadata-reader/1")?;
let timeouts = RequestTimeouts::new(
Duration::from_secs(2),
Duration::from_secs(1),
)?;
let _client = RawLinkLocalBlockingClientBuilder::new(
endpoint,
user_agent,
timeouts,
).build()?;
# Ok(())
# }
# #[cfg(not(feature = "blocking-rustls"))]
# fn main() {}
```
The provider crate supplies the fixed Hetzner identity and typed request and
decoder. Do not generalize this constructor into arbitrary HTTP or credentialed
metadata access.
## Raw Blocking Executor
Use the raw executor below provider authentication and typed client policy. It
sends no bearer token or JSON `Accept`, performs no retry, and retains only
response headers admitted by `RawResponsePolicy`:
```rust,no_run
# #[cfg(feature = "blocking-rustls")]
# fn main() {
use std::time::Duration;
use cloud_sdk::Method;
use cloud_sdk::transport::{
BlockingRawHttpExecutor, EndpointIdentity, EndpointPolicy, EndpointScheme,
MediaType, RawResponsePolicy, RequestTarget, ResponseBuffer,
ResponseMediaPolicy, TransportRequest,
};
use cloud_sdk_reqwest::blocking::{
HttpsEndpoint, RawBlockingClientBuilder, RequestTimeouts, UserAgent,
};
let Ok(identity) =
EndpointIdentity::new(EndpointScheme::Https, "api.example.com", 443, "/v1")
else { return };
let policy = EndpointPolicy::fixed(identity);
let Ok(endpoint) =
HttpsEndpoint::new_with_policy("https://api.example.com/v1", policy)
else { return };
let Ok(user_agent) = UserAgent::new("my-service/1.0") else { return };
let Ok(timeouts) = RequestTimeouts::new(
Duration::from_secs(30),
Duration::from_secs(10),
) else { return };
let Ok(client) =
RawBlockingClientBuilder::new(endpoint, user_agent, timeouts).build()
else { return };
let Ok(policy) = RawResponsePolicy::new(
65_536,
16_384,
ResponseMediaPolicy::Required(&[MediaType::JSON]),
ResponseMediaPolicy::Optional(&[MediaType::JSON]),
&[],
2,
) else { return };
let Ok(target) = RequestTarget::new("/resources") else { return };
let mut body = [0_u8; 65_536];
let body_capacity = body.len();
let mut headers = [0_u8; cloud_sdk::transport::MAX_RESPONSE_HEADER_BYTES];
let mut response = ResponseBuffer::new(&mut body, body_capacity, &mut headers);
if client.execute(
TransportRequest::new(Method::Get, target),
policy,
response.writer(),
).is_err() {
return;
}
# }
# #[cfg(not(feature = "blocking-rustls"))]
# fn main() {}
```
`RawAsyncClientBuilder` implements the same policy through
`AsyncRawHttpExecutor`. Blocking, async, and deterministic-root raw clients
share one bounded HTTP/1 engine. See the complete
[wire and allocation contract](https://github.com/valkyoth/cloud-sdk/blob/main/docs/RAW_HTTP_EXECUTOR.md).
## Blocking Example
```rust,no_run
# #[cfg(feature = "blocking-rustls")]
# fn main() {
use std::time::Duration;
use cloud_sdk::{Method, ProviderId, ServiceId};
use cloud_sdk::authentication::{AuthenticationScopePolicy, ScopeRequirement};
use cloud_sdk::operation::{
ContentTypePolicy, CostIntent, OperationImpact, OperationMetadata,
PreparedRequest, ProviderService, RequestBodySensitivity, RequestIdPolicy,
RequestSemantics, ResponseBodyPolicy, ResponsePolicy, RetryEligibility,
};
use cloud_sdk::transport::{
EndpointPolicy, HeaderName, MediaType, RawResponsePolicy, RequestTarget,
ResponseMediaPolicy, StatusCode, TransportRequest,
};
use cloud_sdk_reqwest::blocking::{
BearerCredential, BearerCredentialScope, BearerToken,
BlockingClientBuilder, CustomEndpointAcknowledgement, HttpsEndpoint,
RequestTimeouts, UserAgent,
};
// Custom endpoints are credential destinations. Keep this value in trusted
// operator configuration; never accept it from tenant-controlled input.
let acknowledgement =
CustomEndpointAcknowledgement::trusted_operator_configuration();
let Ok(endpoint) =
HttpsEndpoint::new_custom("https://api.hetzner.cloud/v1", acknowledgement)
else { return };
let Ok(provider) = ProviderId::new("hetzner") else { return };
let Ok(service) = ServiceId::new("cloud") else { return };
let policy_endpoint = endpoint.clone();
let Ok(endpoint_identity) = policy_endpoint.identity() else { return };
let Ok(token) = BearerToken::new("replace-with-scoped-token") else { return };
let credential_scope =
BearerCredentialScope::new(provider, service, endpoint.clone());
let credential = BearerCredential::new(token, credential_scope);
let authentication_policy = AuthenticationScopePolicy::new(
ScopeRequirement::Required(provider),
ScopeRequirement::Required(service),
ScopeRequirement::Required(endpoint_identity),
ScopeRequirement::Forbidden,
ScopeRequirement::Forbidden,
ScopeRequirement::Forbidden,
);
let Ok(user_agent) = UserAgent::new("my-service/1.0") else { return };
let Ok(timeouts) = RequestTimeouts::new(
Duration::from_secs(30),
Duration::from_secs(10),
) else { return };
let Ok(client) =
BlockingClientBuilder::new(endpoint, credential, user_agent, timeouts).build()
else { return };
let Ok(target) = RequestTarget::new("/servers?page=1") else { return };
let Ok(content_type) = HeaderName::new("content-type") else { return };
let Ok(raw_response_policy) = RawResponsePolicy::new(
65_536,
65_536,
ResponseMediaPolicy::Required(&[MediaType::JSON]),
ResponseMediaPolicy::Required(&[MediaType::JSON]),
&[content_type],
8,
) else { return };
let Ok(response_policy) = ResponsePolicy::new(
&[StatusCode::OK],
ContentTypePolicy::Required(&[MediaType::JSON]),
ResponseBodyPolicy::Required,
65_536,
) else { return };
let Ok(metadata) = OperationMetadata::new(
OperationImpact::ReadOnly,
RequestSemantics::Safe,
RetryEligibility::Never,
CostIntent::NoKnownCost,
RequestIdPolicy::Discard,
) else { return };
let service_policy = ProviderService::new(
provider,
service,
EndpointPolicy::fixed(endpoint_identity),
);
let Ok(request) = PreparedRequest::new(
TransportRequest::new(Method::Get, target),
service_policy,
metadata,
response_policy,
authentication_policy,
raw_response_policy,
RequestBodySensitivity::Public,
) else { return };
let mut response_body = [0_u8; 65_536];
let mut response_headers = [0_u8; cloud_sdk::transport::MAX_RESPONSE_HEADER_BYTES];
let Ok(response) = request.execute_blocking(
&client,
&mut response_body,
&mut response_headers,
) else { return };
assert!(response.status().is_success());
# }
# #[cfg(not(feature = "blocking-rustls"))]
# fn main() {}
```
### Basic Authentication
Basic credentials use separate types and builders, but every send uses the
same mandatory checked `PreparedRequest` path as the bearer example:
Each Basic credential receives a 256-bit opaque lineage binding from the
admitted operating-system CSPRNG. Cloned clients retain the same binding;
constructing replacement credentials creates a new binding. Provider
operations with authenticated-preflight authority can therefore reject a
different credential lifecycle before mutation dispatch without exposing
credential bytes.
```rust,no_run
# #[cfg(feature = "blocking-rustls")]
# fn main() {
use std::time::Duration;
use cloud_sdk::{ProviderId, ServiceId};
use cloud_sdk_reqwest::blocking::{
BasicCredential, BasicCredentialScope, BasicPassword, BasicUsername,
BlockingBasicClientBuilder, CustomEndpointAcknowledgement, HttpsEndpoint,
RequestTimeouts, UserAgent,
};
// Custom endpoints are credential destinations. Keep this value in trusted
// operator configuration; never accept it from tenant-controlled input.
let acknowledgement =
CustomEndpointAcknowledgement::trusted_operator_configuration();
let Ok(endpoint) = HttpsEndpoint::new_custom(
"https://robot-ws.your-server.de",
acknowledgement,
) else { return };
let Ok(provider) = ProviderId::new("hetzner") else { return };
let Ok(service) = ServiceId::new("robot") else { return };
let Ok(username) = BasicUsername::new("webservice-user") else { return };
let Ok(password) = BasicPassword::new("replace-with-secret") else { return };
let scope = BasicCredentialScope::new(provider, service, endpoint.clone());
let Ok(credential) = BasicCredential::new(username, password, scope) else {
return;
};
let Ok(user_agent) = UserAgent::new("my-service/1.0") else { return };
let Ok(timeouts) = RequestTimeouts::new(
Duration::from_secs(30),
Duration::from_secs(10),
) else { return };
let Ok(_client) =
BlockingBasicClientBuilder::new(endpoint, credential, user_agent, timeouts)
.build()
else { return };
# }
# #[cfg(not(feature = "blocking-rustls"))]
# fn main() {}
```
Prefer mutable-byte or guarded-buffer constructors so caller-owned credential
sources can be cleared. Robot authentication rejection can block the source IP
after repeated failed logins; this example constructs a client but performs no
request. Robot operation clients and lockout-aware credential attempts remain
later pre-1.0 milestones.
Responses retain complete bounded header metadata plus one validated
`Content-Type` value for prepared response policy. Duplicate names, controls,
and per-value, count, or aggregate overflow fail closed before body bytes are
returned. Incoming sensitivity already marked by reqwest is preserved. Unknown
fields default to sensitive; only Content-Type, Content-Length, Date, and the
three typed rate-limit fields are classified as reviewed public metadata.
Core volatile-clears the complete caller buffer before endpoint checks and
before lending the smaller operation-admitted response window. Both adapters
also implement `ResponseStorageSanitizer` through `cloud-sdk-sanitization` as
an optional additive hook. Direct transport sends retain the mandatory cleanup
owner in `ResponseBuffer` while lending only its sealed writer to `send`.
## Deterministic Root Snapshot
The standard blocking feature follows the host trust store. Select the
separate deterministic feature to use only the reviewed Mozilla root snapshot
compiled into `webpki-roots`:
```toml
[dependencies]
cloud-sdk = "=0.100.0"
cloud-sdk-reqwest = { version = "=0.37.0", features = ["blocking-rustls-webpki-roots"] }
```
The blocking API is identical to the example above. The custom rustls client
configuration receives only the compiled snapshot, even though reqwest still
compiles its platform-verifier dependency. Host and enterprise roots are not
consulted by this client. Root changes require a reviewed dependency update.
This mode does not add CRL/OCSP revocation checking, private roots, pinning, or
FIPS status.
## FIPS Deferment
The earlier experimental AWS-LC FIPS mode is retired and is not part of the
cloud-sdk 1.0 scope. This crate exposes no FIPS transport or compliance claim.
A future release may integrate Brynja after its exact cryptographic module,
operating environment, API, and validation evidence are stable and reviewed.
See the
[`FIPS_DEFERMENT.md`](https://github.com/valkyoth/cloud-sdk/blob/main/docs/FIPS_DEFERMENT.md)
policy.
## Async Example
The async adapter uses reqwest's Tokio-based execution internally but does not
create or own a runtime. Call it from an active Tokio executor:
```rust,no_run
# #[cfg(feature = "async-rustls")]
# async fn example() {
use std::time::Duration;
use cloud_sdk::{Method, ProviderId, ServiceId};
use cloud_sdk::authentication::{AuthenticationScopePolicy, ScopeRequirement};
use cloud_sdk::operation::{
ContentTypePolicy, CostIntent, OperationImpact, OperationMetadata,
PreparedRequest, ProviderService, RequestBodySensitivity, RequestIdPolicy,
RequestSemantics, ResponseBodyPolicy, ResponsePolicy, RetryEligibility,
};
use cloud_sdk::transport::{
EndpointPolicy, HeaderName, MediaType, RawResponsePolicy, RequestTarget,
ResponseMediaPolicy, StatusCode, TransportRequest,
};
use cloud_sdk_reqwest::asynchronous::{
AsyncClientBuilder, BearerCredential, BearerCredentialScope,
BearerToken, CustomEndpointAcknowledgement, HttpsEndpoint,
RequestTimeouts, UserAgent,
};
// Custom endpoints are credential destinations. Keep this value in trusted
// operator configuration; never accept it from tenant-controlled input.
let acknowledgement =
CustomEndpointAcknowledgement::trusted_operator_configuration();
let Ok(endpoint) =
HttpsEndpoint::new_custom("https://api.hetzner.cloud/v1", acknowledgement)
else { return };
let Ok(provider) = ProviderId::new("hetzner") else { return };
let Ok(service) = ServiceId::new("cloud") else { return };
let policy_endpoint = endpoint.clone();
let Ok(endpoint_identity) = policy_endpoint.identity() else { return };
let Ok(token) = BearerToken::new("replace-with-scoped-token") else { return };
let credential_scope =
BearerCredentialScope::new(provider, service, endpoint.clone());
let credential = BearerCredential::new(token, credential_scope);
let authentication_policy = AuthenticationScopePolicy::new(
ScopeRequirement::Required(provider),
ScopeRequirement::Required(service),
ScopeRequirement::Required(endpoint_identity),
ScopeRequirement::Forbidden,
ScopeRequirement::Forbidden,
ScopeRequirement::Forbidden,
);
let Ok(user_agent) = UserAgent::new("my-service/1.0") else { return };
let Ok(timeouts) = RequestTimeouts::new(
Duration::from_secs(30),
Duration::from_secs(10),
) else { return };
let Ok(client) =
AsyncClientBuilder::new(endpoint, credential, user_agent, timeouts).build()
else { return };
let Ok(target) = RequestTarget::new("/servers?page=1") else { return };
let Ok(content_type) = HeaderName::new("content-type") else { return };
let Ok(raw_response_policy) = RawResponsePolicy::new(
65_536,
65_536,
ResponseMediaPolicy::Required(&[MediaType::JSON]),
ResponseMediaPolicy::Required(&[MediaType::JSON]),
&[content_type],
8,
) else { return };
let Ok(response_policy) = ResponsePolicy::new(
&[StatusCode::OK],
ContentTypePolicy::Required(&[MediaType::JSON]),
ResponseBodyPolicy::Required,
65_536,
) else { return };
let Ok(metadata) = OperationMetadata::new(
OperationImpact::ReadOnly,
RequestSemantics::Safe,
RetryEligibility::Never,
CostIntent::NoKnownCost,
RequestIdPolicy::Discard,
) else { return };
let service_policy = ProviderService::new(
provider,
service,
EndpointPolicy::fixed(endpoint_identity),
);
let Ok(request) = PreparedRequest::new(
TransportRequest::new(Method::Get, target),
service_policy,
metadata,
response_policy,
authentication_policy,
raw_response_policy,
RequestBodySensitivity::Public,
) else { return };
let mut response_body = [0_u8; 65_536];
let mut response_headers = [0_u8; cloud_sdk::transport::MAX_RESPONSE_HEADER_BYTES];
let Ok(response) = request.execute_async(
&client,
&mut response_body,
&mut response_headers,
)
.await
else { return };
assert!(response.status().is_success());
# }
# fn main() {}
```
For a non-empty request body, set an explicit validated content type:
```rust
use cloud_sdk::transport::{
ContentType, MediaType, RequestHeader, RequestHeaders, TransportRequest,
};
# use cloud_sdk::{Method, transport::RequestTarget};
# let Ok(target) = RequestTarget::new("/servers") else { return };
let entries = [
RequestHeader::accept(MediaType::JSON),
RequestHeader::content_type(ContentType::JSON),
];
let Ok(headers) = RequestHeaders::new(&entries) else { return };
let request = TransportRequest::new(Method::Post, target)
.with_headers(headers)
.with_body(br#"{"name":"example"}"#);
assert!(request.headers().get("content-type").is_some());
```
## Shared Clients And Credential Rotation
Blocking and async clients are `Clone + Send + Sync`. Clones share one
credential state and one immutable endpoint identity, while every request body
and response buffer remains caller-owned. The SDK does not create tasks,
queues, semaphores, retries, sleeps, or an executor; callers must bound their
own blocking threads or async task sets.
Both authenticated transport traits send through `&self` and require a complete
provider or operation-owned authentication policy plus raw response policy.
Scope validation completes before header construction. A successful request
then takes a short-lived token snapshot, releases the credential lock before
network work or `.await`, and executes through the shared bounded raw Hyper
engine. Failures retain their conservative delivery phase.
Rotation changes the token for newly started requests atomically; an in-flight
request keeps its previous snapshot, and retired adapter-owned token and header
storage is sanitized after its last owner is dropped.
```rust,no_run
# #[cfg(feature = "blocking-rustls")]
# fn example(client: &cloud_sdk_reqwest::blocking::BlockingClient) {
use cloud_sdk::transport::{BoundTransport, EndpointScheme};
let official = client.endpoint_identity().is_ok_and(|identity| {
identity.scheme() == EndpointScheme::Https
&& identity.host() == "api.hetzner.cloud"
&& identity.effective_port() == 443
&& identity.base_path() == "/v1"
});
assert!(official);
let mut replacement = *b"replace-with-scoped-token";
let result = client.rotate_bearer_token_from_mut_bytes(&mut replacement);
assert!(result.is_ok());
let Ok(handoff) = snapshot.refresh_handoff() else { return };
let mut refreshed = *b"new-refreshed-token";
let refreshed_generation =
client.refresh_bearer_token_from_mut_bytes(handoff, &mut refreshed);
assert!(refreshed_generation.is_ok());