use actix_web::{App, HttpResponse, HttpServer, Responder, web};
use html2pdf_api::integrations::actix::{SharedPool, configure_routes};
use html2pdf_api::prelude::*;
use html2pdf_api::service::{
PdfFromHtmlRequest, PdfFromUrlRequest, generate_pdf_from_html, generate_pdf_from_url,
};
use std::sync::Arc;
use std::time::Duration;
async fn custom_pdf_handler(
pool: web::Data<SharedPool>,
query: web::Query<PdfFromUrlRequest>,
) -> impl Responder {
log::info!("Custom handler called for URL: {}", query.url);
if query.url.contains("blocked-domain.com") {
return HttpResponse::Forbidden().json(serde_json::json!({
"error": "This domain is blocked",
"code": "DOMAIN_BLOCKED"
}));
}
let request_id = uuid::Uuid::new_v4().to_string();
log::info!(
"[{}] Starting PDF generation for: {}",
request_id,
query.url
);
let pool = pool.into_inner();
let request = query.into_inner();
let result = web::block(move || generate_pdf_from_url(&pool, &request)).await;
match result {
Ok(Ok(pdf_response)) => {
log::info!(
"[{}] PDF generated successfully: {} bytes",
request_id,
pdf_response.size()
);
HttpResponse::Ok()
.content_type("application/pdf")
.insert_header(("X-Request-ID", request_id))
.insert_header(("X-PDF-Size", pdf_response.size().to_string()))
.insert_header(("Content-Disposition", pdf_response.content_disposition()))
.body(pdf_response.data)
}
Ok(Err(service_error)) => {
log::error!("[{}] Service error: {}", request_id, service_error);
HttpResponse::build(
actix_web::http::StatusCode::from_u16(service_error.status_code())
.unwrap_or(actix_web::http::StatusCode::INTERNAL_SERVER_ERROR),
)
.insert_header(("X-Request-ID", request_id))
.json(serde_json::json!({
"error": service_error.to_string(),
"code": service_error.error_code(),
"retryable": service_error.is_retryable()
}))
}
Err(blocking_error) => {
log::error!("[{}] Blocking error: {}", request_id, blocking_error);
HttpResponse::InternalServerError()
.insert_header(("X-Request-ID", request_id))
.json(serde_json::json!({
"error": "Internal server error",
"code": "BLOCKING_ERROR"
}))
}
}
}
async fn manual_pdf_handler(pool: web::Data<SharedBrowserPool>) -> impl Responder {
let browser = match pool.get() {
Ok(b) => b,
Err(e) => {
log::error!("Failed to get browser: {}", e);
return HttpResponse::ServiceUnavailable().body(format!("Browser unavailable: {}", e));
}
};
log::info!("Got browser {} from pool", browser.id());
let tab = match browser.new_tab() {
Ok(t) => t,
Err(e) => {
log::error!("Failed to create tab: {}", e);
return HttpResponse::InternalServerError().body("Failed to create tab");
}
};
if let Err(e) = tab.navigate_to("https://www.rust-lang.org") {
log::error!("Failed to navigate: {}", e);
return HttpResponse::InternalServerError().body("Failed to navigate");
}
if let Err(e) = tab.wait_until_navigated() {
log::error!("Navigation timeout: {}", e);
return HttpResponse::InternalServerError().body("Navigation timeout");
}
let pdf_data = match tab.print_to_pdf(None) {
Ok(data) => data,
Err(e) => {
log::error!("Failed to generate PDF: {}", e);
return HttpResponse::InternalServerError().body("Failed to generate PDF");
}
};
log::info!("Generated PDF: {} bytes", pdf_data.len());
HttpResponse::Ok()
.content_type("application/pdf")
.insert_header((
"Content-Disposition",
"attachment; filename=\"rust-lang.pdf\"",
))
.body(pdf_data)
}
#[derive(serde::Deserialize)]
pub struct AdvancedPdfQuery {
pub filename: Option<String>,
pub landscape: Option<bool>,
pub print_background: Option<bool>,
pub waitsecs: Option<u64>,
pub scale: Option<f64>,
pub paper_width: Option<f64>,
pub paper_height: Option<f64>,
pub margin_top: Option<f64>,
pub margin_bottom: Option<f64>,
pub margin_left: Option<f64>,
pub margin_right: Option<f64>,
pub page_ranges: Option<String>,
pub display_header_footer: Option<bool>,
pub header_template: Option<String>,
pub footer_template: Option<String>,
pub prefer_css_page_size: Option<bool>,
pub offline_mode: Option<bool>,
}
async fn advanced_pdf_handler(
pool: web::Data<SharedPool>,
query: web::Query<AdvancedPdfQuery>,
) -> impl Responder {
log::info!("Advanced handler called: Generating secure enterprise report");
let untrusted_html = r#"
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: sans-serif; padding: 20px; }
h1 { color: #2c3e50; }
.confidential { color: #e74c3c; border: 1px solid #e74c3c; padding: 10px; margin-top: 20px; }
</style>
</head>
<body>
<h1>Quarterly Earnings Report</h1>
<p>This report was generated securely via the Advanced Profiling API.</p>
<div class="confidential">
<strong>CONFIDENTIAL DATA</strong>
<p>DO NOT DISTRIBUTE.</p>
</div>
<!-- This fetch will instantly fail because offline_mode is true -->
<script>
fetch('http://169.254.169.254/latest/meta-data/')
.then(r => console.log('Stolen metadata!', r))
.catch(e => console.error('Extraction prevented!', e));
</script>
</body>
</html>
"#.to_string();
let request = PdfFromHtmlRequest {
html: untrusted_html,
filename: query.filename.clone().or(Some("secure_report.pdf".to_string())),
paper_width: query.paper_width.or(Some(8.27)),
paper_height: query.paper_height.or(Some(11.69)),
margin_top: query.margin_top.or(Some(1.2)),
margin_bottom: query.margin_bottom.or(Some(1.2)),
margin_left: query.margin_left.or(Some(0.8)),
margin_right: query.margin_right.or(Some(0.8)),
display_header_footer: query.display_header_footer.or(Some(true)),
header_template: query.header_template.clone().or_else(|| {
Some(r#"<div style="font-size: 10px; color: #7f8c8d; text-align: right; width: 100%; border-bottom: 1px solid #bdc3c7; padding-bottom: 5px; margin: 0 40px;">Internal Organization Document | CONFIDENTIAL</div>"#.to_string())
}),
footer_template: query.footer_template.clone().or_else(|| {
Some(r#"<div style="font-size: 10px; color: #7f8c8d; text-align: center; width: 100%;">Page <span class="pageNumber"></span> of <span class="totalPages"></span></div>"#.to_string())
}),
offline_mode: query.offline_mode.or(Some(true)),
waitsecs: query.waitsecs.or(Some(1)),
landscape: query.landscape,
print_background: query.print_background,
scale: query.scale,
page_ranges: query.page_ranges.clone(),
prefer_css_page_size: query.prefer_css_page_size,
..Default::default()
};
let pool = pool.into_inner();
let result = web::block(move || generate_pdf_from_html(&pool, &request)).await;
match result {
Ok(Ok(pdf_response)) => HttpResponse::Ok()
.content_type("application/pdf")
.insert_header(("Content-Disposition", pdf_response.content_disposition()))
.body(pdf_response.data),
Ok(Err(service_error)) => {
log::error!("Service error: {}", service_error);
HttpResponse::InternalServerError().body(service_error.to_string())
}
Err(e) => {
log::error!("Blocking error: {}", e);
HttpResponse::InternalServerError().body("Internal Server Error")
}
}
}
async fn generate_pdf(pool: web::Data<SharedBrowserPool>) -> impl Responder {
let browser = match pool.get() {
Ok(b) => b,
Err(e) => {
log::error!("Failed to get browser: {}", e);
return HttpResponse::ServiceUnavailable().body(format!("Browser unavailable: {}", e));
}
};
log::info!("Got browser {} from pool", browser.id());
let tab = match browser.new_tab() {
Ok(t) => t,
Err(e) => {
log::error!("Failed to create tab: {}", e);
return HttpResponse::InternalServerError().body("Failed to create tab");
}
};
if let Err(e) = tab.navigate_to("https://google.com") {
log::error!("Failed to navigate: {}", e);
return HttpResponse::InternalServerError().body("Failed to navigate");
}
if let Err(e) = tab.wait_until_navigated() {
log::error!("Navigation timeout: {}", e);
return HttpResponse::InternalServerError().body("Navigation timeout");
}
let pdf_data = match tab.print_to_pdf(None) {
Ok(data) => data,
Err(e) => {
log::error!("Failed to generate PDF: {}", e);
return HttpResponse::InternalServerError().body("Failed to generate PDF");
}
};
log::info!("Generated PDF: {} bytes", pdf_data.len());
HttpResponse::Ok()
.content_type("application/pdf")
.insert_header(("Content-Disposition", "attachment; filename=\"google.pdf\""))
.body(pdf_data)
}
async fn legacy_pool_stats(pool: web::Data<SharedBrowserPool>) -> impl Responder {
let stats = pool.stats();
HttpResponse::Ok().json(serde_json::json!({
"available": stats.available,
"active": stats.active,
"total": stats.total
}))
}
async fn legacy_health() -> impl Responder {
HttpResponse::Ok().body("OK")
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
log::info!("Starting Actix-web example...");
log::info!("This example demonstrates multiple integration approaches:");
log::info!(" 1. Pre-built routes (configure_routes)");
log::info!(" 2. Custom handlers with service functions");
log::info!(" 3. Manual browser control");
let pool = BrowserPool::builder()
.config(
BrowserPoolConfigBuilder::new()
.max_pool_size(3)
.warmup_count(2)
.browser_ttl(Duration::from_secs(3600))
.ping_interval(Duration::from_secs(15)) .build()
.expect("Invalid configuration"),
)
.factory(Box::new(ChromeBrowserFactory::with_defaults()))
.build()
.expect("Failed to create browser pool");
log::info!("Browser pool created, warming up...");
pool.warmup().await.expect("Failed to warmup pool");
log::info!("Pool warmed up successfully");
let shared_pool: SharedBrowserPool = Arc::new(pool);
let shutdown_pool = Arc::clone(&shared_pool);
log::info!("Starting server on http://localhost:8080");
log::info!("");
log::info!("Available endpoints:");
log::info!(" Pre-built handlers (from configure_routes):");
log::info!(" GET http://localhost:8080/pdf?url=https://example.com");
log::info!(" POST http://localhost:8080/pdf/html");
log::info!(" GET http://localhost:8080/pool/stats");
log::info!(" GET http://localhost:8080/health");
log::info!(" GET http://localhost:8080/ready");
log::info!("");
log::info!(" Custom handlers:");
log::info!(" GET http://localhost:8080/custom/pdf?url=https://example.com");
log::info!(" GET http://localhost:8080/manual/pdf");
log::info!(" GET http://localhost:8080/advanced/pdf");
log::info!("");
log::info!(" Legacy handlers (backward compatibility):");
log::info!(" GET http://localhost:8080/legacy/pdf");
log::info!(" GET http://localhost:8080/legacy/stats");
log::info!(" GET http://localhost:8080/legacy/health");
let server = HttpServer::new(move || {
App::new()
.app_data(web::Data::new(Arc::clone(&shared_pool)))
.configure(configure_routes)
.route("/custom/pdf", web::get().to(custom_pdf_handler))
.route("/manual/pdf", web::get().to(manual_pdf_handler))
.route("/advanced/pdf", web::get().to(advanced_pdf_handler))
.service(
web::scope("/legacy")
.route("/pdf", web::get().to(generate_pdf))
.route("/stats", web::get().to(legacy_pool_stats))
.route("/health", web::get().to(legacy_health)),
)
})
.bind("127.0.0.1:8080")?
.run();
let result = server.await;
log::info!("Server stopped, cleaning up browser pool...");
if let Some(mut pool) = Arc::into_inner(shutdown_pool) {
pool.shutdown();
}
log::info!("Cleanup complete");
result
}