The blocking client
The default IamClient is async. When you are not in an async context — a CLI tool, a
build script, a synchronous worker — enable the blocking feature to get a synchronous twin with
identical fail-closed semantics.
Enable the feature
[dependencies]
laravel-iam = { version = "1", features = ["blocking"] }
This turns on reqwest/blocking and exposes laravel_iam::blocking::IamClient.
Build and use
The builder is shared; you just finish it with build_blocking() instead of build():
use laravel_iam::{blocking::IamClient, DecisionQuery, Subject, ResultExt};
use serde_json::json;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let iam = IamClient::builder()
.base_url("https://iam.example.com/api/iam/v1")
.token(std::env::var("IAM_SERVICE_TOKEN")?)
.build_blocking()?; // <- not build()
let decision = iam.check(DecisionQuery {
subject: Subject::user("usr_123"),
permission: "stock.adjust".into(),
resource: Some("wh_milan".into()),
context: json!({ "amount": 300 }),
..Default::default()
}); // <- no .await
if !decision.is_allowed() {
return Err("forbidden".into());
}
Ok(())
}
The API surface is the same minus .await: check(), list_resources(), verify_token(), and the
same Decision / Claims / IamError
types. ResultExt::is_allowed works identically on the blocking result.
Identical semantics by construction
The async and blocking clients are not parallel reimplementations. Everything that does not depend
on how bytes move over the network — URL construction, HTTP-status mapping, response parsing, and JWT
verification — lives in one transport-agnostic wire module. Both clients call into it, so they apply
byte-identical, fail-closed rules. The only difference is reqwest::Client vs
reqwest::blocking::Client and tokio::sync::RwLock vs std::sync::RwLock for the JWKS cache.
See The check flow for the shared pipeline, and
ADR-0002 for why both clients exist.
The one rule
Never call the blocking client from inside an async runtime thread. reqwest::blocking drives its
own internal runtime; calling it from a tokio worker thread can deadlock or panic. If you are already
in async code, use the async IamClient. If you must bridge, hand the call to
tokio::task::spawn_blocking.
When to choose blocking
- Command-line tools and one-shot scripts.
build.rs/ tooling with no runtime.- Synchronous services or libraries that have no async stack.
- Web servers and request handlers (
axum,actix,warp, …). - Anything already running under
tokio. - High-concurrency workloads where threads-per-request is wasteful.
Gotchas
build_blocking(), notbuild()— callingbuild()after enabling the feature gives you the
async client.- No
.awaiton blocking methods; adding one will not compile. - Don’t mix — pick one client per call site; do not call blocking methods from async tasks.