Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
0d87cc5
feat(resources-introspection): add support for resource metadata retr…
dertin Mar 3, 2025
041322e
misc: remove debug print
dertin Mar 3, 2025
819ee93
style: cargo fmt
dertin Mar 3, 2025
91fa813
fix(guards): replace take_guards with get_guards to prevent guard rem…
dertin Mar 3, 2025
176ea5d
ci: downgrade for msrv litemap to version 0.7.4 in justfile
dertin Mar 3, 2025
acd7c58
chore: update changelog and fix docs for CI
dertin Mar 3, 2025
c4be199
ci: downgrade for msrv zerofrom to version 0.1.5 in justfile
dertin Mar 3, 2025
a79dc9d
refactor: improve thread safety and add unit tests for introspection …
dertin Mar 4, 2025
2bb774a
fix(introspection): add conditional arbiter creation for io-uring sup…
dertin Mar 4, 2025
3b99f86
fix(introspection): add conditional arbiter creation for io-uring sup…
dertin Mar 4, 2025
7821ba9
Merge branch 'introspection' of https://github.com/dertin/actix-web i…
dertin Mar 4, 2025
0548b12
Merge branch 'introspection' of https://github.com/dertin/actix-web i…
dertin Mar 4, 2025
ee76215
Merge branch 'introspection' of https://github.com/dertin/actix-web i…
dertin Mar 4, 2025
ae08dcf
refactor(introspection): add GuardDetail enum and remove downcast_ref…
dertin Mar 5, 2025
aebab17
refactor(introspection): add GuardDetail enum and remove downcast_ref…
dertin Mar 5, 2025
a449695
Merge branch 'introspection' of https://github.com/dertin/actix-web i…
dertin Mar 5, 2025
3506512
Merge branch 'master' into introspection
dertin Mar 10, 2025
57b5937
Merge branch 'master' into introspection
dertin Mar 21, 2025
585552f
Merge branch 'master' into introspection
dertin Apr 2, 2025
c809ee8
Merge branch 'master' into introspection
dertin Apr 10, 2025
013a8ec
Merge branch 'master' into introspection
dertin Apr 22, 2025
d501102
feat(introspection): rename feature from `resources-introspection` to…
devdertin May 12, 2025
dcad1d9
Merge branch 'master' into introspection
devdertin May 12, 2025
2f64cdb
fix Cargo.lock
devdertin May 12, 2025
0a9f6c1
feat(introspection): enhance introspection feature with detailed rout…
devdertin May 19, 2025
c8a7271
optimize debug log and apply clippy/fmt suggestions
devdertin May 19, 2025
360baa3
Merge branch 'master' into introspection
dertin May 19, 2025
23fed22
feat(introspection): enhance introspection handlers for JSON and plai…
devdertin May 20, 2025
d1706dc
Merge branch 'introspection' of github.com:dertin/actix-web into intr…
devdertin May 20, 2025
7296f6f
Merge branch 'master' into introspection
dertin May 27, 2025
2b52a60
Merge branch 'master' into introspection
dertin Jun 10, 2025
7ff7768
feat(introspection): implement experimental introspection feature wit…
dertin Jun 11, 2025
dfb4aa3
Merge branch 'master' into introspection
dertin Jun 17, 2025
f3b64b0
Merge branch 'master' into introspection
dertin Jul 23, 2025
a3e428f
Merge branch 'main' into introspection
dertin Nov 18, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions actix-web/CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Unreleased

- Add `experimental-introspection` feature for retrieving configured route paths and HTTP methods.

## 4.12.0

- `actix_web::response::builder::HttpResponseBuilder::streaming()` now sets `Content-Type` to `application/octet-stream` if `Content-Type` does not exist.
Expand Down
3 changes: 3 additions & 0 deletions actix-web/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,9 @@ compat = ["compat-routing-macros-force-pub"]
# Opt-out forwards-compatibility for handler visibility inheritance fix.
compat-routing-macros-force-pub = ["actix-web-codegen?/compat-routing-macros-force-pub"]

# Enabling the retrieval of metadata for initialized resources, including path and HTTP method.
experimental-introspection = []

[dependencies]
actix-codec = "0.5"
actix-macros = { version = "0.2.3", optional = true }
Expand Down
255 changes: 255 additions & 0 deletions actix-web/examples/introspection.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
// Example showcasing the experimental introspection feature.
// Run with: `cargo run --features experimental-introspection --example introspection`

