mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-15 07:36:50 +03:00
live-generate: the dev-server watch answers https and every address
The liveness probe behind `dev_server_gone` only knew `http://` and tried the first address a host resolved to, so an https dev URL, or a `localhost` that resolves to ::1 ahead of the 127.0.0.1 the server listens on, counted as two misses and ended the wait while the server was up. The probe now takes either scheme (a TCP connect is enough to know a server is there, TLS or not) and tries every resolved address; the boot's tag probe shares the address walk and stays plain http, since reading the document over https would need TLS. Written with AI assistance (Claude). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
committed by
Abdul Wahab
co-authored by
Claude Fable 5.1
parent
0603f54a27
commit
2f4d4fb4fc
+57
-24
@@ -49,37 +49,47 @@ pub fn probe(candidates: &[String], token: &str) -> Option<String> {
|
||||
|
||||
/// Whether something accepts connections at the URL's host and port: the
|
||||
/// liveness check `live-generate` runs while it waits for a page, cheap
|
||||
/// enough for every few seconds and immune to a slow first render.
|
||||
/// enough for every few seconds and immune to a slow first render. Any
|
||||
/// scheme: a dev server behind https accepts the TCP connection like any
|
||||
/// other, so no TLS is needed to know it is up.
|
||||
pub fn answers(url: &str) -> bool {
|
||||
let Some(rest) = url.strip_prefix("http://") else {
|
||||
return false;
|
||||
};
|
||||
let Some(host_port) = rest.split('/').next() else {
|
||||
return false;
|
||||
};
|
||||
let (host, port) = match host_port.rsplit_once(':') {
|
||||
Some((h, p)) => match p.parse::<u16>() {
|
||||
Ok(port) => (h, port),
|
||||
Err(_) => return false,
|
||||
},
|
||||
None => (host_port, 80),
|
||||
};
|
||||
let Some(addr) = (host, port).to_socket_addrs().ok().and_then(|mut a| a.next()) else {
|
||||
return false;
|
||||
};
|
||||
TcpStream::connect_timeout(&addr, Duration::from_millis(300)).is_ok()
|
||||
endpoint(url).is_some_and(|(host, port, _)| connect(host, port).is_some())
|
||||
}
|
||||
|
||||
/// A minimal HTTP/1.0 GET of `/`; returns the response body on any 2xx.
|
||||
fn fetch_root(url: &str) -> Option<String> {
|
||||
let rest = url.strip_prefix("http://")?;
|
||||
/// `(host, port, host:port as written)` from an `http://` or `https://`
|
||||
/// origin; None for anything else.
|
||||
fn endpoint(url: &str) -> Option<(&str, u16, &str)> {
|
||||
let (rest, default_port) = match url.strip_prefix("http://") {
|
||||
Some(rest) => (rest, 80),
|
||||
None => (url.strip_prefix("https://")?, 443),
|
||||
};
|
||||
let host_port = rest.split('/').next()?;
|
||||
let (host, port) = match host_port.rsplit_once(':') {
|
||||
Some((h, p)) => (h, p.parse::<u16>().ok()?),
|
||||
None => (host_port, 80),
|
||||
None => (host_port, default_port),
|
||||
};
|
||||
let addr = (host, port).to_socket_addrs().ok()?.next()?;
|
||||
let mut stream = TcpStream::connect_timeout(&addr, Duration::from_millis(300)).ok()?;
|
||||
Some((host.trim_start_matches('[').trim_end_matches(']'), port, host_port))
|
||||
}
|
||||
|
||||
/// A connection to the first address the host resolves to that accepts
|
||||
/// one: `localhost` can resolve to `::1` ahead of `127.0.0.1` while the dev
|
||||
/// server listens on only one of them.
|
||||
fn connect(host: &str, port: u16) -> Option<TcpStream> {
|
||||
(host, port)
|
||||
.to_socket_addrs()
|
||||
.ok()?
|
||||
.find_map(|addr| TcpStream::connect_timeout(&addr, Duration::from_millis(300)).ok())
|
||||
}
|
||||
|
||||
/// A minimal HTTP/1.0 GET of `/`; returns the response body on any 2xx.
|
||||
/// Plain http only: the tag check needs the document, and an https dev
|
||||
/// server would need TLS to hand it over.
|
||||
fn fetch_root(url: &str) -> Option<String> {
|
||||
if !url.starts_with("http://") {
|
||||
return None;
|
||||
}
|
||||
let (host, port, host_port) = endpoint(url)?;
|
||||
let mut stream = connect(host, port)?;
|
||||
stream.set_read_timeout(Some(Duration::from_millis(1500))).ok()?;
|
||||
stream.set_write_timeout(Some(Duration::from_millis(300))).ok()?;
|
||||
stream
|
||||
@@ -138,6 +148,29 @@ mod tests {
|
||||
assert_eq!(probe(&[theirs], "abc-123"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn answers_whatever_scheme_the_page_uses() {
|
||||
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
|
||||
let port = listener.local_addr().unwrap().port();
|
||||
assert!(answers(&format!("http://127.0.0.1:{}/", port)));
|
||||
assert!(
|
||||
answers(&format!("https://127.0.0.1:{}/", port)),
|
||||
"an https dev server accepts the connection like any other"
|
||||
);
|
||||
assert!(!answers(&format!("ftp://127.0.0.1:{}/", port)));
|
||||
drop(listener);
|
||||
assert!(!answers(&format!("https://127.0.0.1:{}/", port)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn answers_tries_every_address_the_host_resolves_to() {
|
||||
// `localhost` resolves to ::1 ahead of 127.0.0.1 on some hosts; a
|
||||
// server listening on just one of them still answers.
|
||||
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
|
||||
let port = listener.local_addr().unwrap().port();
|
||||
assert!(answers(&format!("http://localhost:{}/", port)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn env_override_replaces_the_default_list() {
|
||||
let c = candidates(Some("http://localhost:9999, http://127.0.0.1:7777/"));
|
||||
|
||||
Reference in New Issue
Block a user