Skip to main content

html2pdf_api/integrations/
actix.rs

1//! Actix-web framework integration.
2//!
3//! This module provides helpers and pre-built handlers for using `BrowserPool`
4//! with Actix-web. You can choose between using the pre-built handlers for
5//! quick setup, or writing custom handlers for full control.
6//!
7//! # Quick Start
8//!
9//! ## Option 1: Pre-built Routes (Fastest Setup)
10//!
11//! Use [`configure_routes`] to add all PDF endpoints with a single line:
12//!
13//! ```rust,ignore
14//! use actix_web::{App, HttpServer, web};
15//! use html2pdf_api::prelude::*;
16//!
17//! #[actix_web::main]
18//! async fn main() -> std::io::Result<()> {
19//!     let pool = init_browser_pool().await
20//!         .expect("Failed to initialize browser pool");
21//!
22//!     HttpServer::new(move || {
23//!         App::new()
24//!             .app_data(web::Data::new(pool.clone()))
25//!             .configure(html2pdf_api::integrations::actix::configure_routes)
26//!     })
27//!     .bind("127.0.0.1:8080")?
28//!     .run()
29//!     .await
30//! }
31//! ```
32//!
33//! This gives you the following endpoints:
34//!
35//! | Method | Path | Description |
36//! |--------|------|-------------|
37//! | GET | `/pdf?url=...` | Convert URL to PDF |
38//! | POST | `/pdf/html` | Convert HTML to PDF |
39//! | GET | `/pool/stats` | Pool statistics |
40//! | GET | `/health` | Health check |
41//! | GET | `/ready` | Readiness check |
42//!
43//! ## Option 2: Mix Pre-built and Custom Handlers
44//!
45//! Use individual pre-built handlers alongside your own:
46//!
47//! ```rust,ignore
48//! use actix_web::{App, HttpServer, web};
49//! use html2pdf_api::prelude::*;
50//! use html2pdf_api::integrations::actix::{pdf_from_url, health_check};
51//!
52//! async fn my_custom_handler() -> impl Responder {
53//!     // Your custom logic
54//! }
55//!
56//! #[actix_web::main]
57//! async fn main() -> std::io::Result<()> {
58//!     let pool = init_browser_pool().await?;
59//!
60//!     HttpServer::new(move || {
61//!         App::new()
62//!             .app_data(web::Data::new(pool.clone()))
63//!             // Pre-built handlers
64//!             .route("/pdf", web::get().to(pdf_from_url))
65//!             .route("/health", web::get().to(health_check))
66//!             // Custom handler
67//!             .route("/custom", web::get().to(my_custom_handler))
68//!     })
69//!     .bind("127.0.0.1:8080")?
70//!     .run()
71//!     .await
72//! }
73//! ```
74//!
75//! ## Option 3: Custom Handlers with Service Functions
76//!
77//! For full control, use the service functions directly:
78//!
79//! ```rust,ignore
80//! use actix_web::{web, HttpResponse, Responder};
81//! use html2pdf_api::prelude::*;
82//! use html2pdf_api::service::{generate_pdf_from_url, PdfFromUrlRequest};
83//!
84//! async fn my_pdf_handler(
85//!     pool: web::Data<SharedBrowserPool>,
86//!     query: web::Query<PdfFromUrlRequest>,
87//! ) -> impl Responder {
88//!     // Custom pre-processing: auth, rate limiting, logging, etc.
89//!     log::info!("Custom handler: {}", query.url);
90//!
91//!     let pool = pool.into_inner();
92//!     let request = query.into_inner();
93//!
94//!     // Call service in blocking context
95//!     let result = web::block(move || {
96//!         generate_pdf_from_url(&pool, &request)
97//!     }).await;
98//!
99//!     match result {
100//!         Ok(Ok(pdf)) => {
101//!             // Custom post-processing
102//!             HttpResponse::Ok()
103//!                 .content_type("application/pdf")
104//!                 .insert_header(("X-Custom-Header", "value"))
105//!                 .body(pdf.data)
106//!         }
107//!         Ok(Err(e)) => HttpResponse::BadRequest().body(e.to_string()),
108//!         Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
109//!     }
110//! }
111//! ```
112//!
113//! ## Option 4: Full Manual Control (Original Approach)
114//!
115//! For complete control over browser operations:
116//!
117//! ```rust,ignore
118//! use actix_web::{web, HttpResponse, Responder};
119//! use html2pdf_api::prelude::*;
120//!
121//! async fn manual_pdf_handler(
122//!     pool: web::Data<SharedBrowserPool>,
123//! ) -> impl Responder {
124//!     let pool_guard = match pool.lock() {
125//!         Ok(guard) => guard,
126//!         Err(e) => return HttpResponse::InternalServerError().body(e.to_string()),
127//!     };
128//!
129//!     let browser = match pool_guard.get() {
130//!         Ok(b) => b,
131//!         Err(e) => return HttpResponse::ServiceUnavailable().body(e.to_string()),
132//!     };
133//!
134//!     let tab = browser.new_tab().unwrap();
135//!     tab.navigate_to("https://example.com").unwrap();
136//!     tab.wait_until_navigated().unwrap();
137//!
138//!     let pdf_data = tab.print_to_pdf(None).unwrap();
139//!
140//!     HttpResponse::Ok()
141//!         .content_type("application/pdf")
142//!         .body(pdf_data)
143//! }
144//! ```
145//!
146//! # Setup
147//!
148//! Add to your `Cargo.toml`:
149//!
150//! ```toml
151//! [dependencies]
152//! html2pdf-api = { version = "0.2", features = ["actix-integration"] }
153//! actix-web = "4"
154//! ```
155//!
156//! # Graceful Shutdown
157//!
158//! For proper cleanup, shutdown the pool when the server stops:
159//!
160//! ```rust,ignore
161//! use actix_web::{App, HttpServer, web};
162//! use html2pdf_api::prelude::*;
163//! use std::sync::Arc;
164//!
165//! #[actix_web::main]
166//! async fn main() -> std::io::Result<()> {
167//!     let pool = init_browser_pool().await
168//!         .expect("Failed to initialize browser pool");
169//!
170//!     // Keep a reference for shutdown
171//!     let shutdown_pool = pool.clone();
172//!
173//!     let server = HttpServer::new(move || {
174//!         App::new()
175//!             .app_data(web::Data::new(pool.clone()))
176//!             .configure(html2pdf_api::integrations::actix::configure_routes)
177//!     })
178//!     .bind("127.0.0.1:8080")?
179//!     .run();
180//!
181//!     // Run server
182//!     let result = server.await;
183//!
184//!     // Cleanup pool after server stops
185//!     if let Ok(mut pool) = shutdown_pool.lock() {
186//!         pool.shutdown();
187//!     }
188//!
189//!     result
190//! }
191//! ```
192//!
193//! # API Reference
194//!
195//! ## Pre-built Handlers
196//!
197//! | Handler | Method | Default Path | Description |
198//! |---------|--------|--------------|-------------|
199//! | [`pdf_from_url`] | GET | `/pdf` | Convert URL to PDF |
200//! | [`pdf_from_html`] | POST | `/pdf/html` | Convert HTML to PDF |
201//! | [`pool_stats`] | GET | `/pool/stats` | Pool statistics |
202//! | [`health_check`] | GET | `/health` | Health check (always 200) |
203//! | [`readiness_check`] | GET | `/ready` | Readiness check (checks pool) |
204//!
205//! ## Type Aliases
206//!
207//! | Type | Description |
208//! |------|-------------|
209//! | [`SharedPool`] | `Arc<Mutex<BrowserPool>>` - for service functions |
210//! | [`BrowserPoolData`] | `web::Data<SharedBrowserPool>` - for handler parameters |
211//!
212//! ## Helper Functions
213//!
214//! | Function | Description |
215//! |----------|-------------|
216//! | [`configure_routes`] | Configure all pre-built routes |
217//! | [`create_pool_data`] | Wrap `SharedBrowserPool` in `web::Data` |
218//! | [`create_pool_data_from_arc`] | Wrap `Arc<Mutex<BrowserPool>>` in `web::Data` |
219//!
220//! ## Extension Traits
221//!
222//! | Trait | Description |
223//! |-------|-------------|
224//! | [`BrowserPoolActixExt`] | Adds `into_actix_data()` to `BrowserPool` |
225
226use actix_web::{HttpResponse, Responder, http::header, web};
227use std::sync::Arc;
228use std::time::Duration;
229
230use crate::SharedBrowserPool;
231use crate::pool::BrowserPool;
232use crate::service::{
233    self, DEFAULT_TIMEOUT_SECS, ErrorResponse, HealthResponse, PdfFromHtmlRequest,
234    PdfFromUrlRequest, PdfServiceError,
235};
236
237// ============================================================================
238// Type Aliases
239// ============================================================================
240
241/// Type alias for shared browser pool.
242///
243/// This is the standard pool type used by the service functions.
244/// It's an `Arc<BrowserPool>` which allows safe sharing across
245/// threads and handlers. No outer `Mutex` needed — the pool uses
246/// fine-grained internal locks.
247///
248/// # Usage
249///
250/// ```rust,ignore
251/// use html2pdf_api::integrations::actix::SharedPool;
252///
253/// fn my_function(pool: &SharedPool) {
254///     let browser = pool.get().unwrap();
255///     // ...
256/// }
257/// ```
258pub type SharedPool = Arc<BrowserPool>;
259
260/// Type alias for Actix-web `Data` wrapper around the shared pool.
261///
262/// Use this type in your handler parameters for automatic extraction:
263///
264/// ```rust,ignore
265/// use html2pdf_api::integrations::actix::BrowserPoolData;
266///
267/// async fn handler(pool: BrowserPoolData) -> impl Responder {
268///     let browser = pool.get().unwrap();
269///     // ...
270/// }
271/// ```
272///
273/// # Note
274///
275/// `BrowserPoolData` and `web::Data<SharedPool>` are interchangeable.
276/// Use whichever is more convenient for your code.
277pub type BrowserPoolData = web::Data<SharedBrowserPool>;
278
279// ============================================================================
280// Pre-built Handlers
281// ============================================================================
282
283/// Generate PDF from a URL.
284///
285/// This handler converts a web page to PDF using the browser pool.
286///
287/// # Endpoint
288///
289/// ```text
290/// GET /pdf?url=https://example.com&filename=output.pdf
291/// ```
292///
293/// # Query Parameters
294///
295/// | Parameter | Type | Required | Default | Description |
296/// |-----------|------|----------|---------|-------------|
297/// | `url` | string | **Yes** | - | URL to convert (must be valid HTTP/HTTPS) |
298/// | `filename` | string | No | `"document.pdf"` | Output filename |
299/// | `waitsecs` | u64 | No | `5` | Seconds to wait for JavaScript |
300/// | `landscape` | bool | No | `false` | Use landscape orientation |
301/// | `download` | bool | No | `false` | Force download vs inline display |
302/// | `print_background` | bool | No | `true` | Include background graphics |
303///
304/// # Response
305///
306/// ## Success (200 OK)
307///
308/// Returns PDF binary data with headers:
309/// - `Content-Type: application/pdf`
310/// - `Content-Disposition: inline; filename="document.pdf"` (or `attachment` if `download=true`)
311/// - `Cache-Control: no-cache`
312///
313/// ## Errors
314///
315/// | Status | Code | Description |
316/// |--------|------|-------------|
317/// | 400 | `INVALID_URL` | URL is empty or malformed |
318/// | 502 | `NAVIGATION_FAILED` | Failed to load the URL |
319/// | 503 | `BROWSER_UNAVAILABLE` | No browsers available in pool |
320/// | 504 | `TIMEOUT` | Operation timed out |
321///
322/// # Examples
323///
324/// ## Basic Request
325///
326/// ```text
327/// GET /pdf?url=https://example.com
328/// ```
329///
330/// ## With Options
331///
332/// ```text
333/// GET /pdf?url=https://example.com/report&filename=report.pdf&landscape=true&waitsecs=10
334/// ```
335///
336/// ## Force Download
337///
338/// ```text
339/// GET /pdf?url=https://example.com&download=true&filename=download.pdf
340/// ```
341///
342/// # Usage in App
343///
344/// ```rust,ignore
345/// App::new()
346///     .app_data(web::Data::new(pool.clone()))
347///     .route("/pdf", web::get().to(pdf_from_url))
348/// ```
349pub async fn pdf_from_url(
350    pool: web::Data<SharedPool>,
351    query: web::Query<PdfFromUrlRequest>,
352) -> impl Responder {
353    let request = query.into_inner();
354    let pool = pool.into_inner();
355
356    log::debug!("PDF from URL request: {}", request.url);
357
358    // Run blocking PDF generation with timeout
359    let result = tokio::time::timeout(
360        Duration::from_secs(DEFAULT_TIMEOUT_SECS),
361        web::block(move || service::generate_pdf_from_url(&pool, &request)),
362    )
363    .await;
364
365    match result {
366        Ok(Ok(Ok(response))) => build_pdf_response(response),
367        Ok(Ok(Err(e))) => build_error_response(e),
368        Ok(Err(blocking_err)) => {
369            log::error!("Blocking task error: {}", blocking_err);
370            build_error_response(PdfServiceError::Internal(blocking_err.to_string()))
371        }
372        Err(_timeout) => {
373            log::error!(
374                "PDF generation timed out after {} seconds",
375                DEFAULT_TIMEOUT_SECS
376            );
377            build_error_response(PdfServiceError::Timeout(format!(
378                "Operation timed out after {} seconds",
379                DEFAULT_TIMEOUT_SECS
380            )))
381        }
382    }
383}
384
385/// Generate PDF from HTML content.
386///
387/// This handler converts HTML content directly to PDF without requiring
388/// a web server to host the HTML.
389///
390/// # Endpoint
391///
392/// ```text
393/// POST /pdf/html
394/// Content-Type: application/json
395/// ```
396///
397/// # Request Body
398///
399/// ```json
400/// {
401///     "html": "<html><body><h1>Hello World</h1></body></html>",
402///     "filename": "document.pdf",
403///     "waitsecs": 2,
404///     "landscape": false,
405///     "download": false,
406///     "print_background": true
407/// }
408/// ```
409///
410/// | Field | Type | Required | Default | Description |
411/// |-------|------|----------|---------|-------------|
412/// | `html` | string | **Yes** | - | HTML content to convert |
413/// | `filename` | string | No | `"document.pdf"` | Output filename |
414/// | `waitsecs` | u64 | No | `2` | Seconds to wait for JavaScript |
415/// | `landscape` | bool | No | `false` | Use landscape orientation |
416/// | `download` | bool | No | `false` | Force download vs inline display |
417/// | `print_background` | bool | No | `true` | Include background graphics |
418///
419/// # Response
420///
421/// Same as [`pdf_from_url`].
422///
423/// # Errors
424///
425/// | Status | Code | Description |
426/// |--------|------|-------------|
427/// | 400 | `EMPTY_HTML` | HTML content is empty or whitespace |
428/// | 502 | `PDF_GENERATION_FAILED` | Failed to generate PDF |
429/// | 503 | `BROWSER_UNAVAILABLE` | No browsers available |
430/// | 504 | `TIMEOUT` | Operation timed out |
431///
432/// # Example Request
433///
434/// ```bash
435/// curl -X POST http://localhost:8080/pdf/html \
436///   -H "Content-Type: application/json" \
437///   -d '{"html": "<h1>Hello</h1>", "filename": "hello.pdf"}' \
438///   --output hello.pdf
439/// ```
440///
441/// # Usage in App
442///
443/// ```rust,ignore
444/// App::new()
445///     .app_data(web::Data::new(pool.clone()))
446///     .route("/pdf/html", web::post().to(pdf_from_html))
447/// ```
448pub async fn pdf_from_html(
449    pool: web::Data<SharedPool>,
450    body: web::Json<PdfFromHtmlRequest>,
451) -> impl Responder {
452    let request = body.into_inner();
453    let pool = pool.into_inner();
454
455    log::debug!("PDF from HTML request: {} bytes", request.html.len());
456
457    let result = tokio::time::timeout(
458        Duration::from_secs(DEFAULT_TIMEOUT_SECS),
459        web::block(move || service::generate_pdf_from_html(&pool, &request)),
460    )
461    .await;
462
463    match result {
464        Ok(Ok(Ok(response))) => build_pdf_response(response),
465        Ok(Ok(Err(e))) => build_error_response(e),
466        Ok(Err(blocking_err)) => {
467            log::error!("Blocking task error: {}", blocking_err);
468            build_error_response(PdfServiceError::Internal(blocking_err.to_string()))
469        }
470        Err(_timeout) => {
471            log::error!("PDF generation timed out");
472            build_error_response(PdfServiceError::Timeout(format!(
473                "Operation timed out after {} seconds",
474                DEFAULT_TIMEOUT_SECS
475            )))
476        }
477    }
478}
479
480/// Get browser pool statistics.
481///
482/// Returns real-time metrics about the browser pool including available
483/// browsers, active browsers, and total count.
484///
485/// # Endpoint
486///
487/// ```text
488/// GET /pool/stats
489/// ```
490///
491/// # Response (200 OK)
492///
493/// ```json
494/// {
495///     "available": 3,
496///     "active": 2,
497///     "total": 5
498/// }
499/// ```
500///
501/// | Field | Type | Description |
502/// |-------|------|-------------|
503/// | `available` | number | Browsers ready to handle requests |
504/// | `active` | number | Browsers currently in use |
505/// | `total` | number | Total browsers (available + active) |
506///
507/// # Errors
508///
509/// | Status | Code | Description |
510/// |--------|------|-------------|
511/// | 500 | `POOL_LOCK_FAILED` | Failed to acquire pool lock |
512///
513/// # Use Cases
514///
515/// - Monitoring dashboards
516/// - Prometheus/Grafana metrics
517/// - Capacity planning
518/// - Debugging pool exhaustion
519///
520/// # Usage in App
521///
522/// ```rust,ignore
523/// App::new()
524///     .app_data(web::Data::new(pool.clone()))
525///     .route("/pool/stats", web::get().to(pool_stats))
526/// ```
527pub async fn pool_stats(pool: web::Data<SharedPool>) -> impl Responder {
528    match service::get_pool_stats(&pool) {
529        Ok(stats) => HttpResponse::Ok().json(stats),
530        Err(e) => build_error_response(e),
531    }
532}
533
534/// Health check endpoint.
535///
536/// Simple endpoint that returns 200 OK if the service is running.
537/// Does not check pool health - use [`readiness_check`] for that.
538///
539/// # Endpoint
540///
541/// ```text
542/// GET /health
543/// ```
544///
545/// # Response (200 OK)
546///
547/// ```json
548/// {
549///     "status": "healthy",
550///     "service": "html2pdf-api"
551/// }
552/// ```
553///
554/// # Use Cases
555///
556/// - Kubernetes liveness probe
557/// - Load balancer health check
558/// - Uptime monitoring
559///
560/// # Kubernetes Example
561///
562/// ```yaml
563/// livenessProbe:
564///   httpGet:
565///     path: /health
566///     port: 8080
567///   initialDelaySeconds: 10
568///   periodSeconds: 30
569/// ```
570///
571/// # Usage in App
572///
573/// ```rust,ignore
574/// App::new()
575///     .route("/health", web::get().to(health_check))
576/// ```
577pub async fn health_check() -> impl Responder {
578    HttpResponse::Ok().json(HealthResponse::default())
579}
580
581/// Readiness check endpoint.
582///
583/// Returns 200 OK if the pool has capacity to handle requests,
584/// 503 Service Unavailable otherwise.
585///
586/// Unlike [`health_check`], this actually checks the pool state.
587///
588/// # Endpoint
589///
590/// ```text
591/// GET /ready
592/// ```
593///
594/// # Response
595///
596/// ## Ready (200 OK)
597///
598/// ```json
599/// {
600///     "status": "ready"
601/// }
602/// ```
603///
604/// ## Not Ready (503 Service Unavailable)
605///
606/// ```json
607/// {
608///     "status": "not_ready",
609///     "reason": "no_available_capacity"
610/// }
611/// ```
612///
613/// # Readiness Criteria
614///
615/// The service is "ready" if either:
616/// - There are idle browsers available (`available > 0`), OR
617/// - There is capacity to create new browsers (`active < max_pool_size`)
618///
619/// # Use Cases
620///
621/// - Kubernetes readiness probe
622/// - Load balancer health check (remove from rotation when busy)
623/// - Auto-scaling triggers
624///
625/// # Kubernetes Example
626///
627/// ```yaml
628/// readinessProbe:
629///   httpGet:
630///     path: /ready
631///     port: 8080
632///   initialDelaySeconds: 5
633///   periodSeconds: 10
634/// ```
635///
636/// # Usage in App
637///
638/// ```rust,ignore
639/// App::new()
640///     .app_data(web::Data::new(pool.clone()))
641///     .route("/ready", web::get().to(readiness_check))
642/// ```
643pub async fn readiness_check(pool: web::Data<SharedPool>) -> impl Responder {
644    match service::is_pool_ready(&pool) {
645        Ok(true) => HttpResponse::Ok().json(serde_json::json!({
646            "status": "ready"
647        })),
648        Ok(false) => HttpResponse::ServiceUnavailable().json(serde_json::json!({
649            "status": "not_ready",
650            "reason": "no_available_capacity"
651        })),
652        Err(e) => HttpResponse::ServiceUnavailable().json(ErrorResponse::from(e)),
653    }
654}
655
656// ============================================================================
657// Route Configuration
658// ============================================================================
659
660/// Configure all PDF routes.
661///
662/// Adds all pre-built handlers to the Actix-web app with default paths.
663/// This is the easiest way to set up the PDF service.
664///
665/// # Routes Added
666///
667/// | Method | Path | Handler | Description |
668/// |--------|------|---------|-------------|
669/// | GET | `/pdf` | [`pdf_from_url`] | Convert URL to PDF |
670/// | POST | `/pdf/html` | [`pdf_from_html`] | Convert HTML to PDF |
671/// | GET | `/pool/stats` | [`pool_stats`] | Pool statistics |
672/// | GET | `/health` | [`health_check`] | Health check |
673/// | GET | `/ready` | [`readiness_check`] | Readiness check |
674///
675/// # Example
676///
677/// ```rust,ignore
678/// use actix_web::{App, HttpServer, web};
679/// use html2pdf_api::prelude::*;
680/// use html2pdf_api::integrations::actix::configure_routes;
681///
682/// #[actix_web::main]
683/// async fn main() -> std::io::Result<()> {
684///     let pool = init_browser_pool().await?;
685///
686///     HttpServer::new(move || {
687///         App::new()
688///             .app_data(web::Data::new(pool.clone()))
689///             .configure(configure_routes)
690///     })
691///     .bind("127.0.0.1:8080")?
692///     .run()
693///     .await
694/// }
695/// ```
696///
697/// # Adding Custom Routes
698///
699/// You can combine `configure_routes` with additional routes:
700///
701/// ```rust,ignore
702/// App::new()
703///     .app_data(web::Data::new(pool.clone()))
704///     .configure(configure_routes)  // Pre-built routes
705///     .route("/custom", web::get().to(my_custom_handler))  // Your routes
706/// ```
707///
708/// # Custom Path Prefix
709///
710/// To mount routes under a prefix, use `web::scope`:
711///
712/// ```rust,ignore
713/// App::new()
714///     .app_data(web::Data::new(pool.clone()))
715///     .service(
716///         web::scope("/api/v1")
717///             .configure(configure_routes)
718///     )
719/// // Routes will be: /api/v1/pdf, /api/v1/health, etc.
720/// ```
721pub fn configure_routes(cfg: &mut web::ServiceConfig) {
722    cfg.route("/pdf", web::get().to(pdf_from_url))
723        .route("/pdf/html", web::post().to(pdf_from_html))
724        .route("/pool/stats", web::get().to(pool_stats))
725        .route("/health", web::get().to(health_check))
726        .route("/ready", web::get().to(readiness_check));
727}
728
729// ============================================================================
730// Response Builders (Internal)
731// ============================================================================
732
733/// Build HTTP response for successful PDF generation.
734fn build_pdf_response(response: crate::service::PdfResponse) -> HttpResponse {
735    log::info!(
736        "PDF generated successfully: {} bytes, filename={}",
737        response.size(),
738        response.filename
739    );
740
741    HttpResponse::Ok()
742        .content_type("application/pdf")
743        .insert_header((header::CACHE_CONTROL, "no-cache"))
744        .insert_header((header::CONTENT_DISPOSITION, response.content_disposition()))
745        .body(response.data)
746}
747
748/// Build HTTP response for errors.
749fn build_error_response(error: PdfServiceError) -> HttpResponse {
750    let status_code = error.status_code();
751    let body = ErrorResponse::from(&error);
752
753    log::warn!("PDF generation error: {} (HTTP {})", error, status_code);
754
755    match status_code {
756        400 => HttpResponse::BadRequest().json(body),
757        502 => HttpResponse::BadGateway().json(body),
758        503 => HttpResponse::ServiceUnavailable().json(body),
759        504 => HttpResponse::GatewayTimeout().json(body),
760        _ => HttpResponse::InternalServerError().json(body),
761    }
762}
763
764// ============================================================================
765// Extension Trait (Backward Compatibility)
766// ============================================================================
767
768/// Extension trait for `BrowserPool` with Actix-web helpers.
769///
770/// Provides convenient methods for integrating with Actix-web.
771///
772/// # Example
773///
774/// ```rust,ignore
775/// use html2pdf_api::integrations::actix::BrowserPoolActixExt;
776///
777/// let pool = BrowserPool::builder()
778///     .factory(Box::new(ChromeBrowserFactory::with_defaults()))
779///     .build()?;
780///
781/// pool.warmup().await?;
782///
783/// // Convert directly to Actix-web Data
784/// let pool_data = pool.into_actix_data();
785///
786/// HttpServer::new(move || {
787///     App::new()
788///         .app_data(pool_data.clone())
789///         .configure(configure_routes)
790/// })
791/// ```
792pub trait BrowserPoolActixExt {
793    /// Convert the pool into Actix-web `Data` wrapper.
794    ///
795    /// This is equivalent to calling `into_shared()` and then wrapping
796    /// with `web::Data::new()`.
797    ///
798    /// # Example
799    ///
800    /// ```rust,ignore
801    /// use html2pdf_api::integrations::actix::BrowserPoolActixExt;
802    ///
803    /// let pool = BrowserPool::builder()
804    ///     .factory(Box::new(ChromeBrowserFactory::with_defaults()))
805    ///     .build()?;
806    ///
807    /// let pool_data = pool.into_actix_data();
808    ///
809    /// HttpServer::new(move || {
810    ///     App::new()
811    ///         .app_data(pool_data.clone())
812    /// })
813    /// ```
814    fn into_actix_data(self) -> BrowserPoolData;
815}
816
817impl BrowserPoolActixExt for BrowserPool {
818    fn into_actix_data(self) -> BrowserPoolData {
819        web::Data::new(self.into_shared())
820    }
821}
822
823// ============================================================================
824// Helper Functions (Backward Compatibility)
825// ============================================================================
826
827/// Create Actix-web `Data` from an existing shared pool.
828///
829/// Use this when you already have a `SharedBrowserPool` and want to
830/// wrap it for Actix-web.
831///
832/// # Parameters
833///
834/// * `pool` - The shared browser pool.
835///
836/// # Returns
837///
838/// `BrowserPoolData` ready for use with `App::app_data()`.
839///
840/// # Example
841///
842/// ```rust,ignore
843/// use html2pdf_api::integrations::actix::create_pool_data;
844///
845/// let shared_pool = pool.into_shared();
846/// let pool_data = create_pool_data(shared_pool);
847///
848/// App::new().app_data(pool_data)
849/// ```
850pub fn create_pool_data(pool: SharedBrowserPool) -> BrowserPoolData {
851    web::Data::new(pool)
852}
853
854/// Create Actix-web `Data` from an `Arc` reference.
855///
856/// Use this when you need to keep a reference to the pool for shutdown.
857///
858/// # Parameters
859///
860/// * `pool` - Arc reference to the shared browser pool.
861///
862/// # Returns
863///
864/// `BrowserPoolData` ready for use with `App::app_data()`.
865///
866/// # Example
867///
868/// ```rust,ignore
869/// use html2pdf_api::integrations::actix::create_pool_data_from_arc;
870///
871/// let shared_pool = pool.into_shared();
872/// let pool_for_shutdown = Arc::clone(&shared_pool);
873/// let pool_data = create_pool_data_from_arc(shared_pool);
874///
875/// // Use pool_data in App
876/// // Use pool_for_shutdown for cleanup
877/// ```
878pub fn create_pool_data_from_arc(pool: Arc<BrowserPool>) -> BrowserPoolData {
879    web::Data::new(pool)
880}
881
882// ============================================================================
883// Tests
884// ============================================================================
885
886#[cfg(test)]
887mod tests {
888    use super::*;
889
890    #[test]
891    fn test_type_alias_compiles() {
892        // Verify the type alias is valid
893        fn _accepts_pool_data(_: BrowserPoolData) {}
894        fn _accepts_shared_pool(_: SharedPool) {}
895    }
896
897    #[tokio::test]
898    async fn test_shared_pool_type_matches() {
899        // SharedPool and SharedBrowserPool should be compatible
900        fn _takes_shared_pool(_: SharedPool) {}
901        fn _returns_shared_browser_pool() -> SharedBrowserPool {
902            Arc::new(
903                BrowserPool::builder()
904                    .factory(Box::new(crate::factory::mock::MockBrowserFactory::new()))
905                    .build()
906                    .unwrap(),
907            )
908        }
909
910        // This should compile, proving type compatibility
911        let pool: SharedBrowserPool = _returns_shared_browser_pool();
912        let _: SharedPool = pool;
913    }
914}