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, Mutex};
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<Mutex<BrowserPool>>` which allows safe sharing across
245/// threads and handlers.
246///
247/// # Usage
248///
249/// ```rust,ignore
250/// use html2pdf_api::integrations::actix::SharedPool;
251///
252/// fn my_function(pool: &SharedPool) {
253///     let guard = pool.lock().unwrap();
254///     let browser = guard.get().unwrap();
255///     // ...
256/// }
257/// ```
258pub type SharedPool = Arc<Mutex<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 pool_guard = pool.lock().unwrap();
269///     let browser = pool_guard.get()?;
270///     // ...
271/// }
272/// ```
273///
274/// # Note
275///
276/// `BrowserPoolData` and `web::Data<SharedPool>` are interchangeable.
277/// Use whichever is more convenient for your code.
278pub type BrowserPoolData = web::Data<SharedBrowserPool>;
279
280// ============================================================================
281// Pre-built Handlers
282// ============================================================================
283
284/// Generate PDF from a URL.
285///
286/// This handler converts a web page to PDF using the browser pool.
287///
288/// # Endpoint
289///
290/// ```text
291/// GET /pdf?url=https://example.com&filename=output.pdf
292/// ```
293///
294/// # Query Parameters
295///
296/// | Parameter | Type | Required | Default | Description |
297/// |-----------|------|----------|---------|-------------|
298/// | `url` | string | **Yes** | - | URL to convert (must be valid HTTP/HTTPS) |
299/// | `filename` | string | No | `"document.pdf"` | Output filename |
300/// | `waitsecs` | u64 | No | `5` | Seconds to wait for JavaScript |
301/// | `landscape` | bool | No | `false` | Use landscape orientation |
302/// | `download` | bool | No | `false` | Force download vs inline display |
303/// | `print_background` | bool | No | `true` | Include background graphics |
304///
305/// # Response
306///
307/// ## Success (200 OK)
308///
309/// Returns PDF binary data with headers:
310/// - `Content-Type: application/pdf`
311/// - `Content-Disposition: inline; filename="document.pdf"` (or `attachment` if `download=true`)
312/// - `Cache-Control: no-cache`
313///
314/// ## Errors
315///
316/// | Status | Code | Description |
317/// |--------|------|-------------|
318/// | 400 | `INVALID_URL` | URL is empty or malformed |
319/// | 502 | `NAVIGATION_FAILED` | Failed to load the URL |
320/// | 503 | `BROWSER_UNAVAILABLE` | No browsers available in pool |
321/// | 504 | `TIMEOUT` | Operation timed out |
322///
323/// # Examples
324///
325/// ## Basic Request
326///
327/// ```text
328/// GET /pdf?url=https://example.com
329/// ```
330///
331/// ## With Options
332///
333/// ```text
334/// GET /pdf?url=https://example.com/report&filename=report.pdf&landscape=true&waitsecs=10
335/// ```
336///
337/// ## Force Download
338///
339/// ```text
340/// GET /pdf?url=https://example.com&download=true&filename=download.pdf
341/// ```
342///
343/// # Usage in App
344///
345/// ```rust,ignore
346/// App::new()
347///     .app_data(web::Data::new(pool.clone()))
348///     .route("/pdf", web::get().to(pdf_from_url))
349/// ```
350pub async fn pdf_from_url(
351    pool: web::Data<SharedPool>,
352    query: web::Query<PdfFromUrlRequest>,
353) -> impl Responder {
354    let request = query.into_inner();
355    let pool = pool.into_inner();
356
357    log::debug!("PDF from URL request: {}", request.url);
358
359    // Run blocking PDF generation with timeout
360    let result = tokio::time::timeout(
361        Duration::from_secs(DEFAULT_TIMEOUT_SECS),
362        web::block(move || service::generate_pdf_from_url(&pool, &request)),
363    )
364    .await;
365
366    match result {
367        Ok(Ok(Ok(response))) => build_pdf_response(response),
368        Ok(Ok(Err(e))) => build_error_response(e),
369        Ok(Err(blocking_err)) => {
370            log::error!("Blocking task error: {}", blocking_err);
371            build_error_response(PdfServiceError::Internal(blocking_err.to_string()))
372        }
373        Err(_timeout) => {
374            log::error!(
375                "PDF generation timed out after {} seconds",
376                DEFAULT_TIMEOUT_SECS
377            );
378            build_error_response(PdfServiceError::Timeout(format!(
379                "Operation timed out after {} seconds",
380                DEFAULT_TIMEOUT_SECS
381            )))
382        }
383    }
384}
385
386/// Generate PDF from HTML content.
387///
388/// This handler converts HTML content directly to PDF without requiring
389/// a web server to host the HTML.
390///
391/// # Endpoint
392///
393/// ```text
394/// POST /pdf/html
395/// Content-Type: application/json
396/// ```
397///
398/// # Request Body
399///
400/// ```json
401/// {
402///     "html": "<html><body><h1>Hello World</h1></body></html>",
403///     "filename": "document.pdf",
404///     "waitsecs": 2,
405///     "landscape": false,
406///     "download": false,
407///     "print_background": true
408/// }
409/// ```
410///
411/// | Field | Type | Required | Default | Description |
412/// |-------|------|----------|---------|-------------|
413/// | `html` | string | **Yes** | - | HTML content to convert |
414/// | `filename` | string | No | `"document.pdf"` | Output filename |
415/// | `waitsecs` | u64 | No | `2` | Seconds to wait for JavaScript |
416/// | `landscape` | bool | No | `false` | Use landscape orientation |
417/// | `download` | bool | No | `false` | Force download vs inline display |
418/// | `print_background` | bool | No | `true` | Include background graphics |
419///
420/// # Response
421///
422/// Same as [`pdf_from_url`].
423///
424/// # Errors
425///
426/// | Status | Code | Description |
427/// |--------|------|-------------|
428/// | 400 | `EMPTY_HTML` | HTML content is empty or whitespace |
429/// | 502 | `PDF_GENERATION_FAILED` | Failed to generate PDF |
430/// | 503 | `BROWSER_UNAVAILABLE` | No browsers available |
431/// | 504 | `TIMEOUT` | Operation timed out |
432///
433/// # Example Request
434///
435/// ```bash
436/// curl -X POST http://localhost:8080/pdf/html \
437///   -H "Content-Type: application/json" \
438///   -d '{"html": "<h1>Hello</h1>", "filename": "hello.pdf"}' \
439///   --output hello.pdf
440/// ```
441///
442/// # Usage in App
443///
444/// ```rust,ignore
445/// App::new()
446///     .app_data(web::Data::new(pool.clone()))
447///     .route("/pdf/html", web::post().to(pdf_from_html))
448/// ```
449pub async fn pdf_from_html(
450    pool: web::Data<SharedPool>,
451    body: web::Json<PdfFromHtmlRequest>,
452) -> impl Responder {
453    let request = body.into_inner();
454    let pool = pool.into_inner();
455
456    log::debug!("PDF from HTML request: {} bytes", request.html.len());
457
458    let result = tokio::time::timeout(
459        Duration::from_secs(DEFAULT_TIMEOUT_SECS),
460        web::block(move || service::generate_pdf_from_html(&pool, &request)),
461    )
462    .await;
463
464    match result {
465        Ok(Ok(Ok(response))) => build_pdf_response(response),
466        Ok(Ok(Err(e))) => build_error_response(e),
467        Ok(Err(blocking_err)) => {
468            log::error!("Blocking task error: {}", blocking_err);
469            build_error_response(PdfServiceError::Internal(blocking_err.to_string()))
470        }
471        Err(_timeout) => {
472            log::error!("PDF generation timed out");
473            build_error_response(PdfServiceError::Timeout(format!(
474                "Operation timed out after {} seconds",
475                DEFAULT_TIMEOUT_SECS
476            )))
477        }
478    }
479}
480
481/// Get browser pool statistics.
482///
483/// Returns real-time metrics about the browser pool including available
484/// browsers, active browsers, and total count.
485///
486/// # Endpoint
487///
488/// ```text
489/// GET /pool/stats
490/// ```
491///
492/// # Response (200 OK)
493///
494/// ```json
495/// {
496///     "available": 3,
497///     "active": 2,
498///     "total": 5
499/// }
500/// ```
501///
502/// | Field | Type | Description |
503/// |-------|------|-------------|
504/// | `available` | number | Browsers ready to handle requests |
505/// | `active` | number | Browsers currently in use |
506/// | `total` | number | Total browsers (available + active) |
507///
508/// # Errors
509///
510/// | Status | Code | Description |
511/// |--------|------|-------------|
512/// | 500 | `POOL_LOCK_FAILED` | Failed to acquire pool lock |
513///
514/// # Use Cases
515///
516/// - Monitoring dashboards
517/// - Prometheus/Grafana metrics
518/// - Capacity planning
519/// - Debugging pool exhaustion
520///
521/// # Usage in App
522///
523/// ```rust,ignore
524/// App::new()
525///     .app_data(web::Data::new(pool.clone()))
526///     .route("/pool/stats", web::get().to(pool_stats))
527/// ```
528pub async fn pool_stats(pool: web::Data<SharedPool>) -> impl Responder {
529    match service::get_pool_stats(&pool) {
530        Ok(stats) => HttpResponse::Ok().json(stats),
531        Err(e) => build_error_response(e),
532    }
533}
534
535/// Health check endpoint.
536///
537/// Simple endpoint that returns 200 OK if the service is running.
538/// Does not check pool health - use [`readiness_check`] for that.
539///
540/// # Endpoint
541///
542/// ```text
543/// GET /health
544/// ```
545///
546/// # Response (200 OK)
547///
548/// ```json
549/// {
550///     "status": "healthy",
551///     "service": "html2pdf-api"
552/// }
553/// ```
554///
555/// # Use Cases
556///
557/// - Kubernetes liveness probe
558/// - Load balancer health check
559/// - Uptime monitoring
560///
561/// # Kubernetes Example
562///
563/// ```yaml
564/// livenessProbe:
565///   httpGet:
566///     path: /health
567///     port: 8080
568///   initialDelaySeconds: 10
569///   periodSeconds: 30
570/// ```
571///
572/// # Usage in App
573///
574/// ```rust,ignore
575/// App::new()
576///     .route("/health", web::get().to(health_check))
577/// ```
578pub async fn health_check() -> impl Responder {
579    HttpResponse::Ok().json(HealthResponse::default())
580}
581
582/// Readiness check endpoint.
583///
584/// Returns 200 OK if the pool has capacity to handle requests,
585/// 503 Service Unavailable otherwise.
586///
587/// Unlike [`health_check`], this actually checks the pool state.
588///
589/// # Endpoint
590///
591/// ```text
592/// GET /ready
593/// ```
594///
595/// # Response
596///
597/// ## Ready (200 OK)
598///
599/// ```json
600/// {
601///     "status": "ready"
602/// }
603/// ```
604///
605/// ## Not Ready (503 Service Unavailable)
606///
607/// ```json
608/// {
609///     "status": "not_ready",
610///     "reason": "no_available_capacity"
611/// }
612/// ```
613///
614/// # Readiness Criteria
615///
616/// The service is "ready" if either:
617/// - There are idle browsers available (`available > 0`), OR
618/// - There is capacity to create new browsers (`active < max_pool_size`)
619///
620/// # Use Cases
621///
622/// - Kubernetes readiness probe
623/// - Load balancer health check (remove from rotation when busy)
624/// - Auto-scaling triggers
625///
626/// # Kubernetes Example
627///
628/// ```yaml
629/// readinessProbe:
630///   httpGet:
631///     path: /ready
632///     port: 8080
633///   initialDelaySeconds: 5
634///   periodSeconds: 10
635/// ```
636///
637/// # Usage in App
638///
639/// ```rust,ignore
640/// App::new()
641///     .app_data(web::Data::new(pool.clone()))
642///     .route("/ready", web::get().to(readiness_check))
643/// ```
644pub async fn readiness_check(pool: web::Data<SharedPool>) -> impl Responder {
645    match service::is_pool_ready(&pool) {
646        Ok(true) => HttpResponse::Ok().json(serde_json::json!({
647            "status": "ready"
648        })),
649        Ok(false) => HttpResponse::ServiceUnavailable().json(serde_json::json!({
650            "status": "not_ready",
651            "reason": "no_available_capacity"
652        })),
653        Err(e) => HttpResponse::ServiceUnavailable().json(ErrorResponse::from(e)),
654    }
655}
656
657// ============================================================================
658// Route Configuration
659// ============================================================================
660
661/// Configure all PDF routes.
662///
663/// Adds all pre-built handlers to the Actix-web app with default paths.
664/// This is the easiest way to set up the PDF service.
665///
666/// # Routes Added
667///
668/// | Method | Path | Handler | Description |
669/// |--------|------|---------|-------------|
670/// | GET | `/pdf` | [`pdf_from_url`] | Convert URL to PDF |
671/// | POST | `/pdf/html` | [`pdf_from_html`] | Convert HTML to PDF |
672/// | GET | `/pool/stats` | [`pool_stats`] | Pool statistics |
673/// | GET | `/health` | [`health_check`] | Health check |
674/// | GET | `/ready` | [`readiness_check`] | Readiness check |
675///
676/// # Example
677///
678/// ```rust,ignore
679/// use actix_web::{App, HttpServer, web};
680/// use html2pdf_api::prelude::*;
681/// use html2pdf_api::integrations::actix::configure_routes;
682///
683/// #[actix_web::main]
684/// async fn main() -> std::io::Result<()> {
685///     let pool = init_browser_pool().await?;
686///
687///     HttpServer::new(move || {
688///         App::new()
689///             .app_data(web::Data::new(pool.clone()))
690///             .configure(configure_routes)
691///     })
692///     .bind("127.0.0.1:8080")?
693///     .run()
694///     .await
695/// }
696/// ```
697///
698/// # Adding Custom Routes
699///
700/// You can combine `configure_routes` with additional routes:
701///
702/// ```rust,ignore
703/// App::new()
704///     .app_data(web::Data::new(pool.clone()))
705///     .configure(configure_routes)  // Pre-built routes
706///     .route("/custom", web::get().to(my_custom_handler))  // Your routes
707/// ```
708///
709/// # Custom Path Prefix
710///
711/// To mount routes under a prefix, use `web::scope`:
712///
713/// ```rust,ignore
714/// App::new()
715///     .app_data(web::Data::new(pool.clone()))
716///     .service(
717///         web::scope("/api/v1")
718///             .configure(configure_routes)
719///     )
720/// // Routes will be: /api/v1/pdf, /api/v1/health, etc.
721/// ```
722pub fn configure_routes(cfg: &mut web::ServiceConfig) {
723    cfg.route("/pdf", web::get().to(pdf_from_url))
724        .route("/pdf/html", web::post().to(pdf_from_html))
725        .route("/pool/stats", web::get().to(pool_stats))
726        .route("/health", web::get().to(health_check))
727        .route("/ready", web::get().to(readiness_check));
728}
729
730// ============================================================================
731// Response Builders (Internal)
732// ============================================================================
733
734/// Build HTTP response for successful PDF generation.
735fn build_pdf_response(response: crate::service::PdfResponse) -> HttpResponse {
736    log::info!(
737        "PDF generated successfully: {} bytes, filename={}",
738        response.size(),
739        response.filename
740    );
741
742    HttpResponse::Ok()
743        .content_type("application/pdf")
744        .insert_header((header::CACHE_CONTROL, "no-cache"))
745        .insert_header((header::CONTENT_DISPOSITION, response.content_disposition()))
746        .body(response.data)
747}
748
749/// Build HTTP response for errors.
750fn build_error_response(error: PdfServiceError) -> HttpResponse {
751    let status_code = error.status_code();
752    let body = ErrorResponse::from(&error);
753
754    log::warn!("PDF generation error: {} (HTTP {})", error, status_code);
755
756    match status_code {
757        400 => HttpResponse::BadRequest().json(body),
758        502 => HttpResponse::BadGateway().json(body),
759        503 => HttpResponse::ServiceUnavailable().json(body),
760        504 => HttpResponse::GatewayTimeout().json(body),
761        _ => HttpResponse::InternalServerError().json(body),
762    }
763}
764
765// ============================================================================
766// Extension Trait (Backward Compatibility)
767// ============================================================================
768
769/// Extension trait for `BrowserPool` with Actix-web helpers.
770///
771/// Provides convenient methods for integrating with Actix-web.
772///
773/// # Example
774///
775/// ```rust,ignore
776/// use html2pdf_api::integrations::actix::BrowserPoolActixExt;
777///
778/// let pool = BrowserPool::builder()
779///     .factory(Box::new(ChromeBrowserFactory::with_defaults()))
780///     .build()?;
781///
782/// pool.warmup().await?;
783///
784/// // Convert directly to Actix-web Data
785/// let pool_data = pool.into_actix_data();
786///
787/// HttpServer::new(move || {
788///     App::new()
789///         .app_data(pool_data.clone())
790///         .configure(configure_routes)
791/// })
792/// ```
793pub trait BrowserPoolActixExt {
794    /// Convert the pool into Actix-web `Data` wrapper.
795    ///
796    /// This is equivalent to calling `into_shared()` and then wrapping
797    /// with `web::Data::new()`.
798    ///
799    /// # Example
800    ///
801    /// ```rust,ignore
802    /// use html2pdf_api::integrations::actix::BrowserPoolActixExt;
803    ///
804    /// let pool = BrowserPool::builder()
805    ///     .factory(Box::new(ChromeBrowserFactory::with_defaults()))
806    ///     .build()?;
807    ///
808    /// let pool_data = pool.into_actix_data();
809    ///
810    /// HttpServer::new(move || {
811    ///     App::new()
812    ///         .app_data(pool_data.clone())
813    /// })
814    /// ```
815    fn into_actix_data(self) -> BrowserPoolData;
816}
817
818impl BrowserPoolActixExt for BrowserPool {
819    fn into_actix_data(self) -> BrowserPoolData {
820        web::Data::new(self.into_shared())
821    }
822}
823
824// ============================================================================
825// Helper Functions (Backward Compatibility)
826// ============================================================================
827
828/// Create Actix-web `Data` from an existing shared pool.
829///
830/// Use this when you already have a `SharedBrowserPool` and want to
831/// wrap it for Actix-web.
832///
833/// # Parameters
834///
835/// * `pool` - The shared browser pool.
836///
837/// # Returns
838///
839/// `BrowserPoolData` ready for use with `App::app_data()`.
840///
841/// # Example
842///
843/// ```rust,ignore
844/// use html2pdf_api::integrations::actix::create_pool_data;
845///
846/// let shared_pool = pool.into_shared();
847/// let pool_data = create_pool_data(shared_pool);
848///
849/// App::new().app_data(pool_data)
850/// ```
851pub fn create_pool_data(pool: SharedBrowserPool) -> BrowserPoolData {
852    web::Data::new(pool)
853}
854
855/// Create Actix-web `Data` from an `Arc` reference.
856///
857/// Use this when you need to keep a reference to the pool for shutdown.
858///
859/// # Parameters
860///
861/// * `pool` - Arc reference to the shared browser pool.
862///
863/// # Returns
864///
865/// `BrowserPoolData` ready for use with `App::app_data()`.
866///
867/// # Example
868///
869/// ```rust,ignore
870/// use html2pdf_api::integrations::actix::create_pool_data_from_arc;
871///
872/// let shared_pool = pool.into_shared();
873/// let pool_for_shutdown = Arc::clone(&shared_pool);
874/// let pool_data = create_pool_data_from_arc(shared_pool);
875///
876/// // Use pool_data in App
877/// // Use pool_for_shutdown for cleanup
878/// ```
879pub fn create_pool_data_from_arc(pool: Arc<std::sync::Mutex<BrowserPool>>) -> BrowserPoolData {
880    web::Data::new(pool)
881}
882
883// ============================================================================
884// Tests
885// ============================================================================
886
887#[cfg(test)]
888mod tests {
889    use super::*;
890
891    #[test]
892    fn test_type_alias_compiles() {
893        // Verify the type alias is valid
894        fn _accepts_pool_data(_: BrowserPoolData) {}
895        fn _accepts_shared_pool(_: SharedPool) {}
896    }
897
898    #[tokio::test]
899    async fn test_shared_pool_type_matches() {
900        // SharedPool and SharedBrowserPool should be compatible
901        fn _takes_shared_pool(_: SharedPool) {}
902        fn _returns_shared_browser_pool() -> SharedBrowserPool {
903            Arc::new(std::sync::Mutex::new(
904                BrowserPool::builder()
905                    .factory(Box::new(crate::factory::mock::MockBrowserFactory::new()))
906                    .build()
907                    .unwrap(),
908            ))
909        }
910
911        // This should compile, proving type compatibility
912        let pool: SharedBrowserPool = _returns_shared_browser_pool();
913        let _: SharedPool = pool;
914    }
915}