Skip to main content

eggserve_core/server/
service.rs

1//! Transport-independent service abstraction.
2//!
3//! A [`Service`] receives a canonical eggserve [`Request`] and produces a
4//! canonical [`Response`]. The runtime owns transport, parsing, normalization,
5//! and timeout enforcement. Services never see raw sockets or Hyper types.
6//!
7//! # Example
8//!
9//! ```no_run
10//! use eggserve_core::primitives::{Response, ResponseBody, StatusCode};
11//! use eggserve_core::server::{service_fn, Request};
12//! # fn main() {
13//!
14//! let service = service_fn(|_req| async {
15//!     Ok(Response::builder()
16//!         .status(StatusCode::OK)
17//!         .body(ResponseBody::Bytes(b"hello".to_vec()))
18//!         .unwrap())
19//! });
20//! # }
21//! ```
22
23use std::future::Future;
24use std::pin::Pin;
25
26use crate::primitives::canonical::Response;
27use crate::primitives::request::Request;
28use crate::primitives::request_body_error::RequestBodyError;
29use crate::primitives::request_body_policy::RequestBodyPolicy;
30
31/// Errors produced by a service implementation.
32///
33/// The runtime converts these into appropriate HTTP responses without leaking
34/// internal details. Services should use [`ServiceError::internal`] for
35/// unexpected failures and [`ServiceError::rejected`] for intentional rejections
36/// that should produce a specific status code.
37#[derive(Debug)]
38pub struct ServiceError {
39    kind: ServiceErrorKind,
40    message: String,
41}
42
43#[derive(Debug)]
44enum ServiceErrorKind {
45    /// An unexpected internal failure. Maps to 500.
46    Internal,
47    /// A deliberate rejection with a specific status code.
48    Rejected(u16),
49    /// The handler panicked. Maps to 500.
50    #[allow(dead_code)]
51    Panic,
52    /// The handler timed out. Maps to 504.
53    Timeout,
54}
55
56impl ServiceError {
57    /// Create an internal error (500).
58    pub fn internal(message: impl Into<String>) -> Self {
59        Self {
60            kind: ServiceErrorKind::Internal,
61            message: message.into(),
62        }
63    }
64
65    /// Create a rejection with a specific status code.
66    pub fn rejected(status: u16, message: impl Into<String>) -> Self {
67        Self {
68            kind: ServiceErrorKind::Rejected(status),
69            message: message.into(),
70        }
71    }
72
73    #[allow(dead_code)]
74    pub(crate) fn panic(message: impl Into<String>) -> Self {
75        Self {
76            kind: ServiceErrorKind::Panic,
77            message: message.into(),
78        }
79    }
80
81    pub(crate) fn timeout(message: impl Into<String>) -> Self {
82        Self {
83            kind: ServiceErrorKind::Timeout,
84            message: message.into(),
85        }
86    }
87
88    /// Returns the error message.
89    pub fn message(&self) -> &str {
90        &self.message
91    }
92
93    /// Returns `true` if this error was caused by a handler panic.
94    pub fn is_panic(&self) -> bool {
95        matches!(self.kind, ServiceErrorKind::Panic)
96    }
97
98    /// Returns `true` if this error was caused by a handler timeout.
99    pub fn is_timeout(&self) -> bool {
100        matches!(self.kind, ServiceErrorKind::Timeout)
101    }
102
103    /// Convert this error into an HTTP response.
104    ///
105    /// Internal and panic errors map to 500. Timeout errors map to 504.
106    /// Rejected errors use the provided status code. No internal details
107    /// are included in the response body.
108    pub(crate) fn to_response(&self) -> hyper::Response<crate::response::BoxBodyInner> {
109        let status = match self.kind {
110            ServiceErrorKind::Internal | ServiceErrorKind::Panic => {
111                hyper::StatusCode::INTERNAL_SERVER_ERROR
112            }
113            ServiceErrorKind::Rejected(code) => hyper::StatusCode::from_u16(code)
114                .unwrap_or(hyper::StatusCode::INTERNAL_SERVER_ERROR),
115            ServiceErrorKind::Timeout => hyper::StatusCode::GATEWAY_TIMEOUT,
116        };
117        let body = match self.kind {
118            ServiceErrorKind::Panic => "500 Internal Server Error\n",
119            ServiceErrorKind::Timeout => "504 Gateway Timeout\n",
120            ServiceErrorKind::Rejected(code) => match code {
121                400 => "400 Bad Request\n",
122                403 => "403 Forbidden\n",
123                404 => "404 Not Found\n",
124                405 => "405 Method Not Allowed\n",
125                503 => "503 Service Unavailable\n",
126                _ => "500 Internal Server Error\n",
127            },
128            ServiceErrorKind::Internal => "500 Internal Server Error\n",
129        };
130        crate::response::canonical_error(status, body, false)
131    }
132}
133
134impl std::fmt::Display for ServiceError {
135    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
136        match self.kind {
137            ServiceErrorKind::Internal => write!(f, "internal error: {}", self.message),
138            ServiceErrorKind::Rejected(code) => {
139                write!(f, "rejected ({}): {}", code, self.message)
140            }
141            ServiceErrorKind::Panic => write!(f, "handler panicked: {}", self.message),
142            ServiceErrorKind::Timeout => write!(f, "handler timeout: {}", self.message),
143        }
144    }
145}
146
147impl std::error::Error for ServiceError {}
148
149impl From<RequestBodyError> for ServiceError {
150    fn from(err: RequestBodyError) -> Self {
151        let status = err.to_status_code();
152        let message = match err {
153            RequestBodyError::RejectedByPolicy => "request body rejected by policy",
154            RequestBodyError::DeclaredLengthTooLarge { .. }
155            | RequestBodyError::LimitExceeded { .. } => "request body exceeds configured limit",
156            RequestBodyError::ReadTimeout => "request body read timed out",
157            RequestBodyError::PrematureEof { .. } => {
158                "request body ended before its declared length"
159            }
160            RequestBodyError::LengthMismatch { .. } => "request body length mismatch",
161            RequestBodyError::InvalidChunkFraming(_) => "invalid request body framing",
162            RequestBodyError::Cancelled => "request body consumption cancelled",
163            RequestBodyError::Disconnected => "client disconnected while sending request body",
164            RequestBodyError::AlreadyConsumed | RequestBodyError::MixedConsumptionMode => {
165                "request body was already consumed"
166            }
167            RequestBodyError::Transport(_) => "request body transport failure",
168        };
169        ServiceError::rejected(status, message)
170    }
171}
172
173/// A transport-independent service that handles HTTP requests.
174///
175/// Services are invoked by the runtime after request parsing and validation.
176/// They receive a canonical [`Request`] (head, body, and connection metadata)
177/// and must return a canonical [`Response`].
178///
179/// # Contract
180///
181/// - The service is called once per request.
182/// - The service must not write to raw sockets or access transport internals.
183/// - Panics are caught at the task boundary (JoinSet) and logged; the
184///   connection is dropped without a response.
185/// - The response goes through runtime normalization (hop-by-hop stripping,
186///   content-length computation) before transport.
187///
188/// # Thread safety
189///
190/// Services must be `Send + Sync` to be shared across connection tasks.
191pub trait Service: Send + Sync + 'static {
192    /// Returns the service's preferred request body policy.
193    ///
194    /// The runtime uses this to select the effective body policy before
195    /// service invocation. The runtime enforces a hard global ceiling
196    /// (`max_request_body_bytes`) that no service can exceed. Services
197    /// may only lower the ceiling, not raise it.
198    ///
199    /// The default implementation returns `Reject` (no body accepted),
200    /// which is the safe default for static file services.
201    fn request_body_policy(
202        &self,
203        _head: &crate::primitives::request_head::RequestHead,
204    ) -> RequestBodyPolicy {
205        RequestBodyPolicy::Reject
206    }
207
208    /// Handle an HTTP request.
209    ///
210    /// Returns a future that resolves to a response or a service error.
211    fn call(
212        &self,
213        request: Request,
214    ) -> Pin<Box<dyn Future<Output = Result<Response, ServiceError>> + Send + '_>>;
215}
216
217/// Create a service from a closure or async function.
218///
219/// # Example
220///
221/// ```no_run
222/// use eggserve_core::primitives::{Response, ResponseBody, StatusCode};
223/// use eggserve_core::server::{service_fn, Request};
224/// # fn main() {
225///
226/// let service = service_fn(|_req: Request| async {
227///     Ok(Response::builder()
228///         .status(StatusCode::OK)
229///         .body(ResponseBody::Bytes(b"hello".to_vec()))
230///         .unwrap())
231/// });
232/// # }
233/// ```
234pub fn service_fn<F, Fut>(f: F) -> ServiceFn<F>
235where
236    F: Fn(Request) -> Fut + Send + Sync + 'static,
237    Fut: Future<Output = Result<Response, ServiceError>> + Send + 'static,
238{
239    ServiceFn {
240        f,
241        body_policy: None,
242    }
243}
244
245/// Create a service from a closure that only receives the request head,
246/// discarding the body. The service uses `Reject` body policy.
247pub fn service_fn_head<F, Fut>(f: F) -> ServiceFn<impl Fn(Request) -> Fut + Send + Sync + 'static>
248where
249    F: Fn(crate::primitives::request_head::RequestHead) -> Fut + Send + Sync + 'static,
250    Fut: Future<Output = Result<Response, ServiceError>> + Send + 'static,
251{
252    ServiceFn {
253        f: move |req: Request| {
254            let (head, _body) = req.into_head_and_body();
255            f(head)
256        },
257        body_policy: Some(RequestBodyPolicy::Reject),
258    }
259}
260
261/// Create a service from a closure with an explicit body policy.
262pub fn service_fn_with_policy<F, Fut>(f: F, policy: RequestBodyPolicy) -> ServiceFn<F>
263where
264    F: Fn(Request) -> Fut + Send + Sync + 'static,
265    Fut: Future<Output = Result<Response, ServiceError>> + Send + 'static,
266{
267    ServiceFn {
268        f,
269        body_policy: Some(policy),
270    }
271}
272
273/// A service created from a closure via [`service_fn`].
274pub struct ServiceFn<F> {
275    f: F,
276    body_policy: Option<RequestBodyPolicy>,
277}
278
279impl<F, Fut> Service for ServiceFn<F>
280where
281    F: Fn(Request) -> Fut + Send + Sync + 'static,
282    Fut: Future<Output = Result<Response, ServiceError>> + Send + 'static,
283{
284    fn request_body_policy(
285        &self,
286        _head: &crate::primitives::request_head::RequestHead,
287    ) -> RequestBodyPolicy {
288        self.body_policy.unwrap_or(RequestBodyPolicy::Reject)
289    }
290
291    fn call(
292        &self,
293        request: Request,
294    ) -> Pin<Box<dyn Future<Output = Result<Response, ServiceError>> + Send + '_>> {
295        Box::pin((self.f)(request))
296    }
297}
298
299#[cfg(test)]
300mod tests {
301    use super::*;
302    use crate::primitives::canonical::ResponseBody as CanonicalResponseBody;
303    use crate::primitives::canonical::StatusCode;
304    use crate::primitives::connection_info::{ConnectionInfo, Scheme};
305    use crate::primitives::header_block::HeaderBlock;
306    use crate::primitives::request_body::RequestBody;
307    use std::net::SocketAddr;
308
309    fn make_test_request(path: &str) -> Request {
310        Request::new(
311            crate::primitives::request_head::RequestHead::new(
312                crate::primitives::method::Method::get(),
313                crate::primitives::request_target::RequestTarget::parse(path).unwrap(),
314                crate::primitives::version::HttpVersion::Http11,
315                HeaderBlock::new(),
316            ),
317            RequestBody::empty(),
318            ConnectionInfo {
319                local_addr: "127.0.0.1:8000".parse::<SocketAddr>().unwrap(),
320                remote_addr: "127.0.0.1:12345".parse::<SocketAddr>().unwrap(),
321                scheme: Scheme::Http,
322                tls: None,
323            },
324        )
325    }
326
327    #[tokio::test]
328    async fn service_fn_calls_handler() {
329        let svc = service_fn(|_req: Request| async {
330            Ok(Response::builder()
331                .status(StatusCode::OK)
332                .body(CanonicalResponseBody::Bytes(b"ok".to_vec()))
333                .unwrap())
334        });
335        let req = make_test_request("/test");
336        let resp = svc.call(req).await.unwrap();
337        assert_eq!(resp.status(), StatusCode::OK);
338    }
339
340    #[tokio::test]
341    async fn custom_service_returns_bytes() {
342        struct ByteService;
343        impl Service for ByteService {
344            fn call(
345                &self,
346                _req: Request,
347            ) -> Pin<Box<dyn Future<Output = Result<Response, ServiceError>> + Send + '_>>
348            {
349                Box::pin(async {
350                    Ok(Response::builder()
351                        .status(StatusCode::OK)
352                        .body(CanonicalResponseBody::Bytes(b"custom bytes".to_vec()))
353                        .unwrap())
354                })
355            }
356        }
357        let svc = ByteService;
358        let req = make_test_request("/test");
359        let resp = svc.call(req).await.unwrap();
360        assert_eq!(resp.status(), StatusCode::OK);
361    }
362
363    #[test]
364    fn service_error_response_codes() {
365        // Internal → 500
366        let err = ServiceError::internal("oops");
367        assert_eq!(
368            err.to_response().status(),
369            hyper::StatusCode::INTERNAL_SERVER_ERROR
370        );
371
372        // Panic → 500
373        let err = ServiceError::panic("crashed");
374        assert_eq!(
375            err.to_response().status(),
376            hyper::StatusCode::INTERNAL_SERVER_ERROR
377        );
378
379        // Timeout → 504
380        let err = ServiceError::timeout("slow");
381        assert_eq!(
382            err.to_response().status(),
383            hyper::StatusCode::GATEWAY_TIMEOUT
384        );
385
386        // Rejected(400) → 400
387        let err = ServiceError::rejected(400, "bad");
388        assert_eq!(err.to_response().status(), hyper::StatusCode::BAD_REQUEST);
389
390        // Rejected(403) → 403
391        let err = ServiceError::rejected(403, "no");
392        assert_eq!(err.to_response().status(), hyper::StatusCode::FORBIDDEN);
393
394        // Rejected(404) → 404
395        let err = ServiceError::rejected(404, "miss");
396        assert_eq!(err.to_response().status(), hyper::StatusCode::NOT_FOUND);
397
398        // Rejected(405) → 405
399        let err = ServiceError::rejected(405, "nope");
400        assert_eq!(
401            err.to_response().status(),
402            hyper::StatusCode::METHOD_NOT_ALLOWED
403        );
404
405        // Rejected(503) → 503
406        let err = ServiceError::rejected(503, "busy");
407        assert_eq!(
408            err.to_response().status(),
409            hyper::StatusCode::SERVICE_UNAVAILABLE
410        );
411
412        // Rejected(999) → 999 (valid status code, used as-is)
413        let err = ServiceError::rejected(999, "weird");
414        assert_eq!(
415            err.to_response().status(),
416            hyper::StatusCode::from_u16(999).unwrap()
417        );
418    }
419
420    #[tokio::test]
421    async fn service_fn_with_captured_state() {
422        let greeting = "hello";
423        let svc = service_fn(move |_req: Request| {
424            let greeting = greeting.to_string();
425            async move {
426                Ok(Response::builder()
427                    .status(StatusCode::OK)
428                    .body(CanonicalResponseBody::Bytes(greeting.into_bytes()))
429                    .unwrap())
430            }
431        });
432        let req = make_test_request("/test");
433        let resp = svc.call(req).await.unwrap();
434        assert_eq!(resp.status(), StatusCode::OK);
435    }
436
437    #[test]
438    fn service_fn_implements_service() {
439        let svc = service_fn(|_req: Request| async {
440            Ok(Response::builder()
441                .status(StatusCode::OK)
442                .body(CanonicalResponseBody::Empty)
443                .unwrap())
444        });
445        fn assert_service<S: Service>(_svc: &S) {}
446        assert_service(&svc);
447    }
448
449    #[test]
450    fn service_error_display() {
451        let err = ServiceError::internal("something broke");
452        assert!(err.to_string().contains("something broke"));
453        assert!(!err.is_panic());
454        assert!(!err.is_timeout());
455
456        let err = ServiceError::rejected(404, "not found");
457        assert!(err.to_string().contains("404"));
458
459        let err = ServiceError::panic("oops");
460        assert!(err.is_panic());
461
462        let err = ServiceError::timeout("too slow");
463        assert!(err.is_timeout());
464    }
465
466    #[test]
467    fn service_error_to_response() {
468        let err = ServiceError::panic("oops");
469        let resp = err.to_response();
470        assert_eq!(resp.status(), hyper::StatusCode::INTERNAL_SERVER_ERROR);
471
472        let err = ServiceError::timeout("slow");
473        let resp = err.to_response();
474        assert_eq!(resp.status(), hyper::StatusCode::GATEWAY_TIMEOUT);
475
476        let err = ServiceError::rejected(404, "nope");
477        let resp = err.to_response();
478        assert_eq!(resp.status(), hyper::StatusCode::NOT_FOUND);
479    }
480}