Skip to main content

go_http/
server.rs

1// SPDX-License-Identifier: Apache-2.0
2
3/// Server — port of Go's net/http Server.
4///
5/// Goroutine-per-connection model backed by go-lib.
6///
7/// ## I/O model
8///
9/// go-lib's `TcpListener::accept()` integrates with the kqueue/epoll/IOCP
10/// netpoll and parks goroutines without blocking OS threads.
11///
12/// `TcpStream` implements `std::io::Read` and `std::io::Write` directly
13/// (go-lib ≥ 0.5.1), so `serve_conn` uses `stream.try_clone()` to split the
14/// connection into independent read and write halves — no unsafe fd
15/// manipulation required.
16use std::io::{self, Read, Write};
17use std::sync::{Arc, Mutex};
18use std::time::Duration;
19
20use go_lib::chan::{chan, Sender};
21use go_lib::net::{TcpListener, TcpStream};
22use go_lib::sync::WaitGroup;
23use rustls::ServerConnection;
24use url::Url;
25
26use crate::error::HttpError;
27use crate::handler::{default_serve_mux, Handler};
28use crate::header::Header;
29use crate::parse::request::{read_request, DEFAULT_MAX_HEADER_BYTES};
30use crate::parse::transfer::Body;
31use crate::status;
32use crate::request::Request;
33use crate::response::{ConnResponseWriter, ResponseWriter};
34
35// ---------------------------------------------------------------------------
36// Server struct
37// ---------------------------------------------------------------------------
38
39/// An HTTP/1.1 server.  Mirrors Go's `http.Server`.
40pub struct Server {
41    /// TCP address to listen on, e.g. `"127.0.0.1:8080"`.
42    pub addr: String,
43    /// Request handler.  `None` uses the global `DefaultServeMux`.
44    pub handler: Option<Arc<dyn Handler>>,
45    pub read_timeout:  Option<Duration>,
46    pub write_timeout: Option<Duration>,
47    pub idle_timeout:  Option<Duration>,
48    /// Maximum bytes consumed while reading request headers.
49    pub max_header_bytes: usize,
50    /// Maximum request body size in bytes.  `None` means unlimited.
51    /// Requests with a known Content-Length exceeding this limit are rejected
52    /// with 413 before the handler runs.  Chunked bodies are wrapped in a
53    /// capped reader that returns an error when the limit is exceeded.
54    pub max_body_bytes: Option<u64>,
55    /// Populated by `listen_and_serve`; send `()` to request shutdown.
56    shutdown_tx: Mutex<Option<Sender<()>>>,
57    /// Tracks the number of active connections.  `shutdown()` waits on this
58    /// so it returns only after all in-flight requests finish.
59    active_conns: Arc<WaitGroup>,
60}
61
62impl Server {
63    pub fn new(addr: impl Into<String>) -> Self {
64        Self {
65            addr:             addr.into(),
66            handler:          None,
67            read_timeout:     None,
68            write_timeout:    None,
69            idle_timeout:     None,
70            max_header_bytes: DEFAULT_MAX_HEADER_BYTES,
71            max_body_bytes:   None,
72            shutdown_tx:      Mutex::new(None),
73            active_conns:     Arc::new(WaitGroup::new()),
74        }
75    }
76
77    /// Bind, listen, and serve HTTP/1.1 requests.
78    ///
79    /// **Must be called from within a `#[go_lib::main]` body (goroutine context).**
80    ///
81    /// Blocks the calling goroutine until `shutdown()` is called or a fatal
82    /// listener error occurs.  Port of Go's `(*Server).ListenAndServe`.
83    pub fn listen_and_serve(&self) -> Result<(), HttpError> {
84        let listener = TcpListener::bind(&self.addr as &str).map_err(HttpError::Io)?;
85
86        let handler: Arc<dyn Handler> = match &self.handler {
87            Some(h) => Arc::clone(h),
88            None    => default_serve_mux(),
89        };
90        let max_header_bytes = self.max_header_bytes;
91        let max_body_bytes   = self.max_body_bytes;
92        let idle_timeout     = self.idle_timeout;
93        let active_conns     = Arc::clone(&self.active_conns);
94
95        // Shutdown signal (buffered so shutdown() never blocks).
96        let (shutdown_tx, shutdown_rx) = chan::<()>(1);
97        *self.shutdown_tx.lock().unwrap() = Some(shutdown_tx);
98
99        // Channel that delivers accepted connections to the dispatch loop.
100        let (conn_tx, conn_rx) = chan::<TcpStream>(8);
101
102        // ── Accept goroutine ──────────────────────────────────────────────────
103        // listener.accept() uses go-lib's netpoll: parks the goroutine via
104        // gopark and resumes via goready when a connection arrives.
105        // We must NOT wrap it in with_syscall because gopark is illegal
106        // during entersyscall.
107        go_lib::go!(move || {
108            loop {
109                match listener.accept() {
110                    Err(_)       => break,
111                    Ok(stream)   => {
112                        let _ = std::panic::catch_unwind(
113                            std::panic::AssertUnwindSafe(|| conn_tx.send(stream))
114                        );
115                    }
116                }
117            }
118        });
119
120        // ── Dispatch loop ─────────────────────────────────────────────────────
121        loop {
122            go_lib::select! {
123                recv(shutdown_rx) -> _sig => { break }
124                recv(conn_rx) -> conn => {
125                    match conn {
126                        None         => break,
127                        Some(stream) => {
128                            let h  = Arc::clone(&handler);
129                            let wg = Arc::clone(&active_conns);
130                            wg.add(1);
131                            go_lib::go!(move || {
132                                serve_conn(stream, h, max_header_bytes, max_body_bytes, idle_timeout);
133                                wg.done();
134                            });
135                        }
136                    }
137                }
138            }
139        }
140
141        Ok(())
142    }
143
144    /// Bind, listen, and serve HTTPS/1.1 requests.
145    ///
146    /// Equivalent to `listen_and_serve` but wraps each accepted connection in
147    /// a TLS session using the certificate and key loaded from `cert_file` and
148    /// `key_file` (PEM format).
149    ///
150    /// **Must be called from within a `#[go_lib::main]` body (goroutine context).**
151    /// Port of Go's `(*Server).ListenAndServeTLS`.
152    pub fn listen_and_serve_tls(
153        &self,
154        cert_file: &str,
155        key_file:  &str,
156    ) -> Result<(), HttpError> {
157        let tls_config = crate::tls::server_config(cert_file, key_file)?;
158        let listener   = TcpListener::bind(&self.addr as &str).map_err(HttpError::Io)?;
159
160        let handler: Arc<dyn Handler> = match &self.handler {
161            Some(h) => Arc::clone(h),
162            None    => default_serve_mux(),
163        };
164        let max_header_bytes = self.max_header_bytes;
165        let max_body_bytes   = self.max_body_bytes;
166        let idle_timeout     = self.idle_timeout;
167        let active_conns     = Arc::clone(&self.active_conns);
168
169        let (shutdown_tx, shutdown_rx) = chan::<()>(1);
170        *self.shutdown_tx.lock().unwrap() = Some(shutdown_tx);
171
172        let (conn_tx, conn_rx) = chan::<TcpStream>(8);
173
174        go_lib::go!(move || {
175            loop {
176                match listener.accept() {
177                    Err(_)     => break,
178                    Ok(stream) => {
179                        let _ = std::panic::catch_unwind(
180                            std::panic::AssertUnwindSafe(|| conn_tx.send(stream))
181                        );
182                    }
183                }
184            }
185        });
186
187        loop {
188            go_lib::select! {
189                recv(shutdown_rx) -> _sig => { break }
190                recv(conn_rx) -> conn => {
191                    match conn {
192                        None         => break,
193                        Some(stream) => {
194                            let h   = Arc::clone(&handler);
195                            let cfg = Arc::clone(&tls_config);
196                            let wg  = Arc::clone(&active_conns);
197                            wg.add(1);
198                            go_lib::go!(move || {
199                                serve_conn_tls(stream, cfg, h, max_header_bytes, max_body_bytes, idle_timeout);
200                                wg.done();
201                            });
202                        }
203                    }
204                }
205            }
206        }
207
208        Ok(())
209    }
210
211    /// Stop accepting new connections and wait for all active connections to
212    /// finish serving their current requests.
213    ///
214    /// Returns only after the last in-flight handler has returned, so callers
215    /// can safely clean up resources after `shutdown()` completes.
216    ///
217    /// Port of Go's `(*Server).Shutdown` (simplified — no context deadline).
218    pub fn shutdown(&self) {
219        if let Some(tx) = self.shutdown_tx.lock().unwrap().take() {
220            let _ = std::panic::catch_unwind(
221                std::panic::AssertUnwindSafe(|| tx.send(()))
222            );
223        }
224        // Parks the calling goroutine until active_conns reaches zero.
225        self.active_conns.wait();
226    }
227}
228
229// ---------------------------------------------------------------------------
230// serve_conn — handle one connection through its lifetime
231// ---------------------------------------------------------------------------
232
233/// Serve HTTP/1.1 requests on a single TCP connection.
234///
235/// `TcpStream::try_clone()` (go-lib ≥ 0.5.1) duplicates the underlying fd so
236/// reading and writing can happen on independent halves without unsafe code.
237/// The write half is cloned once at the start; the read half is re-cloned for
238/// each request so `read_request` can take ownership of it (and attach it to
239/// the body reader) while the write half remains available for the response.
240fn serve_conn(
241    stream:           TcpStream,
242    handler:          Arc<dyn Handler>,
243    max_header_bytes: usize,
244    max_body_bytes:   Option<u64>,
245    idle_timeout:     Option<Duration>,
246) {
247    let remote_addr = stream.peer_addr()
248        .map(|a| a.to_string())
249        .unwrap_or_default();
250
251    // Write half: cloned once, reused across all keep-alive requests.
252    let mut write_half = match stream.try_clone() {
253        Ok(s)  => s,
254        Err(_) => return,
255    };
256
257    // Raw fd/socket used by the idle-timeout watchdog to interrupt read_request().
258    // The handle remains valid for the lifetime of `stream`; the watchdog takes
259    // a non-owning view via from_raw_fd/from_raw_socket + forget.
260    #[cfg(unix)]
261    let raw_fd = stream.as_raw_fd();
262    #[cfg(windows)]
263    let raw_socket = stream.as_raw_socket() as u64;
264
265    loop {
266        // ── Idle timeout watchdog ─────────────────────────────────────────────
267        // Spawn a goroutine that fires after `idle_timeout` and shuts down the
268        // read side of the socket, causing read_request() to return an error.
269        // Cancelled by sending on idle_cancel_tx when the read completes.
270        let (idle_cancel_tx, idle_cancel_rx) = go_lib::chan::chan::<()>(1);
271        if let Some(dur) = idle_timeout {
272            let (ctx, _cancel) = go_lib::context::with_timeout(
273                &go_lib::context::background(), dur,
274            );
275            go_lib::go!(move || {
276                go_lib::select! {
277                    recv(ctx.done())     -> _sig => {
278                        #[cfg(unix)] {
279                            use std::os::unix::io::FromRawFd;
280                            // SAFETY: raw_fd is valid while stream is alive; forget
281                            // prevents double-close since from_raw_fd takes ownership.
282                            let s = unsafe { std::net::TcpStream::from_raw_fd(raw_fd) };
283                            let _ = s.shutdown(std::net::Shutdown::Read);
284                            std::mem::forget(s);
285                        }
286                        #[cfg(windows)] {
287                            use std::os::windows::io::FromRawSocket;
288                            let s = unsafe { std::net::TcpStream::from_raw_socket(raw_socket) };
289                            let _ = s.shutdown(std::net::Shutdown::Read);
290                            std::mem::forget(s);
291                        }
292                    }
293                    recv(idle_cancel_rx) -> _sig => {}
294                }
295            });
296        }
297
298        // ── Parse the next request ────────────────────────────────────────────
299        // Clone the stream for this request's read half.  try_clone() calls
300        // dup(2)/DuplicateHandle so the clone shares the same TCP socket read
301        // position with `stream`.  The clone is consumed by read_request and
302        // stored inside the body; when the body is dropped the clone's fd is
303        // closed, but the original `stream` (and `write_half`) remain open.
304        let read_half = match stream.try_clone() {
305            Ok(s)  => s,
306            Err(_) => break,
307        };
308
309        let parsed = match read_request(read_half, max_header_bytes) {
310            Ok(p)  => { let _ = idle_cancel_tx.try_send(()); p }
311            Err(_) => break,
312        };
313
314        let connection_close = {
315            let hdr = parsed.header.get("Connection").unwrap_or("").to_ascii_lowercase();
316            hdr.contains("close") || parsed.proto_minor == 0
317        };
318
319        // ── Expect / body-size pre-checks ─────────────────────────────────────
320        // Check Expect header before inspecting body size; an unrecognised
321        // expectation is an immediate 417 regardless of body length.
322        let expect = check_expect(&parsed.header);
323        if let ExpectResult::Unknown = &expect {
324            send_error_response(&mut write_half, status::EXPECTATION_FAILED);
325            break;
326        }
327
328        // Reject oversized Content-Length bodies before dispatching.
329        if let Some(max) = max_body_bytes
330            && parsed.content_length > 0 && parsed.content_length as u64 > max
331        {
332            send_error_response(&mut write_half, status::REQUEST_ENTITY_TOO_LARGE);
333            break;
334        }
335
336        // Send provisional 100 Continue.  write_half is an independent fd clone
337        // — safe to write while the body wrapper (on the stream clone) exists.
338        if let ExpectResult::Continue = expect {
339            let _ = write_half.write_all(b"HTTP/1.1 100 Continue\r\n\r\n");
340            let _ = write_half.flush();
341        }
342
343        // ── Build Request ─────────────────────────────────────────────────────
344        let mut req = match build_request(parsed, remote_addr.clone()) {
345            Ok(r)  => r,
346            Err(_) => break,
347        };
348
349        // Wrap chunked / unbounded bodies with a hard byte cap so oversized
350        // payloads surface as an IO error to the handler rather than silently
351        // consuming unbounded memory.
352        if let Some(max) = max_body_bytes
353            && matches!(req.body, Some(Body::Chunked(_)) | Some(Body::Unbounded(_)))
354        {
355            let old = req.body.take().unwrap();
356            req.body = Some(old.capped(max));
357        }
358
359        // ── Dispatch ──────────────────────────────────────────────────────────
360        let mut w = ConnResponseWriter::new(&mut write_half);
361        w.header().set("Server", "go-http/0.1");
362        if connection_close {
363            w.header().set("Connection", "close");
364        }
365
366        handler.serve_http(&mut w, &mut req);
367
368        if w.finish().is_err() {
369            break;
370        }
371
372        // ── Keep-alive body drain ─────────────────────────────────────────────
373        // If the handler did not consume the request body, drain it now so that
374        // leftover bytes are not misread as the next request's data.
375        if !connection_close
376            && let Some(ref mut body) = req.body
377        {
378            let mut drain = [0u8; 8192];
379            while body.read(&mut drain).map(|n| n > 0).unwrap_or(false) {}
380        }
381
382        if connection_close {
383            break;
384        }
385    }
386}
387
388// ---------------------------------------------------------------------------
389// TLS connection handler
390// ---------------------------------------------------------------------------
391
392/// A raw-pointer `Read` wrapper that "lends" an `impl Read` to `read_request`
393/// without giving up ownership.
394///
395/// # Safety
396/// The pointer must remain valid and exclusively accessible for the lifetime
397/// of the `RawTlsRead` value.  In `serve_conn_tls` this is guaranteed: the
398/// TLS stream lives for the entire connection loop; `RawTlsRead` is created
399/// just before `read_request` and dropped (with the body) before any write
400/// happens.  No concurrent access occurs because everything runs in one
401/// goroutine.
402struct RawTlsRead(*mut dyn Read);
403unsafe impl Send for RawTlsRead {}
404impl Read for RawTlsRead {
405    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
406        unsafe { (*self.0).read(buf) }
407    }
408}
409
410/// Serve HTTP/1.1 requests over a TLS-wrapped `TcpStream`.
411///
412/// Unlike `serve_conn`, TLS sessions cannot be `try_clone`'d — read and write
413/// share the same stateful TLS record layer.  We use `RawTlsRead` to hand the
414/// parser a borrow-as-`'static` pointer to the stream, which is safe because:
415/// - The body is fully consumed (or dropped) before any response bytes are written.
416/// - The stream outlives both the parser and the body within this goroutine.
417fn serve_conn_tls(
418    stream:           TcpStream,
419    tls_config:       Arc<rustls::ServerConfig>,
420    handler:          Arc<dyn Handler>,
421    max_header_bytes: usize,
422    max_body_bytes:   Option<u64>,
423    idle_timeout:     Option<Duration>,
424) {
425    let remote_addr = stream.peer_addr()
426        .map(|a| a.to_string())
427        .unwrap_or_default();
428
429    // Capture raw fd/socket before moving stream into StreamOwned.
430    #[cfg(unix)]
431    let raw_fd = stream.as_raw_fd();
432    #[cfg(windows)]
433    let raw_socket = stream.as_raw_socket() as u64;
434
435    let server_conn = match ServerConnection::new(tls_config) {
436        Ok(c)  => c,
437        Err(_) => return,
438    };
439    let mut tls = rustls::StreamOwned::new(server_conn, stream);
440
441    loop {
442        // ── Idle timeout watchdog ─────────────────────────────────────────────
443        let (idle_cancel_tx, idle_cancel_rx) = go_lib::chan::chan::<()>(1);
444        if let Some(dur) = idle_timeout {
445            let (ctx, _cancel) = go_lib::context::with_timeout(
446                &go_lib::context::background(), dur,
447            );
448            go_lib::go!(move || {
449                go_lib::select! {
450                    recv(ctx.done())     -> _sig => {
451                        #[cfg(unix)] {
452                            use std::os::unix::io::FromRawFd;
453                            let s = unsafe { std::net::TcpStream::from_raw_fd(raw_fd) };
454                            let _ = s.shutdown(std::net::Shutdown::Read);
455                            std::mem::forget(s);
456                        }
457                        #[cfg(windows)] {
458                            use std::os::windows::io::FromRawSocket;
459                            let s = unsafe { std::net::TcpStream::from_raw_socket(raw_socket) };
460                            let _ = s.shutdown(std::net::Shutdown::Read);
461                            std::mem::forget(s);
462                        }
463                    }
464                    recv(idle_cancel_rx) -> _sig => {}
465                }
466            });
467        }
468
469        // ── Parse ─────────────────────────────────────────────────────────────
470        // SAFETY: `tls` outlives `read_ptr` and there is no concurrent access.
471        // The raw pointer is stored inside the Body returned by read_request.
472        // All reads through it happen before the first response write, and all
473        // writes (100 Continue, full response) happen sequentially in this
474        // goroutine — there is never simultaneous read + write access.
475        let read_ptr: *mut dyn Read = &mut tls as &mut dyn Read as *mut dyn Read;
476        let parsed = match read_request(RawTlsRead(read_ptr), max_header_bytes) {
477            Ok(p)  => { let _ = idle_cancel_tx.try_send(()); p }
478            Err(_) => break,
479        };
480
481        let connection_close = {
482            let hdr = parsed.header.get("Connection").unwrap_or("").to_ascii_lowercase();
483            hdr.contains("close") || parsed.proto_minor == 0
484        };
485
486        // ── Expect / body-size pre-checks ─────────────────────────────────────
487        let expect = check_expect(&parsed.header);
488        if let ExpectResult::Unknown = &expect {
489            send_error_response(&mut tls, status::EXPECTATION_FAILED);
490            break;
491        }
492
493        if let Some(max) = max_body_bytes
494            && parsed.content_length > 0 && parsed.content_length as u64 > max
495        {
496            send_error_response(&mut tls, status::REQUEST_ENTITY_TOO_LARGE);
497            break;
498        }
499
500        // SAFETY: Writing 100 Continue through `&mut tls` while Body holds a
501        // RawTlsRead(*mut tls) is safe here: the body has not been read yet,
502        // the write is synchronous and completes before the handler sees the
503        // body, and the goroutine is single-threaded (no concurrent access).
504        if let ExpectResult::Continue = expect {
505            let _ = tls.write_all(b"HTTP/1.1 100 Continue\r\n\r\n");
506            let _ = tls.flush();
507        }
508
509        // ── Build Request ─────────────────────────────────────────────────────
510        // Use https:// scheme for the reconstructed URL.
511        let mut req = match build_request_scheme(parsed, remote_addr.clone(), "https") {
512            Ok(r)  => r,
513            Err(_) => break,
514        };
515
516        if let Some(max) = max_body_bytes
517            && matches!(req.body, Some(Body::Chunked(_)) | Some(Body::Unbounded(_)))
518        {
519            let old = req.body.take().unwrap();
520            req.body = Some(old.capped(max));
521        }
522
523        // ── Dispatch ──────────────────────────────────────────────────────────
524        // Response writes go through `&mut tls`.  The body (RawTlsRead) must
525        // not be read concurrently with response writes; they are sequenced:
526        // the handler reads the body first, then writes the response.
527        let mut w = ConnResponseWriter::new(&mut tls);
528        w.header().set("Server", "go-http/0.1");
529        if connection_close {
530            w.header().set("Connection", "close");
531        }
532
533        handler.serve_http(&mut w, &mut req);
534
535        if w.finish().is_err() {
536            break;
537        }
538
539        // ── Keep-alive body drain ─────────────────────────────────────────────
540        if !connection_close
541            && let Some(ref mut body) = req.body
542        {
543            let mut drain = [0u8; 8192];
544            while body.read(&mut drain).map(|n| n > 0).unwrap_or(false) {}
545        }
546
547        if connection_close {
548            break;
549        }
550    }
551}
552
553// ---------------------------------------------------------------------------
554// Build a Request from a ParsedRequest
555// ---------------------------------------------------------------------------
556
557fn build_request(
558    parsed:      crate::parse::request::ParsedRequest,
559    remote_addr: String,
560) -> Result<Request, HttpError> {
561    build_request_scheme(parsed, remote_addr, "http")
562}
563
564fn build_request_scheme(
565    parsed:      crate::parse::request::ParsedRequest,
566    remote_addr: String,
567    scheme:      &str,
568) -> Result<Request, HttpError> {
569    let host = if parsed.host.is_empty() { "localhost".to_owned() } else { parsed.host.clone() };
570
571    let url_str = if parsed.request_uri.starts_with("http://")
572        || parsed.request_uri.starts_with("https://")
573    {
574        parsed.request_uri.clone()
575    } else {
576        format!("{scheme}://{host}{}", parsed.request_uri)
577    };
578
579    let url  = Url::parse(&url_str).map_err(|e| HttpError::InvalidUrl(e.to_string()))?;
580    let ctx  = go_lib::context::background();
581    let body = match parsed.body {
582        Body::Empty => None,
583        other       => Some(other),
584    };
585
586    let mut req = Request::new_with_context(&parsed.method, url.as_str(), body, ctx)?;
587    req.proto             = parsed.proto;
588    req.proto_major       = parsed.proto_major;
589    req.proto_minor       = parsed.proto_minor;
590    req.header            = parsed.header;
591    req.host              = parsed.host;
592    req.content_length    = parsed.content_length;
593    req.transfer_encoding = parsed.transfer_encoding;
594    req.remote_addr       = remote_addr;
595    Ok(req)
596}
597
598// ---------------------------------------------------------------------------
599// Free functions
600// ---------------------------------------------------------------------------
601
602/// Bind to `addr`, use `handler` (or `DefaultServeMux` if `None`), and serve.
603///
604/// Must be called from within a `#[go_lib::main]` body (goroutine context).
605/// Port of Go's `http.ListenAndServe`.
606pub fn listen_and_serve(
607    addr:    &str,
608    handler: Option<Arc<dyn Handler>>,
609) -> Result<(), HttpError> {
610    let mut srv = Server::new(addr);
611    srv.handler = handler;
612    srv.listen_and_serve()
613}
614
615/// Bind to `addr`, load TLS credentials, and serve HTTPS.
616///
617/// Must be called from within a `#[go_lib::main]` body (goroutine context).
618/// Port of Go's `http.ListenAndServeTLS`.
619pub fn listen_and_serve_tls(
620    addr:      &str,
621    cert_file: &str,
622    key_file:  &str,
623    handler:   Option<Arc<dyn Handler>>,
624) -> Result<(), HttpError> {
625    let mut srv = Server::new(addr);
626    srv.handler = handler;
627    srv.listen_and_serve_tls(cert_file, key_file)
628}
629
630pub use crate::handler::handle;
631pub use crate::handler::handle_func;
632
633// ---------------------------------------------------------------------------
634// Expect header helpers
635// ---------------------------------------------------------------------------
636
637enum ExpectResult {
638    None,
639    Continue,
640    Unknown,
641}
642
643fn check_expect(header: &Header) -> ExpectResult {
644    match header.get("Expect") {
645        None    => ExpectResult::None,
646        Some(v) if v.eq_ignore_ascii_case("100-continue") => ExpectResult::Continue,
647        Some(_) => ExpectResult::Unknown,
648    }
649}
650
651/// Write a minimal error response and flush.
652fn send_error_response<W: Write>(w: &mut W, code: u16) {
653    let text = crate::status::status_text(code);
654    let body = format!("{code} {text}\n");
655    let _ = write!(
656        w,
657        "HTTP/1.1 {code} {text}\r\nContent-Type: text/plain; charset=utf-8\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
658        body.len(),
659        body
660    );
661    let _ = w.flush();
662}
663
664// ---------------------------------------------------------------------------
665// Tests
666// ---------------------------------------------------------------------------
667
668#[cfg(test)]
669mod tests {
670    use super::*;
671
672    #[test]
673    fn build_request_basic() {
674        use crate::parse::request::ParsedRequest;
675        use crate::parse::transfer::Body;
676
677        let mut hdr = crate::header::Header::new();
678        hdr.set("Host", "example.com");
679
680        let pr = ParsedRequest {
681            method:            "GET".into(),
682            request_uri:       "/hello?q=1".into(),
683            proto:             "HTTP/1.1".into(),
684            proto_major:       1,
685            proto_minor:       1,
686            header:            hdr,
687            body:              Body::Empty,
688            content_length:    -1,
689            transfer_encoding: vec![],
690            host:              "example.com".into(),
691        };
692
693        let req = build_request(pr, "10.0.0.1:42".into()).unwrap();
694        assert_eq!(req.method, "GET");
695        assert_eq!(req.host, "example.com");
696        assert_eq!(req.url.path(), "/hello");
697        assert_eq!(req.url.query(), Some("q=1"));
698        assert_eq!(req.remote_addr, "10.0.0.1:42");
699    }
700
701    #[test]
702    fn response_writer_output() {
703        let mut buf = Vec::<u8>::new();
704        let mut w   = ConnResponseWriter::new(&mut buf);
705        w.header().set("Content-Type", "text/plain");
706        w.write_header(200);
707        w.write(b"Hello!").unwrap();
708        w.finish().unwrap();
709
710        let s = String::from_utf8(buf).unwrap();
711        assert!(s.starts_with("HTTP/1.1 200 OK\r\n"), "bad status: {s:?}");
712        assert!(s.contains("Content-Type: text/plain\r\n"));
713        assert!(s.contains("Hello!"));
714        assert!(s.contains("0\r\n\r\n"), "missing chunked terminal: {s:?}");
715    }
716
717}