#[actix_web::main]
async fn main() -> std::io::Result<()> {
#[cfg(feature = "experimental-introspection")]
{
use actix_web::{dev::Service, guard, web, App, HttpResponse, HttpServer, Responder};
use serde::Deserialize;

// Initialize logging
env_logger::Builder::new()
.filter_level(log::LevelFilter::Debug)
.init();

// Custom guard to check if the Content-Type header is present.
struct ContentTypeGuard;

impl guard::Guard for ContentTypeGuard {
fn check(&self, req: &guard::GuardContext<'_>) -> bool {
req.head()
.headers()
.contains_key(actix_web::http::header::CONTENT_TYPE)
}
}

// Data structure for endpoints that receive JSON.
#[derive(Deserialize)]
struct UserInfo {
username: String,
age: u8,
}

// GET /introspection for JSON response
async fn introspection_handler_json(
tree: web::Data<actix_web::introspection::IntrospectionTree>,
) -> impl Responder {
let report = tree.report_as_json();
HttpResponse::Ok()
.content_type("application/json")
.body(report)
}

// GET /introspection for plain text response
async fn introspection_handler_text(
tree: web::Data<actix_web::introspection::IntrospectionTree>,
) -> impl Responder {
let report = tree.report_as_text();
HttpResponse::Ok().content_type("text/plain").body(report)
}

// GET /api/v1/item/{id} and GET /v1/item/{id}
#[actix_web::get("/item/{id}")]
async fn get_item(path: web::Path<u32>) -> impl Responder {
let id = path.into_inner();
HttpResponse::Ok().body(format!("Requested item with id: {}", id))
}

// POST /api/v1/info
#[actix_web::post("/info")]
async fn post_user_info(info: web::Json<UserInfo>) -> impl Responder {
HttpResponse::Ok().json(format!(
"User {} with age {} received",
info.username, info.age
))
}

// /api/v1/guarded
async fn guarded_handler() -> impl Responder {
HttpResponse::Ok().body("Passed the Content-Type guard!")
}

// GET /api/v2/hello
async fn hello_v2() -> impl Responder {
HttpResponse::Ok().body("Hello from API v2!")
}

// GET /admin/dashboard
async fn admin_dashboard() -> impl Responder {
HttpResponse::Ok().body("Welcome to the Admin Dashboard!")
}

// GET /admin/settings
async fn get_settings() -> impl Responder {
HttpResponse::Ok().body("Current settings: ...")
}

// POST /admin/settings
async fn update_settings() -> impl Responder {
HttpResponse::Ok().body("Settings have been updated!")
}

// GET and POST on /
async fn root_index() -> impl Responder {
HttpResponse::Ok().body("Welcome to the Root Endpoint!")
}

// Additional endpoints for /extra
fn extra_endpoints(cfg: &mut web::ServiceConfig) {
cfg.service(
web::scope("/extra")
.route(
"/ping",
web::get().to(|| async { HttpResponse::Ok().body("pong") }), // GET /extra/ping
)
.service(
web::resource("/multi")
.route(web::get().to(|| async {
HttpResponse::Ok().body("GET response from /extra/multi")
})) // GET /extra/multi
.route(web::post().to(|| async {
HttpResponse::Ok().body("POST response from /extra/multi")
})), // POST /extra/multi
)
.service(
web::scope("{entities_id:\\d+}")
.service(
web::scope("/secure")
.route(
"",
web::get().to(|| async {
HttpResponse::Ok()
.body("GET response from /extra/secure")
}),
) // GET /extra/{entities_id}/secure/
.route(
"/post",
web::post().to(|| async {
HttpResponse::Ok()
.body("POST response from /extra/secure")
}),
), // POST /extra/{entities_id}/secure/post
)
.wrap_fn(|req, srv| {
println!(
"Request to /extra/secure with id: {}",
req.match_info().get("entities_id").unwrap()
);
let fut = srv.call(req);
async move {
let res = fut.await?;
Ok(res)
}
}),
),
);
}

// Additional endpoints for /foo
fn other_endpoints(cfg: &mut web::ServiceConfig) {
cfg.service(
web::scope("/extra")
.route(
"/ping",
web::post()
.to(|| async { HttpResponse::Ok().body("post from /extra/ping") }), // POST /foo/extra/ping
)
.route(
"/ping",
web::delete()
.to(|| async { HttpResponse::Ok().body("delete from /extra/ping") }), // DELETE /foo/extra/ping
),
);
}

// Create the HTTP server with all the routes and handlers
let server = HttpServer::new(|| {
App::new()
// Get introspection report
// curl --location '127.0.0.1:8080/introspection' --header 'Accept: application/json'
// curl --location '127.0.0.1:8080/introspection' --header 'Accept: text/plain'
.service(
web::resource("/introspection")
.route(
web::get()
.guard(guard::Header("accept", "application/json"))
.to(introspection_handler_json),
)
.route(
web::get()
.guard(guard::Header("accept", "text/plain"))
.to(introspection_handler_text),
),
)
// API endpoints under /api
.service(
web::scope("/api")
// Endpoints under /api/v1
.service(
web::scope("/v1")
.service(get_item) // GET /api/v1/item/{id}
.service(post_user_info) // POST /api/v1/info
.route(
"/guarded",
web::route().guard(ContentTypeGuard).to(guarded_handler), // /api/v1/guarded
),
)
// Endpoints under /api/v2
.service(web::scope("/v2").route("/hello", web::get().to(hello_v2))), // GET /api/v2/hello
)
// Endpoints under /v1 (outside /api)
.service(web::scope("/v1").service(get_item)) // GET /v1/item/{id}
// Admin endpoints under /admin
.service(
web::scope("/admin")
.route("/dashboard", web::get().to(admin_dashboard)) // GET /admin/dashboard
.service(
web::resource("/settings")
.route(web::get().to(get_settings)) // GET /admin/settings
.route(web::post().to(update_settings)), // POST /admin/settings
),
)
// Root endpoints
.service(
web::resource("/")
.route(web::get().to(root_index)) // GET /
.route(web::post().to(root_index)), // POST /
)
// Endpoints under /bar
.service(web::scope("/bar").configure(extra_endpoints)) // /bar/extra/ping, /bar/extra/multi, etc.
// Endpoints under /foo
.service(web::scope("/foo").configure(other_endpoints)) // /foo/extra/ping with POST and DELETE
// Additional endpoints under /extra
.configure(extra_endpoints) // /extra/ping, /extra/multi, etc.
.configure(other_endpoints)
// Endpoint that rejects GET on /not_guard (allows other methods)
.route(
"/not_guard",
web::route()
.guard(guard::Not(guard::Get()))
.to(HttpResponse::MethodNotAllowed),
)
// Endpoint that requires GET with header or POST on /all_guard
.route(
"/all_guard",
web::route()
.guard(
guard::All(guard::Get())
.and(guard::Header("content-type", "plain/text"))
.and(guard::Any(guard::Post())),
)
.to(HttpResponse::MethodNotAllowed),
)
})
.workers(5)
.bind("127.0.0.1:8080")?;

server.run().await
}
#[cfg(not(feature = "experimental-introspection"))]
{
eprintln!("This example requires the 'experimental-introspection' feature to be enabled.");
std::process::exit(1);
}
}
52 changes: 52 additions & 0 deletions actix-web/examples/introspection_multi_servers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Example showcasing the experimental introspection feature with multiple App instances.
// Run with: `cargo run --features experimental-introspection --example introspection_multi_servers`

