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}