1use std::collections::BTreeMap;
89use std::io::{BufRead, BufReader, ErrorKind, Read, Write};
90use std::net::{TcpListener, TcpStream, ToSocketAddrs};
91use std::rc::Rc;
92use std::sync::atomic::{AtomicU64, Ordering};
93use std::sync::{Arc, Mutex};
94use std::time::{Duration, Instant};
95
96use cove_schema::builtins::RESULT;
97
98use crate::error::RuntimeError;
99use crate::host::{HostApi, NoReentry, Reentry, ResourceHandle};
100use crate::schema::ModuleSchema;
101use crate::value::{EnumValue, Repr, StructValue, Value};
102
103const READ_TIMEOUT: Duration = Duration::from_secs(30);
111
112const POLL_INTERVAL: Duration = Duration::from_millis(2);
124
125const MAX_REQUEST_LINE: usize = 8 * 1024;
134
135const MAX_HEADER_BYTES: usize = 8 * 1024;
141
142const MAX_HEADERS_BYTES: usize = 32 * 1024;
150
151const MAX_HEADER_COUNT: usize = 100;
157
158const MAX_BODY_BYTES: usize = 1024 * 1024;
167
168const MAX_RESPONSE_BYTES: usize = 1024 * 1024;
187
188const SCHEMA: ModuleSchema = cove_schema::hosts::HTTP;
194
195pub struct Http {
197 source: HttpSource,
198 open: Mutex<BTreeMap<u64, Listener>>,
205 next_id: AtomicU64,
208}
209
210enum HttpSource {
211 Real,
213 Recorded {
216 answers: BTreeMap<String, RecordedResponse>,
217 requests: Vec<ScriptedRequest>,
218 served: Arc<Mutex<Vec<String>>>,
221 },
222 Denied,
224}
225
226#[derive(Clone, Debug)]
233pub struct RecordedResponse {
234 pub status: i64,
236 pub body: String,
238}
239
240impl RecordedResponse {
241 pub fn ok(body: &str) -> RecordedResponse {
243 RecordedResponse::new(200, body)
244 }
245
246 pub fn new(status: i64, body: &str) -> RecordedResponse {
248 RecordedResponse {
249 status,
250 body: body.to_string(),
251 }
252 }
253}
254
255#[derive(Clone, Debug)]
257pub struct ScriptedRequest {
258 pub method: String,
260 pub path: String,
262 pub body: String,
264}
265
266impl ScriptedRequest {
267 pub fn get(path: &str) -> ScriptedRequest {
269 ScriptedRequest {
270 method: "Get".to_string(),
271 path: path.to_string(),
272 body: String::new(),
273 }
274 }
275
276 pub fn post(path: &str, body: &str) -> ScriptedRequest {
278 ScriptedRequest {
279 method: "Post".to_string(),
280 path: path.to_string(),
281 body: body.to_string(),
282 }
283 }
284}
285
286enum Listener {
288 Real(TcpListener),
290 Scripted {
292 port: i64,
293 requests: Vec<ScriptedRequest>,
294 },
295}
296
297impl Http {
298 pub fn real() -> Self {
304 Http::with_source(HttpSource::Real)
305 }
306
307 pub fn recorded(
314 answers: BTreeMap<String, RecordedResponse>,
315 requests: Vec<ScriptedRequest>,
316 ) -> Self {
317 Http::with_source(HttpSource::Recorded {
318 answers,
319 requests,
320 served: Arc::new(Mutex::new(Vec::new())),
321 })
322 }
323
324 pub fn served(&self) -> Served {
329 match &self.source {
330 HttpSource::Recorded { served, .. } => Served(Arc::clone(served)),
331 _ => Served(Arc::new(Mutex::new(Vec::new()))),
332 }
333 }
334
335 pub fn denied() -> Self {
337 Http::with_source(HttpSource::Denied)
338 }
339
340 fn with_source(source: HttpSource) -> Self {
341 Http {
342 source,
343 open: Mutex::new(BTreeMap::new()),
344 next_id: AtomicU64::new(1),
345 }
346 }
347
348 fn listen(&self, port: i64) -> Result<Value, RuntimeError> {
350 if !(0..=65535).contains(&port) {
351 return Ok(Value::err(Value::error(format!(
352 "http: {port} is not a port number"
353 ))));
354 }
355 let listener = match &self.source {
356 HttpSource::Real => match TcpListener::bind(("127.0.0.1", port as u16)) {
357 Ok(listener) => Listener::Real(listener),
358 Err(e) => {
359 return Ok(Value::err(Value::error(format!(
360 "http: cannot listen on 127.0.0.1:{port}: {e}"
361 ))))
362 }
363 },
364 HttpSource::Recorded { requests, .. } => Listener::Scripted {
365 port,
366 requests: requests.clone(),
367 },
368 HttpSource::Denied => {
369 return Ok(Value::err(Value::error(
370 "http: this host has no network, so nothing can listen",
371 )))
372 }
373 };
374 let id = self.next_id.fetch_add(1, Ordering::Relaxed);
375 self.locked().insert(id, listener);
376 Ok(Value::ok(Value(Repr::Resource(ResourceHandle::new(
377 "http",
378 &SCHEMA.resources[0],
379 id,
380 )))))
381 }
382
383 fn locked(&self) -> std::sync::MutexGuard<'_, BTreeMap<u64, Listener>> {
384 self.open
385 .lock()
386 .unwrap_or_else(|poisoned| poisoned.into_inner())
387 }
388
389 fn fetch(&self, url: &str, back: &dyn Reentry) -> Value {
406 match &self.source {
407 HttpSource::Real => match fetch_over_tcp(url, back) {
408 Ok((status, body)) => Value::ok(response(status, &body)),
409 Err(message) => Value::err(Value::error(message)),
410 },
411 HttpSource::Recorded { answers, .. } => match answers.get(url) {
412 Some(answer) => Value::ok(response(answer.status, &answer.body)),
413 None => Value::err(Value::error(format!(
414 "http: no recorded answer for `{url}`"
415 ))),
416 },
417 HttpSource::Denied => Value::err(Value::error(
418 "http: this host has no network, so no request can be sent",
419 )),
420 }
421 }
422
423 fn serve_one(
440 &self,
441 handle: &ResourceHandle,
442 routes: &Value,
443 back: &mut dyn Reentry,
444 ) -> Result<Value, RuntimeError> {
445 let Value(Repr::Array(routes)) = routes else {
446 return Err(RuntimeError::new(format!(
447 "`http.Server.handle` takes an `Array<http.Route>`, but found `{}`",
448 routes.type_name()
449 )));
450 };
451 let next = {
455 let mut open = self.locked();
456 match open.get_mut(&handle.id) {
457 None => return Err(stale(handle, "handle")),
458 Some(Listener::Scripted { requests, .. }) => {
459 if requests.is_empty() {
460 return Ok(Value::ok(Value(Repr::Bool(false))));
461 }
462 Next::Scripted(requests.remove(0))
463 }
464 Some(Listener::Real(listener)) => match listener.try_clone() {
465 Ok(listener) => Next::Real(listener),
466 Err(e) => {
467 return Ok(Value::err(Value::error(format!(
468 "http: cannot accept on {handle}: {e}"
469 ))))
470 }
471 },
472 }
473 };
474
475 let (asked, connection) = match next {
476 Next::Scripted(scripted) => (scripted, None),
477 Next::Real(listener) => match accept_when_ready(&listener, back) {
478 Waited::Connected(stream) => {
479 let until = Instant::now() + bounded(READ_TIMEOUT, back.time_left());
482 match read_request(&stream, until) {
483 Ok((method, path, body)) => (
484 ScriptedRequest {
485 method: method_case(&method),
486 path,
487 body,
488 },
489 Some(stream),
490 ),
491 Err(unread) => {
495 let _ = write_response(
496 &stream,
497 unread.status,
498 &json_string(&unread.message),
499 );
500 return Ok(Value::ok(Value(Repr::Bool(true))));
501 }
502 }
503 }
504 Waited::Stopped => return Ok(Value::ok(Value(Repr::Bool(false)))),
505 Waited::Failed(e) => {
506 return Ok(Value::err(Value::error(format!(
507 "http: cannot accept on {handle}: {e}"
508 ))))
509 }
510 },
511 };
512
513 let (status, body) = match route_for(routes, &asked) {
514 Some(handler) => {
515 let answered = back.call(
516 &handler,
517 vec![request(&asked.method, &asked.path, &asked.body)],
518 )?;
519 response_of(&answered)?
520 }
521 None => (
522 404,
523 json_string(&format!("no route for {} {}", asked.method, asked.path)),
524 ),
525 };
526
527 match connection {
528 Some(stream) => {
529 if let Err(message) = write_response(&stream, status, &body) {
530 return Ok(Value::err(Value::error(message)));
531 }
532 }
533 None => self.record_served(status, &body),
534 }
535 Ok(Value::ok(Value(Repr::Bool(true))))
536 }
537
538 fn record_served(&self, status: i64, body: &str) {
540 if let HttpSource::Recorded { served, .. } = &self.source {
541 served
542 .lock()
543 .unwrap_or_else(|poisoned| poisoned.into_inner())
544 .push(format!("{status} {body}"));
545 }
546 }
547}
548
549enum Next {
551 Scripted(ScriptedRequest),
552 Real(TcpListener),
553}
554
555enum Waited {
557 Connected(TcpStream),
559 Stopped,
561 Failed(std::io::Error),
564}
565
566fn accept_when_ready(listener: &TcpListener, back: &dyn Reentry) -> Waited {
584 if let Err(e) = listener.set_nonblocking(true) {
585 return Waited::Failed(e);
586 }
587 loop {
588 match listener.accept() {
589 Ok((stream, _)) => {
590 return match stream.set_nonblocking(false) {
596 Ok(()) => Waited::Connected(stream),
597 Err(e) => Waited::Failed(e),
598 };
599 }
600 Err(e) if e.kind() == ErrorKind::WouldBlock => {
601 if stopped(back) {
606 return Waited::Stopped;
607 }
608 std::thread::sleep(POLL_INTERVAL);
609 }
610 Err(e) if e.kind() == ErrorKind::Interrupted => {}
613 Err(e) => return Waited::Failed(e),
614 }
615 }
616}
617
618fn stopped(back: &dyn Reentry) -> bool {
633 back.is_cancelled() || back.time_left().is_some_and(|left| left.is_zero())
634}
635
636fn bounded(allowance: Duration, time_left: Option<Duration>) -> Duration {
643 match time_left {
644 Some(left) => allowance.min(left),
645 None => allowance,
646 }
647}
648
649fn route_for(routes: &[Value], asked: &ScriptedRequest) -> Option<Value> {
651 routes.iter().find_map(|route| {
652 let Value(Repr::Struct(route)) = route else {
653 return None;
654 };
655 let method = match route.get("method") {
656 Some(Value(Repr::Enum(method))) => method.case.to_string(),
657 _ => return None,
658 };
659 let path = match route.get("path") {
660 Some(Value(Repr::Str(path))) => path.to_string(),
661 _ => return None,
662 };
663 (method == asked.method && path == asked.path).then(|| route.get("handler").cloned())?
664 })
665}
666
667fn response_of(value: &Value) -> Result<(i64, String), RuntimeError> {
673 match value {
674 Value(Repr::Struct(structure)) if &*structure.type_name == "http.Response" => {
675 let status = match structure.get("status") {
676 Some(Value(Repr::Int(status))) => *status,
677 _ => 200,
678 };
679 let body = match structure.get("body") {
680 Some(Value(Repr::Str(body))) => body.to_string(),
681 Some(other) => json_of(other),
682 None => String::new(),
683 };
684 Ok((status, body))
685 }
686 Value(Repr::Enum(result)) if &*result.type_name == RESULT.name => {
687 match value.ok_payload() {
688 Some(payload) => response_of(payload.first().unwrap_or(&Value(Repr::Unit))),
689 None => Ok((
690 500,
691 json_string(
692 &result
693 .payload
694 .first()
695 .map(ToString::to_string)
696 .unwrap_or_default(),
697 ),
698 )),
699 }
700 }
701 other => Err(RuntimeError::new(format!(
702 "a route handler must answer with an `http.Response`, but this one answered `{}`",
703 other.type_name()
704 ))
705 .with_help("build one with `http.json(status, value)`")),
706 }
707}
708
709fn method_case(method: &str) -> String {
711 match method.to_ascii_uppercase().as_str() {
712 "POST" => "Post".to_string(),
713 _ => "Get".to_string(),
714 }
715}
716
717#[derive(Clone)]
723pub struct Served(Arc<Mutex<Vec<String>>>);
724
725impl Served {
726 pub fn responses(&self) -> Vec<String> {
728 self.0
729 .lock()
730 .unwrap_or_else(|poisoned| poisoned.into_inner())
731 .clone()
732 }
733}
734
735fn stale(handle: &ResourceHandle, op: &str) -> RuntimeError {
737 RuntimeError::new(format!(
738 "`{handle}` is closed, so `{op}` has nothing to act on"
739 ))
740 .with_rule(
741 "A host resource handle names a resource the host owns. Closing the resource ends the handle; the name outlives it and addresses nothing.",
742 )
743 .with_help("open a new one, or move the `close` after the last use")
744}
745
746impl HostApi for Http {
747 fn module_schema(&self) -> ModuleSchema {
748 SCHEMA
749 }
750
751 fn call_with(
756 &self,
757 op: &str,
758 args: Vec<Value>,
759 back: &mut dyn Reentry,
760 ) -> Result<Value, RuntimeError> {
761 match op {
762 "fetch" => {
763 let [Value(Repr::Str(url))] = args.as_slice() else {
764 unreachable!("checked by HostRegistry::call")
765 };
766 Ok(self.fetch(url, back))
767 }
768 _ => self.call(op, args),
769 }
770 }
771
772 fn call(&self, op: &str, args: Vec<Value>) -> Result<Value, RuntimeError> {
773 match op {
774 "fetch" => {
775 let [Value(Repr::Str(url))] = args.as_slice() else {
776 unreachable!("checked by HostRegistry::call")
777 };
778 Ok(self.fetch(url, &NoReentry))
782 }
783 "json" => {
784 let [Value(Repr::Int(status)), body] = args.as_slice() else {
785 unreachable!("checked by HostRegistry::call")
786 };
787 Ok(response(*status, &json_of(body)))
788 }
789 "listen" => {
790 let [Value(Repr::Int(port))] = args.as_slice() else {
791 unreachable!("checked by HostRegistry::call")
792 };
793 self.listen(*port)
794 }
795 _ => unreachable!("checked by HostRegistry::call"),
796 }
797 }
798
799 fn call_resource(
800 &self,
801 handle: &ResourceHandle,
802 op: &str,
803 args: Vec<Value>,
804 back: &mut dyn Reentry,
805 ) -> Result<Value, RuntimeError> {
806 match op {
807 "port" => match self.locked().get(&handle.id) {
808 Some(Listener::Real(listener)) => Ok(Value(Repr::Int(
809 listener
810 .local_addr()
811 .map(|a| i64::from(a.port()))
812 .unwrap_or(0),
813 ))),
814 Some(Listener::Scripted { port, .. }) => Ok(Value(Repr::Int(*port))),
815 None => Err(stale(handle, "port")),
816 },
817 "handle" => {
818 let [routes] = args.as_slice() else {
819 unreachable!("checked by HostRegistry::call")
820 };
821 self.serve_one(handle, routes, back)
822 }
823 "close" => match self.locked().remove(&handle.id) {
824 Some(_) => Ok(Value::ok(Value(Repr::Unit))),
825 None => Err(stale(handle, "close")),
826 },
827 _ => unreachable!("checked by HostRegistry::call_resource"),
828 }
829 }
830}
831
832fn response(status: i64, body: &str) -> Value {
834 Value(Repr::Struct(Rc::new(StructValue {
835 type_name: "http.Response".into(),
836 fields: vec![
837 ("status".into(), Value(Repr::Int(status))),
838 ("body".into(), Value(Repr::Str(body.into()))),
839 ],
840 opaque: false,
841 })))
842}
843
844fn request(method: &str, path: &str, body: &str) -> Value {
846 Value(Repr::Struct(Rc::new(StructValue {
847 type_name: "http.Request".into(),
848 fields: vec![
849 ("method".into(), method_value(method)),
850 ("path".into(), Value(Repr::Str(path.into()))),
851 ("body".into(), Value(Repr::Str(body.into()))),
852 ],
853 opaque: false,
854 })))
855}
856
857fn method_value(case: &str) -> Value {
859 Value(Repr::Enum(Box::new(EnumValue {
860 type_name: "http.Method".into(),
861 case: case.into(),
862 payload: crate::value::Payload::Empty,
863 })))
864}
865
866fn json_of(value: &Value) -> String {
872 match value {
873 Value(Repr::Unit) => "null".to_string(),
874 Value(Repr::Bool(b)) => b.to_string(),
875 Value(Repr::Int(n)) => n.to_string(),
876 Value(Repr::Float(x)) if x.is_finite() => format!("{x:?}"),
877 Value(Repr::Str(s)) => json_string(s),
878 Value(Repr::Array(items)) => {
879 let items = items.iter().map(json_of).collect::<Vec<_>>().join(",");
880 format!("[{items}]")
881 }
882 Value(Repr::Vector(storage)) => {
883 let items = storage
884 .elements
885 .borrow()
886 .iter()
887 .map(json_of)
888 .collect::<Vec<_>>()
889 .join(",");
890 format!("[{items}]")
891 }
892 Value(Repr::Map(entries)) => {
893 let entries = entries
894 .iter()
895 .map(|(key, value)| format!("{}:{}", json_string(&key.to_string()), json_of(value)))
896 .collect::<Vec<_>>()
897 .join(",");
898 format!("{{{entries}}}")
899 }
900 Value(Repr::Struct(structure)) => {
901 let fields = structure
902 .fields
903 .iter()
904 .map(|(name, field)| format!("{}:{}", json_string(name), json_of(field)))
905 .collect::<Vec<_>>()
906 .join(",");
907 format!("{{{fields}}}")
908 }
909 Value(Repr::Enum(enumeration)) if enumeration.payload.is_empty() => {
912 json_string(&enumeration.case)
913 }
914 Value(Repr::Enum(enumeration)) => {
915 let payload = enumeration
916 .payload
917 .iter()
918 .map(json_of)
919 .collect::<Vec<_>>()
920 .join(",");
921 format!("{{{}:[{payload}]}}", json_string(&enumeration.case))
922 }
923 other => json_string(&other.to_string()),
926 }
927}
928
929fn json_string(s: &str) -> String {
931 let mut out = String::with_capacity(s.len() + 2);
932 out.push('"');
933 for c in s.chars() {
934 match c {
935 '"' => out.push_str("\\\""),
936 '\\' => out.push_str("\\\\"),
937 '\n' => out.push_str("\\n"),
938 '\r' => out.push_str("\\r"),
939 '\t' => out.push_str("\\t"),
940 c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
941 c => out.push(c),
942 }
943 }
944 out.push('"');
945 out
946}
947
948fn fetch_over_tcp(url: &str, back: &dyn Reentry) -> Result<(i64, String), String> {
970 let (authority, path) = split_url(url)?;
971 let allowance = bounded(READ_TIMEOUT, back.time_left());
972 if allowance.is_zero() {
973 return Err(format!(
974 "http: the run ran out of time before {authority} could be asked"
975 ));
976 }
977 if back.is_cancelled() {
978 return Err(format!(
979 "http: the run was stopped before {authority} could be asked"
980 ));
981 }
982 let until = Instant::now() + allowance;
985 let mut stream = connect_within(&authority, allowance)?;
986 let request = format!(
987 "GET {path} HTTP/1.1\r\nHost: {authority}\r\nConnection: close\r\nAccept: */*\r\n\r\n"
988 );
989 stream
990 .write_all(request.as_bytes())
991 .map_err(|e| format!("http: cannot send to {authority}: {e}"))?;
992 let answer = read_response_within(&stream, &authority, until, back)?;
993 let answer = String::from_utf8_lossy(&answer).into_owned();
994 let (head, body) = answer
995 .split_once("\r\n\r\n")
996 .ok_or_else(|| format!("http: {authority} sent no complete response"))?;
997 let status = head
998 .lines()
999 .next()
1000 .and_then(|line| line.split_whitespace().nth(1))
1001 .and_then(|code| code.parse::<i64>().ok())
1002 .ok_or_else(|| format!("http: {authority} sent no status line"))?;
1003 Ok((status, body.to_string()))
1004}
1005
1006fn connect_within(authority: &str, allowance: Duration) -> Result<TcpStream, String> {
1026 let addresses = authority
1027 .to_socket_addrs()
1028 .map_err(|e| format!("http: cannot connect to {authority}: {e}"))?;
1029 let mut refused = None;
1030 for address in addresses {
1031 match TcpStream::connect_timeout(&address, allowance) {
1032 Ok(stream) => return Ok(stream),
1033 Err(e) => refused = Some(e),
1034 }
1035 }
1036 Err(match refused {
1037 Some(e) => format!("http: cannot connect to {authority}: {e}"),
1038 None => format!("http: cannot connect to {authority}: it names no address"),
1041 })
1042}
1043
1044fn read_response_within(
1071 stream: &TcpStream,
1072 authority: &str,
1073 until: Instant,
1074 back: &dyn Reentry,
1075) -> Result<Vec<u8>, String> {
1076 stream
1077 .set_read_timeout(Some(POLL_INTERVAL))
1078 .map_err(|e| format!("http: cannot bound the read from {authority}: {e}"))?;
1079 let mut reader = stream;
1080 let mut answer = Vec::new();
1081 let mut chunk = [0u8; 8 * 1024];
1082 loop {
1083 if stopped(back) {
1087 return Err(format!(
1088 "http: the run was stopped before {authority} answered"
1089 ));
1090 }
1091 if Instant::now() >= until {
1092 return Err(format!(
1093 "http: {authority} did not answer within the time allowed for it"
1094 ));
1095 }
1096 match reader.read(&mut chunk) {
1097 Ok(0) => return Ok(answer),
1098 Ok(read) => {
1099 answer.extend_from_slice(&chunk[..read]);
1100 if answer.len() > MAX_RESPONSE_BYTES {
1101 return Err(format!(
1102 "http: {authority} sent more than the {MAX_RESPONSE_BYTES} bytes this host reads"
1103 ));
1104 }
1105 }
1106 Err(e) if matches!(e.kind(), ErrorKind::WouldBlock | ErrorKind::TimedOut) => {}
1110 Err(e) if e.kind() == ErrorKind::Interrupted => {}
1113 Err(e) => return Err(format!("http: cannot read from {authority}: {e}")),
1114 }
1115 }
1116}
1117
1118fn split_url(url: &str) -> Result<(String, String), String> {
1125 let rest = match url.split_once("://") {
1126 Some(("http", rest)) => rest,
1127 Some(("https", _)) => {
1128 return Err(format!(
1129 "http: `{url}` is https, which this host does not speak"
1130 ))
1131 }
1132 Some((scheme, _)) => {
1133 return Err(format!("http: `{url}` uses the unknown scheme `{scheme}`"))
1134 }
1135 None => return Err(format!("http: `{url}` is not an absolute URL")),
1136 };
1137 let (authority, path) = match rest.find('/') {
1138 Some(at) => (&rest[..at], &rest[at..]),
1139 None => (rest, "/"),
1140 };
1141 if authority.is_empty() {
1142 return Err(format!("http: `{url}` names no host"));
1143 }
1144 let authority = if authority.contains(':') {
1145 authority.to_string()
1146 } else {
1147 format!("{authority}:80")
1148 };
1149 Ok((authority, path.to_string()))
1150}
1151
1152fn reason(status: i64) -> &'static str {
1154 match status {
1155 200 => "OK",
1156 201 => "Created",
1157 204 => "No Content",
1158 400 => "Bad Request",
1159 404 => "Not Found",
1160 408 => "Request Timeout",
1161 413 => "Payload Too Large",
1162 414 => "URI Too Long",
1163 431 => "Request Header Fields Too Large",
1164 500 => "Internal Server Error",
1165 501 => "Not Implemented",
1166 _ => "Status",
1167 }
1168}
1169
1170struct Unread {
1172 status: i64,
1177 message: String,
1179}
1180
1181impl Unread {
1182 fn malformed(message: String) -> Unread {
1184 Unread {
1185 status: 400,
1186 message,
1187 }
1188 }
1189
1190 fn too_large(status: i64, message: String) -> Unread {
1197 Unread { status, message }
1198 }
1199
1200 fn unsupported(message: String) -> Unread {
1203 Unread {
1204 status: 501,
1205 message,
1206 }
1207 }
1208
1209 fn timed_out() -> Unread {
1215 Unread {
1216 status: 408,
1217 message: "http: this request did not arrive within the time allowed for it".to_string(),
1218 }
1219 }
1220
1221 fn from_read(what: &str, e: std::io::Error) -> Unread {
1225 match e.kind() {
1226 ErrorKind::WouldBlock | ErrorKind::TimedOut => Unread::timed_out(),
1227 _ => Unread::malformed(format!("http: cannot read {what}: {e}")),
1228 }
1229 }
1230}
1231
1232fn allow_until(stream: &TcpStream, until: Instant) -> Result<(), Unread> {
1240 let left = until.saturating_duration_since(Instant::now());
1241 if left.is_zero() {
1242 return Err(Unread::timed_out());
1243 }
1244 stream
1245 .set_read_timeout(Some(left))
1246 .map_err(|e| Unread::malformed(format!("http: cannot bound the read: {e}")))
1247}
1248
1249fn read_request(stream: &TcpStream, until: Instant) -> Result<(String, String, String), Unread> {
1260 let mut reader = BufReader::new(stream);
1261 let line = match line_within(&mut reader, stream, until, MAX_REQUEST_LINE, "the request line")? {
1262 Line::Read(line) => line,
1263 Line::Ended => String::new(),
1266 Line::TooLong => {
1267 return Err(Unread::too_large(
1268 414,
1269 format!(
1270 "http: this request line is longer than the {MAX_REQUEST_LINE} bytes this host reads"
1271 ),
1272 ))
1273 }
1274 };
1275 let mut parts = line.split_whitespace();
1276 let method = parts.next().unwrap_or_default().to_string();
1277 let target = parts.next().unwrap_or_default().to_string();
1278
1279 let mut headers = 0usize;
1280 let mut header_bytes = 0usize;
1281 let mut claimed: Option<String> = None;
1284 let mut coding: Option<String> = None;
1285 loop {
1286 let header = match line_within(&mut reader, stream, until, MAX_HEADER_BYTES, "a header")? {
1287 Line::Read(header) => header,
1288 Line::Ended => break,
1289 Line::TooLong => {
1290 return Err(Unread::too_large(
1291 431,
1292 format!(
1293 "http: this request has a header longer than the {MAX_HEADER_BYTES} bytes this host reads"
1294 ),
1295 ))
1296 }
1297 };
1298 if header.trim().is_empty() {
1299 break;
1300 }
1301 headers += 1;
1302 header_bytes += header.len();
1303 if headers > MAX_HEADER_COUNT {
1304 return Err(Unread::too_large(
1305 431,
1306 format!("http: this request has more than the {MAX_HEADER_COUNT} headers this host reads"),
1307 ));
1308 }
1309 if header_bytes > MAX_HEADERS_BYTES {
1310 return Err(Unread::too_large(
1311 431,
1312 format!(
1313 "http: this request's headers are longer than the {MAX_HEADERS_BYTES} bytes this host reads"
1314 ),
1315 ));
1316 }
1317 let Some((name, value)) = header.split_once(':') else {
1318 continue;
1319 };
1320 let (name, value) = (name.trim(), value.trim());
1321 if name.eq_ignore_ascii_case("content-length") {
1322 match &claimed {
1328 Some(first) if first != value => {
1329 return Err(Unread::malformed(format!(
1330 "http: this request gives `Content-Length` as both `{first}` and `{value}`"
1331 )))
1332 }
1333 _ => claimed = Some(value.to_string()),
1334 }
1335 } else if name.eq_ignore_ascii_case("transfer-encoding") {
1336 coding = Some(value.to_string());
1337 }
1338 }
1339 if let Some(coding) = coding {
1344 return Err(Unread::unsupported(format!(
1345 "http: this host does not speak `Transfer-Encoding: {coding}`, so it cannot tell where this body ends"
1346 )));
1347 }
1348
1349 let length = match &claimed {
1350 Some(value) => content_length(value)?,
1351 None => 0,
1352 };
1353 let body = body_within(&mut reader, stream, until, length)?;
1354 let path = target.split('?').next().unwrap_or("/").to_string();
1355 Ok((method, path, String::from_utf8_lossy(&body).into_owned()))
1356}
1357
1358enum Line {
1360 Read(String),
1362 Ended,
1365 TooLong,
1369}
1370
1371fn line_within(
1378 reader: &mut BufReader<&TcpStream>,
1379 stream: &TcpStream,
1380 until: Instant,
1381 limit: usize,
1382 what: &str,
1383) -> Result<Line, Unread> {
1384 allow_until(stream, until)?;
1385 let mut bytes = Vec::new();
1386 reader
1387 .by_ref()
1388 .take(limit as u64)
1389 .read_until(b'\n', &mut bytes)
1390 .map_err(|e| Unread::from_read(what, e))?;
1391 match bytes.last() {
1392 None => Ok(Line::Ended),
1393 Some(b'\n') => Ok(Line::Read(String::from_utf8_lossy(&bytes).into_owned())),
1394 Some(_) if bytes.len() >= limit => Ok(Line::TooLong),
1395 Some(_) => Err(Unread::malformed(format!(
1399 "http: this connection ended in the middle of {what}"
1400 ))),
1401 }
1402}
1403
1404fn content_length(value: &str) -> Result<usize, Unread> {
1412 if value.is_empty() || !value.bytes().all(|b| b.is_ascii_digit()) {
1413 return Err(Unread::malformed(format!(
1414 "http: `Content-Length: {value}` is not a count of bytes"
1415 )));
1416 }
1417 match value.parse::<usize>() {
1418 Ok(length) if length <= MAX_BODY_BYTES => Ok(length),
1419 _ => Err(Unread::too_large(
1423 413,
1424 format!(
1425 "http: this request claims {value} bytes of body, and this host reads at most {MAX_BODY_BYTES}"
1426 ),
1427 )),
1428 }
1429}
1430
1431fn body_within(
1440 reader: &mut BufReader<&TcpStream>,
1441 stream: &TcpStream,
1442 until: Instant,
1443 length: usize,
1444) -> Result<Vec<u8>, Unread> {
1445 let mut body = Vec::new();
1446 let mut chunk = [0u8; 8 * 1024];
1447 while body.len() < length {
1448 allow_until(stream, until)?;
1449 let want = chunk.len().min(length - body.len());
1450 match reader.read(&mut chunk[..want]) {
1451 Ok(0) => break,
1452 Ok(read) => body.extend_from_slice(&chunk[..read]),
1453 Err(e) if e.kind() == ErrorKind::Interrupted => {}
1456 Err(e) => return Err(Unread::from_read("the body", e)),
1457 }
1458 }
1459 if body.len() < length {
1460 return Err(Unread::malformed(format!(
1461 "http: this request claims {length} bytes of body and sent {}",
1462 body.len()
1463 )));
1464 }
1465 Ok(body)
1466}
1467
1468fn write_response(mut stream: &TcpStream, status: i64, body: &str) -> Result<(), String> {
1470 let head = format!(
1471 "HTTP/1.1 {status} {}\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
1472 reason(status),
1473 body.len()
1474 );
1475 stream
1476 .write_all(head.as_bytes())
1477 .and_then(|_| stream.write_all(body.as_bytes()))
1478 .and_then(|_| stream.flush())
1479 .map_err(|e| format!("http: cannot send the response: {e}"))
1480}
1481
1482#[cfg(test)]
1483mod tests {
1484 use super::*;
1485 use crate::budget::Cancellation;
1486 use crate::value::MapKey;
1487 use std::cell::RefCell;
1488 use std::rc::Rc;
1489 use std::sync::atomic::{AtomicBool, AtomicUsize};
1490
1491 fn err_message(value: Value) -> String {
1492 match value.err_payload() {
1493 Some(payload) => payload.first().map(ToString::to_string).unwrap_or_default(),
1494 None => panic!("expected `Err(...)`, found {value}"),
1495 }
1496 }
1497
1498 fn is_ok(value: &Value) -> bool {
1499 value.is_ok()
1500 }
1501
1502 fn ok_response(value: Value) -> (i64, String) {
1508 let Some(payload) = value.ok_payload() else {
1509 panic!("expected `Ok(...)`, found {value}");
1510 };
1511 match payload.first() {
1512 Some(Value(Repr::Struct(fields))) if &*fields.type_name == "http.Response" => {
1513 let status = match fields.get("status") {
1514 Some(Value(Repr::Int(status))) => *status,
1515 other => panic!("expected an `Int` status, found {other:?}"),
1516 };
1517 let body = match fields.get("body") {
1518 Some(Value(Repr::Str(body))) => body.to_string(),
1519 other => panic!("expected a `String` body, found {other:?}"),
1520 };
1521 (status, body)
1522 }
1523 other => panic!("expected `Ok(http.Response)`, found {other:?}"),
1524 }
1525 }
1526
1527 fn bool_ok(value: Value) -> bool {
1528 match value.ok_payload() {
1529 Some(payload) => match payload.first() {
1530 Some(Value(Repr::Bool(b))) => *b,
1531 other => panic!("expected `Ok(Bool)`, found {other:?}"),
1532 },
1533 None => panic!("expected `Ok(...)`, found {value}"),
1534 }
1535 }
1536
1537 fn response_body(value: Value) -> String {
1540 match value {
1541 Value(Repr::Struct(structure)) if &*structure.type_name == "http.Response" => {
1542 match structure.get("body") {
1543 Some(Value(Repr::Str(body))) => body.to_string(),
1544 other => panic!("expected a `String` body, found {other:?}"),
1545 }
1546 }
1547 other => panic!("expected an `http.Response`, found {other}"),
1548 }
1549 }
1550
1551 fn listen(http: &Http, port: i64) -> Arc<ResourceHandle> {
1553 let answered = http.call("listen", vec![Value(Repr::Int(port))]).unwrap();
1554 match answered.ok_payload() {
1555 Some(payload) => match payload.first() {
1556 Some(Value(Repr::Resource(handle))) => handle.clone(),
1557 other => panic!("expected `Ok(Resource)`, found {other:?}"),
1558 },
1559 None => panic!("expected `Ok(...)`, found {answered}"),
1560 }
1561 }
1562
1563 fn route(method: &str, path: &str) -> Value {
1566 Value(Repr::Struct(Rc::new(StructValue {
1567 type_name: "http.Route".into(),
1568 fields: vec![
1569 (
1570 "method".into(),
1571 Value(Repr::Enum(Box::new(EnumValue {
1572 type_name: "http.Method".into(),
1573 case: method.into(),
1574 payload: crate::value::Payload::Empty,
1575 }))),
1576 ),
1577 ("path".into(), Value(Repr::Str(path.into()))),
1578 ("handler".into(), Value(Repr::Unit)),
1579 ],
1580 opaque: false,
1581 })))
1582 }
1583
1584 fn read_request_head(stream: &TcpStream) {
1589 let mut reader = BufReader::new(stream);
1590 loop {
1591 let mut line = String::new();
1592 let read = reader
1593 .read_line(&mut line)
1594 .expect("reading a header line should succeed");
1595 if read == 0 || line == "\r\n" || line == "\n" {
1596 break;
1597 }
1598 }
1599 }
1600
1601 struct StubReentry {
1606 calls: usize,
1607 respond: Box<dyn FnMut() -> Result<Value, RuntimeError>>,
1608 seen: Rc<RefCell<Vec<Value>>>,
1611 stop: Cancellation,
1613 expires_at: Option<Instant>,
1617 looks: Arc<AtomicUsize>,
1620 }
1621
1622 impl StubReentry {
1623 fn new(respond: impl FnMut() -> Result<Value, RuntimeError> + 'static) -> Self {
1624 StubReentry {
1625 calls: 0,
1626 respond: Box::new(respond),
1627 seen: Rc::new(RefCell::new(Vec::new())),
1628 stop: Cancellation::new(),
1629 expires_at: None,
1630 looks: Arc::new(AtomicUsize::new(0)),
1631 }
1632 }
1633
1634 fn stop(&self) -> Cancellation {
1637 self.stop.clone()
1638 }
1639
1640 fn expiring_in(mut self, left: Duration) -> Self {
1642 self.expires_at = Some(Instant::now() + left);
1643 self
1644 }
1645
1646 fn looks(&self) -> Arc<AtomicUsize> {
1648 Arc::clone(&self.looks)
1649 }
1650
1651 fn seen(&self) -> Rc<RefCell<Vec<Value>>> {
1653 Rc::clone(&self.seen)
1654 }
1655 }
1656
1657 impl Reentry for StubReentry {
1658 fn call(&mut self, _callee: &Value, args: Vec<Value>) -> Result<Value, RuntimeError> {
1659 self.calls += 1;
1660 self.seen.borrow_mut().extend(args);
1661 (self.respond)()
1662 }
1663
1664 fn call_until(
1665 &mut self,
1666 callee: &Value,
1667 args: Vec<Value>,
1668 _stop: &Cancellation,
1669 ) -> Result<Value, RuntimeError> {
1670 self.call(callee, args)
1671 }
1672
1673 fn is_cancelled(&self) -> bool {
1674 self.looks.fetch_add(1, Ordering::Relaxed);
1675 self.stop.is_cancelled()
1676 }
1677
1678 fn time_left(&self) -> Option<Duration> {
1679 self.expires_at
1680 .map(|at| at.saturating_duration_since(Instant::now()))
1681 }
1682
1683 fn task(&self) -> u64 {
1686 crate::runtime::ENTRY_TASK
1687 }
1688 }
1689
1690 #[test]
1691 fn a_denied_host_refuses_to_fetch() {
1692 let http = Http::denied();
1693 let answer = http
1694 .call(
1695 "fetch",
1696 vec![Value(Repr::Str("http://example.com/".into()))],
1697 )
1698 .unwrap();
1699 assert_eq!(
1700 err_message(answer),
1701 "http: this host has no network, so no request can be sent"
1702 );
1703 }
1704
1705 #[test]
1706 fn a_denied_host_refuses_to_listen() {
1707 let http = Http::denied();
1708 let answer = http.call("listen", vec![Value(Repr::Int(8080))]).unwrap();
1709 assert_eq!(
1710 err_message(answer),
1711 "http: this host has no network, so nothing can listen"
1712 );
1713 }
1714
1715 #[test]
1716 fn a_recorded_fetch_answers_its_response() {
1717 let http = Http::recorded(
1718 BTreeMap::from([(
1719 "http://example.com/".to_string(),
1720 RecordedResponse::ok("hello"),
1721 )]),
1722 Vec::new(),
1723 );
1724 let answer = http
1725 .call(
1726 "fetch",
1727 vec![Value(Repr::Str("http://example.com/".into()))],
1728 )
1729 .unwrap();
1730 assert_eq!(ok_response(answer), (200, "hello".to_string()));
1731 }
1732
1733 #[test]
1741 fn a_recorded_fetch_answers_a_status_outside_the_2xx_range() {
1742 let http = Http::recorded(
1743 BTreeMap::from([(
1744 "http://example.com/missing".to_string(),
1745 RecordedResponse::new(404, "gone"),
1746 )]),
1747 Vec::new(),
1748 );
1749 let answer = http
1750 .call(
1751 "fetch",
1752 vec![Value(Repr::Str("http://example.com/missing".into()))],
1753 )
1754 .unwrap();
1755 assert_eq!(ok_response(answer), (404, "gone".to_string()));
1756 }
1757
1758 #[test]
1759 fn a_fetch_the_fake_has_no_answer_for_says_so() {
1760 let http = Http::recorded(BTreeMap::new(), Vec::new());
1761 let answer = http
1762 .call(
1763 "fetch",
1764 vec![Value(Repr::Str("http://example.com/missing".into()))],
1765 )
1766 .unwrap();
1767 assert_eq!(
1768 err_message(answer),
1769 "http: no recorded answer for `http://example.com/missing`"
1770 );
1771 }
1772
1773 #[test]
1774 fn listen_issues_a_task_safe_server_handle() {
1775 let http = Http::recorded(BTreeMap::new(), Vec::new());
1776 let handle = listen(&http, 0);
1777 assert_eq!(handle.qualified_type(), "http.Server");
1778 assert!(handle.task_safe);
1779 }
1780
1781 #[test]
1782 fn port_answers_the_port_the_program_asked_for() {
1783 let http = Http::recorded(BTreeMap::new(), Vec::new());
1784 let handle = listen(&http, 4242);
1785 match http
1786 .call_resource(&handle, "port", Vec::new(), &mut NoReentry)
1787 .unwrap()
1788 {
1789 Value(Repr::Int(port)) => assert_eq!(port, 4242),
1790 other => panic!("expected an `Int`, found {other}"),
1791 }
1792 }
1793
1794 #[test]
1795 fn handle_routes_a_matching_request_to_its_handler() {
1796 let http = Http::recorded(BTreeMap::new(), vec![ScriptedRequest::get("/health")]);
1797 let handle = listen(&http, 0);
1798
1799 let routes = Value(Repr::Array(vec![route("Get", "/health")].into()));
1800 let mut back = StubReentry::new(|| Ok(response(200, "healthy")));
1801 let answer = http
1802 .call_resource(&handle, "handle", vec![routes], &mut back)
1803 .unwrap();
1804
1805 assert!(
1806 bool_ok(answer),
1807 "a scripted request should have been served"
1808 );
1809 assert_eq!(back.calls, 1, "the handler runs exactly once per request");
1810 assert_eq!(http.served().responses(), vec!["200 healthy".to_string()]);
1811 }
1812
1813 #[test]
1814 fn handle_answers_404_for_an_unrouted_request() {
1815 let http = Http::recorded(BTreeMap::new(), vec![ScriptedRequest::get("/missing")]);
1816 let handle = listen(&http, 0);
1817
1818 let routes = Value(Repr::Array(vec![route("Get", "/health")].into()));
1819 let answer = http
1820 .call_resource(&handle, "handle", vec![routes], &mut NoReentry)
1821 .unwrap();
1822
1823 assert!(
1824 bool_ok(answer),
1825 "an unrouted request is still served, just with a 404"
1826 );
1827 assert_eq!(
1828 http.served().responses(),
1829 vec!["404 \"no route for Get /missing\"".to_string()]
1830 );
1831 }
1832
1833 #[test]
1834 fn handle_drains_its_scripted_queue_then_answers_false() {
1835 let http = Http::recorded(BTreeMap::new(), vec![ScriptedRequest::get("/health")]);
1836 let handle = listen(&http, 0);
1837 let routes = Value(Repr::Array(vec![route("Get", "/health")].into()));
1838 let mut back = StubReentry::new(|| Ok(response(200, "healthy")));
1839
1840 let first = http
1841 .call_resource(&handle, "handle", vec![routes.clone()], &mut back)
1842 .unwrap();
1843 assert!(bool_ok(first), "the one scripted request should be served");
1844
1845 let second = http
1846 .call_resource(&handle, "handle", vec![routes], &mut back)
1847 .unwrap();
1848 assert!(
1849 !bool_ok(second),
1850 "an empty queue answers false rather than waiting for more"
1851 );
1852 }
1853
1854 fn within<T: Send + 'static>(limit: Duration, body: impl FnOnce() -> T + Send + 'static) -> T {
1862 let (finished, done) = std::sync::mpsc::channel();
1863 std::thread::spawn(move || {
1864 let _ = finished.send(body());
1865 });
1866 done.recv_timeout(limit)
1867 .unwrap_or_else(|_| panic!("this did not finish within {limit:?}"))
1868 }
1869
1870 #[test]
1881 fn a_handler_may_call_back_into_the_same_host_that_is_serving_it() {
1882 let (served, answered) = within(Duration::from_secs(10), || {
1883 let http = Arc::new(Http::recorded(
1884 BTreeMap::new(),
1885 vec![ScriptedRequest::get("/health")],
1886 ));
1887 let handle = listen(&http, 4242);
1888 let inside = Arc::clone(&http);
1889 let serving = Arc::clone(&handle);
1890 let mut back = StubReentry::new(move || {
1891 let port = inside.call_resource(&serving, "port", Vec::new(), &mut NoReentry)?;
1892 let second = inside.call("listen", vec![Value(Repr::Int(8080))])?;
1893 assert!(is_ok(&second), "a second listener opened: {second}");
1894 Ok(response(200, &format!("{port}")))
1895 });
1896
1897 let routes = Value(Repr::Array(vec![route("Get", "/health")].into()));
1898 let answer = http
1899 .call_resource(&handle, "handle", vec![routes], &mut back)
1900 .unwrap();
1901 (http.served().responses(), bool_ok(answer))
1902 });
1903 assert!(answered, "the scripted request was served");
1904 assert_eq!(served, vec!["200 4242".to_string()]);
1905 }
1906
1907 #[test]
1911 fn close_ends_the_handle_and_a_later_call_reports_it() {
1912 let http = Http::recorded(BTreeMap::new(), Vec::new());
1913 let handle = listen(&http, 0);
1914
1915 let closed = http
1916 .call_resource(&handle, "close", Vec::new(), &mut NoReentry)
1917 .unwrap();
1918 assert!(is_ok(&closed), "{closed}");
1919
1920 let error = http
1921 .call_resource(&handle, "port", Vec::new(), &mut NoReentry)
1922 .expect_err("a closed handle's port cannot be read");
1923 assert_eq!(
1924 error.message,
1925 format!("`{handle}` is closed, so `port` has nothing to act on")
1926 );
1927 }
1928
1929 #[test]
1930 fn json_encodes_a_struct_as_an_object() {
1931 let http = Http::denied();
1932 let payload = Value(Repr::Struct(Rc::new(StructValue {
1933 type_name: "demo.Point".into(),
1934 fields: vec![
1935 ("x".into(), Value(Repr::Int(1))),
1936 ("y".into(), Value(Repr::Int(2))),
1937 ],
1938 opaque: false,
1939 })));
1940 let answer = http
1941 .call("json", vec![Value(Repr::Int(200)), payload])
1942 .unwrap();
1943 assert_eq!(response_body(answer), "{\"x\":1,\"y\":2}");
1944 }
1945
1946 #[test]
1947 fn json_encodes_a_map_as_an_object() {
1948 let http = Http::denied();
1949 let mut map = BTreeMap::new();
1950 map.insert(MapKey::Str("a".to_string()), Value(Repr::Int(1)));
1951 let answer = http
1952 .call(
1953 "json",
1954 vec![Value(Repr::Int(200)), Value(Repr::Map(Rc::new(map)))],
1955 )
1956 .unwrap();
1957 assert_eq!(response_body(answer), "{\"a\":1}");
1958 }
1959
1960 #[test]
1961 fn json_encodes_a_string_with_its_quotes() {
1962 let http = Http::denied();
1963 let answer = http
1964 .call(
1965 "json",
1966 vec![Value(Repr::Int(200)), Value(Repr::Str("hi".into()))],
1967 )
1968 .unwrap();
1969 assert_eq!(response_body(answer), "\"hi\"");
1970 }
1971
1972 #[test]
1973 fn json_encodes_an_array() {
1974 let http = Http::denied();
1975 let payload = Value(Repr::Array(
1976 vec![Value(Repr::Int(1)), Value(Repr::Int(2))].into(),
1977 ));
1978 let answer = http
1979 .call("json", vec![Value(Repr::Int(200)), payload])
1980 .unwrap();
1981 assert_eq!(response_body(answer), "[1,2]");
1982 }
1983
1984 #[test]
1985 fn json_encodes_a_payload_free_enum_case_as_its_name() {
1986 let http = Http::denied();
1987 let payload = Value(Repr::Enum(Box::new(EnumValue {
1988 type_name: "demo.Color".into(),
1989 case: "Red".into(),
1990 payload: crate::value::Payload::Empty,
1991 })));
1992 let answer = http
1993 .call("json", vec![Value(Repr::Int(200)), payload])
1994 .unwrap();
1995 assert_eq!(response_body(answer), "\"Red\"");
1996 }
1997
1998 #[test]
1999 fn json_escapes_a_quote_and_a_newline() {
2000 let http = Http::denied();
2001 let payload = Value(Repr::Str("a\"b\nc".into()));
2002 let answer = http
2003 .call("json", vec![Value(Repr::Int(200)), payload])
2004 .unwrap();
2005 assert_eq!(response_body(answer), r#""a\"b\nc""#);
2006 }
2007
2008 #[test]
2009 fn split_url_refuses_https() {
2010 assert_eq!(
2011 split_url("https://example.com/").unwrap_err(),
2012 "http: `https://example.com/` is https, which this host does not speak"
2013 );
2014 }
2015
2016 #[test]
2017 fn split_url_refuses_an_unknown_scheme() {
2018 assert_eq!(
2019 split_url("ftp://example.com/").unwrap_err(),
2020 "http: `ftp://example.com/` uses the unknown scheme `ftp`"
2021 );
2022 }
2023
2024 #[test]
2025 fn split_url_refuses_a_non_absolute_url() {
2026 assert_eq!(
2027 split_url("example.com/path").unwrap_err(),
2028 "http: `example.com/path` is not an absolute URL"
2029 );
2030 }
2031
2032 #[test]
2033 fn a_real_fetch_reads_the_body_a_2xx_response_carries() {
2034 let listener =
2035 TcpListener::bind("127.0.0.1:0").expect("binding to loopback should succeed");
2036 let port = listener
2037 .local_addr()
2038 .expect("the bound address should be known")
2039 .port();
2040
2041 let server = std::thread::spawn(move || {
2042 let (mut stream, _) = listener
2043 .accept()
2044 .expect("accepting the one connection should succeed");
2045 read_request_head(&stream);
2046 let body = "hello from loopback";
2047 let response = format!(
2048 "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
2049 body.len(),
2050 body
2051 );
2052 stream
2053 .write_all(response.as_bytes())
2054 .expect("writing the canned response should succeed");
2055 });
2056
2057 let url = format!("http://127.0.0.1:{port}/");
2058 let answer = Http::real()
2059 .call("fetch", vec![Value(Repr::Str(url.into()))])
2060 .unwrap();
2061 server.join().expect("the server thread should not panic");
2062
2063 assert_eq!(
2064 ok_response(answer),
2065 (200, "hello from loopback".to_string())
2066 );
2067 }
2068
2069 #[test]
2077 fn a_real_fetch_answers_a_non_2xx_status_and_its_body() {
2078 let listener =
2079 TcpListener::bind("127.0.0.1:0").expect("binding to loopback should succeed");
2080 let port = listener
2081 .local_addr()
2082 .expect("the bound address should be known")
2083 .port();
2084
2085 let server = std::thread::spawn(move || {
2086 let (mut stream, _) = listener
2087 .accept()
2088 .expect("accepting the one connection should succeed");
2089 read_request_head(&stream);
2090 let body = "not found here";
2091 let response = format!(
2092 "HTTP/1.1 404 Not Found\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
2093 body.len(),
2094 body
2095 );
2096 stream
2097 .write_all(response.as_bytes())
2098 .expect("writing the canned response should succeed");
2099 });
2100
2101 let url = format!("http://127.0.0.1:{port}/");
2102 let answer = Http::real()
2103 .call("fetch", vec![Value(Repr::Str(url.clone().into()))])
2104 .unwrap();
2105 server.join().expect("the server thread should not panic");
2106
2107 assert_eq!(ok_response(answer), (404, "not found here".to_string()));
2108 }
2109
2110 #[test]
2120 fn a_real_fetch_refuses_a_response_past_the_bound() {
2121 let listener =
2122 TcpListener::bind("127.0.0.1:0").expect("binding to loopback should succeed");
2123 let port = listener
2124 .local_addr()
2125 .expect("the bound address should be known")
2126 .port();
2127
2128 let server = std::thread::spawn(move || {
2129 let (mut stream, _) = listener
2130 .accept()
2131 .expect("accepting the one connection should succeed");
2132 read_request_head(&stream);
2133 let body = "a".repeat(MAX_RESPONSE_BYTES + 1);
2134 let response = format!(
2135 "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
2136 body.len(),
2137 body
2138 );
2139 let _ = stream.write_all(response.as_bytes());
2144 });
2145
2146 let url = format!("http://127.0.0.1:{port}/");
2147 let answer = Http::real()
2148 .call("fetch", vec![Value(Repr::Str(url.into()))])
2149 .unwrap();
2150 let _ = server.join();
2151
2152 assert!(
2153 err_message(answer).ends_with(&format!(
2154 "sent more than the {MAX_RESPONSE_BYTES} bytes this host reads"
2155 )),
2156 "the bound is named"
2157 );
2158 }
2159
2160 #[test]
2165 fn the_real_host_serves_a_request_end_to_end_over_loopback() {
2166 let http = Http::real();
2167 let opened = http.call("listen", vec![Value(Repr::Int(0))]).unwrap();
2168 let Value(Repr::Enum(result)) = opened else {
2169 panic!("expected `Ok(...)`");
2170 };
2171 let Some(Value(Repr::Resource(handle))) = result.payload.into_vec().into_iter().next()
2172 else {
2173 panic!("`listen` should answer a handle");
2174 };
2175 let port = match http
2176 .call_resource(&handle, "port", Vec::new(), &mut NoReentry)
2177 .unwrap()
2178 {
2179 Value(Repr::Int(port)) => port,
2180 other => panic!("expected an `Int` port, found {other}"),
2181 };
2182
2183 let client = std::thread::spawn(move || {
2184 let mut stream = TcpStream::connect(("127.0.0.1", port as u16))
2185 .expect("connecting to the loopback listener should succeed");
2186 stream
2187 .write_all(b"GET /health HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\n\r\n")
2188 .expect("writing the request should succeed");
2189 let mut answer = Vec::new();
2190 stream
2191 .read_to_end(&mut answer)
2192 .expect("reading the response should succeed");
2193 String::from_utf8_lossy(&answer).into_owned()
2194 });
2195
2196 let routes = Value(Repr::Array(vec![route("Get", "/health")].into()));
2197 let mut back = StubReentry::new(|| Ok(response(200, "healthy")));
2198 let served = bool_ok(
2199 http.call_resource(&handle, "handle", vec![routes], &mut back)
2200 .unwrap(),
2201 );
2202 assert!(served, "the listener should have answered one request");
2203
2204 let received = client.join().expect("the client thread should not panic");
2205 assert!(received.starts_with("HTTP/1.1 200"), "{received}");
2206 assert!(received.ends_with("healthy"), "{received}");
2207
2208 http.call_resource(&handle, "close", Vec::new(), &mut NoReentry)
2209 .unwrap();
2210 }
2211
2212 const PROMPTLY: Duration = Duration::from_secs(2);
2221
2222 const CUT_SHORT_AFTER: Duration = Duration::from_millis(150);
2231
2232 fn quiet_listener() -> (Http, Arc<ResourceHandle>, Value) {
2238 let http = Http::real();
2239 let handle = listen(&http, 0);
2240 (
2241 http,
2242 handle,
2243 Value(Repr::Array(vec![route("Get", "/health")].into())),
2244 )
2245 }
2246
2247 #[test]
2254 fn cancelling_a_run_waiting_for_a_connection_stops_the_wait() {
2255 let (http, handle, routes) = quiet_listener();
2256 let mut back = StubReentry::new(|| panic!("nothing connects, so no handler runs"));
2257 let stop = back.stop();
2258 let raised_after = Duration::from_millis(50);
2259 let started = Instant::now();
2263 std::thread::spawn(move || {
2264 std::thread::sleep(raised_after);
2265 stop.cancel();
2266 });
2267
2268 let answer = http
2269 .call_resource(&handle, "handle", vec![routes], &mut back)
2270 .unwrap();
2271 let waited = started.elapsed();
2272
2273 assert!(
2274 !bool_ok(answer),
2275 "a listener that was stopped has nothing more to serve"
2276 );
2277 assert!(
2278 waited >= raised_after,
2279 "the wait ended before there was anything to end it, after {waited:?}"
2280 );
2281 assert!(
2282 waited < PROMPTLY,
2283 "the wait outlived the cancellation by {waited:?}"
2284 );
2285 assert_eq!(back.calls, 0, "no request arrived, so no handler ran");
2286 }
2287
2288 #[test]
2292 fn a_run_deadline_that_expires_while_waiting_for_a_connection_ends_the_wait() {
2293 let (http, handle, routes) = quiet_listener();
2294 let left = Duration::from_millis(50);
2295 let started = Instant::now();
2299 let mut back =
2300 StubReentry::new(|| panic!("nothing connects, so no handler runs")).expiring_in(left);
2301
2302 let answer = http
2303 .call_resource(&handle, "handle", vec![routes], &mut back)
2304 .unwrap();
2305 let waited = started.elapsed();
2306
2307 assert!(
2308 !bool_ok(answer),
2309 "a listener whose run has run out of time has nothing more to serve"
2310 );
2311 assert!(
2312 waited >= left,
2313 "the wait ended before the deadline it was waiting for, after {waited:?}"
2314 );
2315 assert!(
2316 waited < PROMPTLY,
2317 "the wait outlived the deadline by {waited:?}"
2318 );
2319 }
2320
2321 #[test]
2330 fn waiting_for_a_connection_polls_rather_than_spinning() {
2331 let (http, handle, routes) = quiet_listener();
2332 let waiting = Duration::from_millis(200);
2333 let mut back = StubReentry::new(|| panic!("nothing connects, so no handler runs"))
2334 .expiring_in(waiting);
2335 let looks = back.looks();
2336
2337 http.call_resource(&handle, "handle", vec![routes], &mut back)
2338 .unwrap();
2339
2340 let looks = looks.load(Ordering::Relaxed);
2341 let sleeping = (waiting.as_millis() / POLL_INTERVAL.as_millis()) as usize;
2342 assert!(looks >= 1, "the host never looked at the run at all");
2343 assert!(
2344 looks < sleeping * 20,
2345 "{looks} looks in {waiting:?} is a spin, not a poll at one every {POLL_INTERVAL:?}"
2346 );
2347 }
2348
2349 #[test]
2357 fn a_run_deadline_that_expires_while_reading_answers_408() {
2358 let http = Http::real();
2359 let handle = listen(&http, 0);
2360 let port = match http
2361 .call_resource(&handle, "port", Vec::new(), &mut NoReentry)
2362 .unwrap()
2363 {
2364 Value(Repr::Int(port)) => port,
2365 other => panic!("expected an `Int` port, found {other}"),
2366 };
2367
2368 let client = std::thread::spawn(move || {
2371 let mut stream = TcpStream::connect(("127.0.0.1", port as u16))
2372 .expect("connecting to the loopback listener should succeed");
2373 stream
2374 .write_all(b"GET /health HTT")
2375 .expect("writing half a request line should succeed");
2376 let mut answer = Vec::new();
2377 stream
2378 .read_to_end(&mut answer)
2379 .expect("reading the response should succeed");
2380 String::from_utf8_lossy(&answer).into_owned()
2381 });
2382
2383 let routes = Value(Repr::Array(vec![route("Get", "/health")].into()));
2384 let mut back = StubReentry::new(|| panic!("no whole request arrives, so no handler runs"))
2385 .expiring_in(Duration::from_millis(150));
2386 let started = Instant::now();
2387 let answer = http
2388 .call_resource(&handle, "handle", vec![routes], &mut back)
2389 .unwrap();
2390 let took = started.elapsed();
2391
2392 assert!(
2393 bool_ok(answer),
2394 "a request that arrived and could not be read is still one that arrived"
2395 );
2396 assert!(
2397 took < PROMPTLY,
2398 "the read waited on `READ_TIMEOUT` rather than on the run, for {took:?}"
2399 );
2400 let received = client.join().expect("the client thread should not panic");
2401 assert!(
2402 received.starts_with("HTTP/1.1 408 Request Timeout"),
2403 "{received}"
2404 );
2405
2406 http.call_resource(&handle, "close", Vec::new(), &mut NoReentry)
2407 .unwrap();
2408 }
2409
2410 #[test]
2414 fn an_allowance_is_the_shorter_of_the_hosts_and_the_runs() {
2415 assert_eq!(bounded(READ_TIMEOUT, None), READ_TIMEOUT);
2416 assert_eq!(
2417 bounded(READ_TIMEOUT, Some(Duration::from_millis(200))),
2418 Duration::from_millis(200)
2419 );
2420 assert_eq!(
2421 bounded(READ_TIMEOUT, Some(Duration::from_secs(600))),
2422 READ_TIMEOUT
2423 );
2424 assert_eq!(bounded(READ_TIMEOUT, Some(Duration::ZERO)), Duration::ZERO);
2425 }
2426
2427 struct Exchange {
2429 served: bool,
2431 received: String,
2433 took: Duration,
2436 seen: Vec<Value>,
2439 }
2440
2441 fn serve_raw(request: Vec<u8>) -> Exchange {
2450 let http = Http::real();
2451 let handle = listen(&http, 0);
2452 let port = match http
2453 .call_resource(&handle, "port", Vec::new(), &mut NoReentry)
2454 .unwrap()
2455 {
2456 Value(Repr::Int(port)) => port,
2457 other => panic!("expected an `Int` port, found {other}"),
2458 };
2459
2460 let client = std::thread::spawn(move || {
2461 let mut stream = TcpStream::connect(("127.0.0.1", port as u16))
2462 .expect("connecting to the loopback listener should succeed");
2463 stream
2466 .set_read_timeout(Some(PROMPTLY))
2467 .expect("bounding the client's own read should succeed");
2468 let _ = stream.write_all(&request);
2469 let mut answer = Vec::new();
2470 let _ = stream.read_to_end(&mut answer);
2471 String::from_utf8_lossy(&answer).into_owned()
2472 });
2473
2474 let routes = Value(Repr::Array(
2475 vec![route("Get", "/health"), route("Post", "/echo")].into(),
2476 ));
2477 let mut back = StubReentry::new(|| Ok(response(200, "healthy")));
2478 let seen = back.seen();
2479 let started = Instant::now();
2480 let served = bool_ok(
2481 http.call_resource(&handle, "handle", vec![routes], &mut back)
2482 .unwrap(),
2483 );
2484 let took = started.elapsed();
2485 let received = client.join().expect("the client thread should not panic");
2486 http.call_resource(&handle, "close", Vec::new(), &mut NoReentry)
2487 .unwrap();
2488 let seen = seen.borrow().clone();
2489 Exchange {
2490 served,
2491 received,
2492 took,
2493 seen,
2494 }
2495 }
2496
2497 fn status_line(exchange: &Exchange) -> &str {
2499 exchange
2500 .received
2501 .lines()
2502 .next()
2503 .unwrap_or_else(|| panic!("the peer was told nothing at all"))
2504 }
2505
2506 fn refused(exchange: &Exchange, status: &str) {
2509 assert_eq!(
2510 status_line(exchange),
2511 status,
2512 "the peer was told: {}",
2513 exchange.received
2514 );
2515 assert!(
2516 exchange.served,
2517 "a request that arrived and was refused is still one that arrived"
2518 );
2519 assert!(
2520 exchange.seen.is_empty(),
2521 "a refused request must not reach a handler"
2522 );
2523 assert!(
2524 exchange.took < PROMPTLY,
2525 "the refusal took {:?}, which is long enough to have read the whole thing",
2526 exchange.took
2527 );
2528 }
2529
2530 #[test]
2531 fn a_request_line_past_the_bound_is_refused_with_414() {
2532 let target = "/".to_string() + &"a".repeat(MAX_REQUEST_LINE);
2533 let request = format!("GET {target} HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n");
2534 refused(
2535 &serve_raw(request.into_bytes()),
2536 "HTTP/1.1 414 URI Too Long",
2537 );
2538 }
2539
2540 #[test]
2541 fn a_header_past_the_bound_is_refused_with_431() {
2542 let request = format!(
2543 "GET /health HTTP/1.1\r\nHost: 127.0.0.1\r\nX-Long: {}\r\n\r\n",
2544 "a".repeat(MAX_HEADER_BYTES)
2545 );
2546 refused(
2547 &serve_raw(request.into_bytes()),
2548 "HTTP/1.1 431 Request Header Fields Too Large",
2549 );
2550 }
2551
2552 #[test]
2553 fn more_headers_than_the_bound_are_refused_with_431() {
2554 let mut request = "GET /health HTTP/1.1\r\n".to_string();
2555 for n in 0..=MAX_HEADER_COUNT {
2558 request.push_str(&format!("X-{n}: 1\r\n"));
2559 }
2560 request.push_str("\r\n");
2561 assert!(
2562 request.len() < MAX_HEADERS_BYTES,
2563 "this request should pass the count bound, not the byte one"
2564 );
2565 refused(
2566 &serve_raw(request.into_bytes()),
2567 "HTTP/1.1 431 Request Header Fields Too Large",
2568 );
2569 }
2570
2571 #[test]
2572 fn more_header_bytes_than_the_bound_are_refused_with_431() {
2573 let mut request = "GET /health HTTP/1.1\r\n".to_string();
2574 let padding = "a".repeat(4 * 1024);
2577 for n in 0..16 {
2578 request.push_str(&format!("X-{n}: {padding}\r\n"));
2579 }
2580 request.push_str("\r\n");
2581 refused(
2582 &serve_raw(request.into_bytes()),
2583 "HTTP/1.1 431 Request Header Fields Too Large",
2584 );
2585 }
2586
2587 #[test]
2588 fn a_body_past_the_bound_is_refused_with_413() {
2589 let request = format!(
2590 "POST /echo HTTP/1.1\r\nHost: 127.0.0.1\r\nContent-Length: {}\r\n\r\n",
2591 MAX_BODY_BYTES + 1
2592 );
2593 refused(
2594 &serve_raw(request.into_bytes()),
2595 "HTTP/1.1 413 Payload Too Large",
2596 );
2597 }
2598
2599 #[test]
2607 fn a_preposterous_content_length_is_refused_before_any_of_it_is_read() {
2608 let request =
2609 "POST /echo HTTP/1.1\r\nHost: 127.0.0.1\r\nContent-Length: 999999999999999\r\n\r\n";
2610 refused(
2611 &serve_raw(request.as_bytes().to_vec()),
2612 "HTTP/1.1 413 Payload Too Large",
2613 );
2614 }
2615
2616 #[test]
2617 fn a_content_length_that_is_not_a_number_is_refused_with_400() {
2618 let request = "POST /echo HTTP/1.1\r\nHost: 127.0.0.1\r\nContent-Length: some\r\n\r\n";
2619 refused(
2620 &serve_raw(request.as_bytes().to_vec()),
2621 "HTTP/1.1 400 Bad Request",
2622 );
2623 }
2624
2625 #[test]
2626 fn two_content_lengths_that_disagree_are_refused_with_400() {
2627 let request =
2628 "POST /echo HTTP/1.1\r\nContent-Length: 3\r\nContent-Length: 4\r\n\r\nabc\r\n\r\n";
2629 refused(
2630 &serve_raw(request.as_bytes().to_vec()),
2631 "HTTP/1.1 400 Bad Request",
2632 );
2633 }
2634
2635 #[test]
2638 fn two_content_lengths_that_agree_are_served() {
2639 let request = "POST /echo HTTP/1.1\r\nContent-Length: 3\r\nContent-Length: 3\r\n\r\nabc";
2640 let exchange = serve_raw(request.as_bytes().to_vec());
2641 assert_eq!(status_line(&exchange), "HTTP/1.1 200 OK");
2642 assert_eq!(body_of(&exchange.seen), "abc");
2643 }
2644
2645 #[test]
2646 fn a_transfer_encoding_is_refused_with_501() {
2647 let request =
2648 "POST /echo HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n3\r\nabc\r\n0\r\n\r\n";
2649 refused(
2650 &serve_raw(request.as_bytes().to_vec()),
2651 "HTTP/1.1 501 Not Implemented",
2652 );
2653 }
2654
2655 #[test]
2657 fn a_request_with_an_ordinary_body_is_still_served() {
2658 let body = "{\"name\":\"cove\"}";
2659 let request = format!(
2660 "POST /echo HTTP/1.1\r\nHost: 127.0.0.1\r\nContent-Length: {}\r\n\r\n{body}",
2661 body.len()
2662 );
2663 let exchange = serve_raw(request.into_bytes());
2664 assert_eq!(status_line(&exchange), "HTTP/1.1 200 OK");
2665 assert!(
2666 exchange.received.ends_with("healthy"),
2667 "{}",
2668 exchange.received
2669 );
2670 assert_eq!(body_of(&exchange.seen), body);
2671 }
2672
2673 #[test]
2675 fn a_body_of_exactly_the_bound_is_served() {
2676 let body = "a".repeat(MAX_BODY_BYTES);
2677 let request = format!(
2678 "POST /echo HTTP/1.1\r\nContent-Length: {}\r\n\r\n{body}",
2679 body.len()
2680 );
2681 let exchange = serve_raw(request.into_bytes());
2682 assert_eq!(status_line(&exchange), "HTTP/1.1 200 OK");
2683 assert_eq!(body_of(&exchange.seen).len(), MAX_BODY_BYTES);
2684 }
2685
2686 fn body_of(seen: &[Value]) -> String {
2688 match seen {
2689 [Value(Repr::Struct(request))] => match request.get("body") {
2690 Some(Value(Repr::Str(body))) => body.to_string(),
2691 other => panic!("expected a `String` body, found {other:?}"),
2692 },
2693 other => panic!("expected exactly one request, found {other:?}"),
2694 }
2695 }
2696
2697 #[test]
2700 fn a_content_length_is_a_count_of_bytes_or_a_refusal() {
2701 assert_eq!(content_length("0").ok(), Some(0));
2702 assert_eq!(content_length("12").ok(), Some(12));
2703 assert_eq!(
2704 content_length(&MAX_BODY_BYTES.to_string()).ok(),
2705 Some(MAX_BODY_BYTES)
2706 );
2707
2708 for value in ["", "-1", "1 2", "0x10", "12kb", "+3", "one"] {
2709 let refused = content_length(value).expect_err("this is not a count");
2710 assert_eq!(refused.status, 400, "`{value}` is malformed, not too big");
2711 }
2712
2713 for value in [
2716 (MAX_BODY_BYTES + 1).to_string(),
2717 format!("{}", u64::MAX),
2718 "9".repeat(200),
2719 ] {
2720 let refused = content_length(&value).expect_err("this is too big");
2721 assert_eq!(refused.status, 413, "`{value}` is too big, not malformed");
2722 }
2723 }
2724
2725 #[test]
2728 fn a_fetch_with_no_time_left_is_refused_before_it_connects() {
2729 let back =
2730 StubReentry::new(|| panic!("a fetch runs no handler")).expiring_in(Duration::ZERO);
2731 let answer = fetch_over_tcp("http://127.0.0.1:1/", &back)
2732 .expect_err("a run with no time left cannot fetch");
2733 assert_eq!(
2734 answer,
2735 "http: the run ran out of time before 127.0.0.1:1 could be asked"
2736 );
2737 }
2738
2739 #[test]
2747 fn a_fetch_made_by_a_stopped_run_is_refused_before_it_connects() {
2748 let back = StubReentry::new(|| panic!("a fetch runs no handler"));
2749 back.stop().cancel();
2750 let answer =
2751 fetch_over_tcp("http://127.0.0.1:1/", &back).expect_err("a stopped run cannot fetch");
2752 assert_eq!(
2753 answer,
2754 "http: the run was stopped before 127.0.0.1:1 could be asked"
2755 );
2756 }
2757
2758 fn stalling_server(hung_up: Arc<AtomicBool>) -> (u16, std::thread::JoinHandle<()>) {
2768 let listener =
2769 TcpListener::bind("127.0.0.1:0").expect("binding to loopback should succeed");
2770 let port = listener
2771 .local_addr()
2772 .expect("the bound address should be known")
2773 .port();
2774 let thread = std::thread::spawn(move || {
2775 let (stream, _) = listener
2776 .accept()
2777 .expect("accepting the one connection should succeed");
2778 read_request_head(&stream);
2779 while !hung_up.load(Ordering::Relaxed) {
2780 std::thread::sleep(Duration::from_millis(5));
2781 }
2782 drop(stream);
2783 });
2784 (port, thread)
2785 }
2786
2787 #[test]
2800 fn a_cancellation_raised_while_a_fetch_reads_cuts_the_read_short() {
2801 let hung_up = Arc::new(AtomicBool::new(false));
2802 let (port, server) = stalling_server(Arc::clone(&hung_up));
2803
2804 let back = StubReentry::new(|| panic!("a fetch runs no handler"));
2805 let stop = back.stop();
2806 let raising = std::thread::spawn(move || {
2807 std::thread::sleep(CUT_SHORT_AFTER);
2808 stop.cancel();
2809 });
2810
2811 let url = format!("http://127.0.0.1:{port}/");
2812 let started = Instant::now();
2813 let answer = fetch_over_tcp(&url, &back).expect_err("a cancelled fetch has no response");
2814 let took = started.elapsed();
2815
2816 hung_up.store(true, Ordering::Relaxed);
2817 raising.join().expect("the raising thread should not panic");
2818 server.join().expect("the server thread should not panic");
2819
2820 assert_eq!(
2821 answer,
2822 format!("http: the run was stopped before 127.0.0.1:{port} answered")
2823 );
2824 assert!(
2825 took >= CUT_SHORT_AFTER,
2826 "the read ended before the cancellation that was supposed to end it, after {took:?}"
2827 );
2828 assert!(
2829 took < PROMPTLY,
2830 "the read waited on `READ_TIMEOUT` rather than on the cancellation, for {took:?}"
2831 );
2832 }
2833
2834 #[test]
2842 fn a_run_deadline_that_expires_while_a_fetch_reads_cuts_the_read_short() {
2843 let hung_up = Arc::new(AtomicBool::new(false));
2844 let (port, server) = stalling_server(Arc::clone(&hung_up));
2845
2846 let back =
2847 StubReentry::new(|| panic!("a fetch runs no handler")).expiring_in(CUT_SHORT_AFTER);
2848 let url = format!("http://127.0.0.1:{port}/");
2849 let started = Instant::now();
2850 let answer = fetch_over_tcp(&url, &back).expect_err("an expired fetch has no response");
2851 let took = started.elapsed();
2852
2853 hung_up.store(true, Ordering::Relaxed);
2854 server.join().expect("the server thread should not panic");
2855
2856 assert_eq!(
2857 answer,
2858 format!("http: the run was stopped before 127.0.0.1:{port} answered")
2859 );
2860 assert!(
2861 took < PROMPTLY,
2862 "the read waited on `READ_TIMEOUT` rather than on the run, for {took:?}"
2863 );
2864 }
2865
2866 #[test]
2874 fn waiting_for_a_response_polls_rather_than_spinning() {
2875 let hung_up = Arc::new(AtomicBool::new(false));
2876 let (port, server) = stalling_server(Arc::clone(&hung_up));
2877
2878 let back =
2879 StubReentry::new(|| panic!("a fetch runs no handler")).expiring_in(CUT_SHORT_AFTER);
2880 let looks = back.looks();
2881 let url = format!("http://127.0.0.1:{port}/");
2882 let _ = fetch_over_tcp(&url, &back);
2883
2884 hung_up.store(true, Ordering::Relaxed);
2885 server.join().expect("the server thread should not panic");
2886
2887 let looks = looks.load(Ordering::Relaxed);
2888 let polling = (CUT_SHORT_AFTER.as_millis() / POLL_INTERVAL.as_millis()) as usize;
2889 assert!(looks >= 1, "the client never looked at the run at all");
2890 assert!(
2891 looks < polling * 20,
2892 "{looks} looks in {CUT_SHORT_AFTER:?} is a spin, not a poll at one every {POLL_INTERVAL:?}"
2893 );
2894 }
2895}