#[actix_web::main]
async fn main() -> std::io::Result<()> {
#[cfg(feature = "experimental-introspection")]
{
use actix_web::{web, App, HttpResponse, HttpServer, Responder};
use futures_util::future;

async fn introspection_handler(
tree: web::Data<actix_web::introspection::IntrospectionTree>,
) -> impl Responder {
HttpResponse::Ok()
.content_type("text/plain")
.body(tree.report_as_text())
}

async fn index() -> impl Responder {
HttpResponse::Ok().body("Hello from app")
}

let srv1 = HttpServer::new(|| {
App::new()
.service(web::resource("/a").route(web::get().to(index)))
.service(
web::resource("/introspection").route(web::get().to(introspection_handler)),
)
})
.workers(8)
.bind("127.0.0.1:8081")?
.run();

let srv2 = HttpServer::new(|| {
App::new()
.service(web::resource("/b").route(web::get().to(index)))
.service(
web::resource("/introspection").route(web::get().to(introspection_handler)),
)
})
.workers(3)
.bind("127.0.0.1:8082")?
.run();

future::try_join(srv1, srv2).await?;
}
#[cfg(not(feature = "experimental-introspection"))]
{
eprintln!("This example requires the 'experimental-introspection' feature to be enabled.");
}
Ok(())
}
12 changes: 12 additions & 0 deletions actix-web/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ pub struct App<T> {
data_factories: Vec<FnDataFactory>,
external: Vec<ResourceDef>,
extensions: Extensions,
#[cfg(feature = "experimental-introspection")]
introspector: Rc<RefCell<crate::introspection::IntrospectionCollector>>,
}

impl App<AppEntry> {
Expand All @@ -46,6 +48,10 @@ impl App<AppEntry> {
factory_ref,
external: Vec::new(),
extensions: Extensions::new(),
#[cfg(feature = "experimental-introspection")]
introspector: Rc::new(RefCell::new(
crate::introspection::IntrospectionCollector::new(),
)),
}
}
}
Expand Down Expand Up @@ -366,6 +372,8 @@ where
factory_ref: self.factory_ref,
external: self.external,
extensions: self.extensions,
#[cfg(feature = "experimental-introspection")]
introspector: self.introspector,
}
}

Expand Down Expand Up @@ -429,6 +437,8 @@ where
factory_ref: self.factory_ref,
external: self.external,
extensions: self.extensions,
#[cfg(feature = "experimental-introspection")]
introspector: self.introspector,
}
}
}
Expand All @@ -453,6 +463,8 @@ where
default: self.default,
factory_ref: self.factory_ref,
extensions: RefCell::new(Some(self.extensions)),
#[cfg(feature = "experimental-introspection")]
introspector: Rc::clone(&self.introspector),
}
}
}
Expand Down
Loading
Loading