Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
f7488dd
stash
katrinafyi Oct 4, 2025
a9fe9ad
stash very broken
katrinafyi Oct 4, 2025
cef997e
Revert "stash very broken"
katrinafyi Oct 4, 2025
25f130b
Revert "stash"
katrinafyi Oct 4, 2025
d272a2b
feat: add CreateRequestItem error kind
katrinafyi Oct 4, 2025
f5fd8df
wrap
katrinafyi Oct 4, 2025
e4c5482
smuggle request errors through CreateRequestItem
katrinafyi Oct 4, 2025
963b276
stash Result with RawUri
katrinafyi Oct 4, 2025
9461325
Revert "stash Result with RawUri"
katrinafyi Oct 4, 2025
c1bd7ca
add help
katrinafyi Oct 4, 2025
65afcdd
add CollectResult to be less dubious
katrinafyi Oct 4, 2025
891255d
fix lints
katrinafyi Oct 4, 2025
ff87745
touch
katrinafyi Oct 4, 2025
024772a
introduce separate RequestError type. but...
katrinafyi Oct 5, 2025
3ee280e
add Status::RequestError
katrinafyi Oct 5, 2025
1ad2020
mark as error
katrinafyi Oct 5, 2025
25d32ce
remove ErrorKind::CreateRequestItem case
katrinafyi Oct 5, 2025
c7536d1
blah
katrinafyi Oct 5, 2025
d10190a
uncomment
katrinafyi Oct 5, 2025
4f74a77
restore old panic behaviour for input-source errors
katrinafyi Oct 5, 2025
3bca6c8
docs
katrinafyi Oct 5, 2025
49c550e
remove unused imports
katrinafyi Oct 5, 2025
6a007c2
fix example
katrinafyi Oct 7, 2025
d7ebbc0
clippy. includes boxing errorkind because it's big
katrinafyi Oct 7, 2025
17fad2c
propagate input loading errors too
katrinafyi Oct 7, 2025
976904a
handle is no longer fallible. add helper function
katrinafyi Oct 7, 2025
f0eda83
Delete clippy.toml
katrinafyi Oct 9, 2025
704ad98
Revert "Delete clippy.toml"
katrinafyi Oct 9, 2025
e5152cc
explain ignore-interior-mutability
katrinafyi Oct 9, 2025
6c959f3
Merge remote-tracking branch 'upstream/master' into propagate-early-e…
katrinafyi Oct 23, 2025
eef8b78
fix compilation
katrinafyi Oct 23, 2025
1a1b5df
review: add new error case for user-provided input failures, but
katrinafyi Oct 23, 2025
f331dbe
lint
katrinafyi Oct 23, 2025
5716ee9
move UserInputContent case into fn handle
katrinafyi Oct 24, 2025
300e368
try fix lint. help welcome. i can't compile this for some reason
katrinafyi Oct 24, 2025
e22f13c
add early checking for file and dir permissions (and fmt πŸ™Š)
katrinafyi Oct 24, 2025
5cc0c1b
lint
katrinafyi Oct 24, 2025
45906e8
use DirTraversal error for dir failures
katrinafyi Oct 24, 2025
2397577
update tests, including adjusting existing
katrinafyi Oct 24, 2025
802427b
remove "Skip relative URLs" from readme feature table
katrinafyi Oct 24, 2025
fe04a7c
Merge remote-tracking branch 'upstream/master' into propagate-early-e…
katrinafyi Nov 12, 2025
c594f6b
lint;
katrinafyi Nov 12, 2025
048edd6
fix invalid glob test
katrinafyi Nov 12, 2025
df91d1e
Preprocessor PathBuf
katrinafyi Nov 13, 2025
347a030
Revert "Preprocessor PathBuf"
katrinafyi Nov 13, 2025
33141c9
review comments aside from struct variant
katrinafyi Nov 13, 2025
012fd68
lint
katrinafyi Nov 13, 2025
8c34cf4
capitalise paragraph comments
katrinafyi Nov 13, 2025
e066600
comment for internal function
katrinafyi Nov 13, 2025
918e216
add light test case test_create_request_from_relative_file_path_errors
katrinafyi Nov 13, 2025
acfa23c
comment2
katrinafyi Nov 13, 2025
759b691
RequestBatch
katrinafyi Nov 16, 2025
bbbd94b
Revert "RequestBatch"
katrinafyi Nov 16, 2025
b7bb174
use Vec<Result<..>>
katrinafyi Nov 16, 2025
438a2d3
add LycheeResult to disambiguate from std Result
katrinafyi Nov 16, 2025
50e91d1
manually deduplicate valid requests but not errors.
katrinafyi Nov 16, 2025
8a6bc3f
change preprocessor tests to expect link checking errors
katrinafyi Nov 16, 2025
9a34dac
clippy
katrinafyi Nov 16, 2025
64be9a3
Merge remote-tracking branch 'upstream/master' into propagate-early-e…
katrinafyi Nov 16, 2025
6db8db4
merge main_command removal
katrinafyi Nov 16, 2025
25b1290
change to `error:` and add into_response helper function
katrinafyi Nov 16, 2025
70f793b
move request_error into separate file.
katrinafyi Nov 16, 2025
a6ba2a1
revert
katrinafyi Nov 16, 2025
ec943db
remove box (thanks to Thomas Zahner)
katrinafyi Nov 17, 2025
fdbf533
use lazylock
katrinafyi Nov 17, 2025
5aff2e6
inline errs_iter and reqs_iter with parentheses
katrinafyi Nov 17, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,6 @@ outdated information.
| Custom user agent | ![yes] | ![no] | ![no] | ![yes] | ![no] | ![yes] | ![no] | ![no] |
| Relative URLs | ![yes] | ![yes] | ![no] | ![yes] | ![yes] | ![yes] | ![yes] | ![yes] |
| Anchors/Fragments | ![yes] | ![no] | ![no] | ![no] | ![no] | ![yes] | ![yes] | ![no] |
| Skip relative URLs | ![yes] | ![no] | ![no] | ![maybe] | ![no] | ![no] | ![no] | ![no] |
| Include patterns | ![yes]️ | ![yes] | ![no] | ![yes] | ![no] | ![no] | ![no] | ![no] |
| Exclude patterns | ![yes] | ![no] | ![yes] | ![yes] | ![yes] | ![yes] | ![yes] | ![yes] |
| Handle redirects | ![yes] | ![yes] | ![yes] | ![yes] | ![yes] | ![yes] | ![yes] | ![yes] |
Expand Down
6 changes: 3 additions & 3 deletions examples/collect_links/collect_links.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
use lychee_lib::{Collector, Input, InputSource, Result};
use lychee_lib::{Collector, Input, InputSource, RequestError};
use reqwest::Url;
use std::{collections::HashSet, path::PathBuf};
use tokio_stream::StreamExt;

#[tokio::main]
async fn main() -> Result<()> {
async fn main() -> Result<(), Box<RequestError>> {
// Collect all links from the following inputs
let inputs = HashSet::from_iter([
Input::from_input_source(InputSource::RemoteUrl(Box::new(
Expand All @@ -19,7 +19,7 @@ async fn main() -> Result<()> {
.skip_ignored(false) // skip files that are ignored by git? (default=true)
.use_html5ever(false) // use html5ever for parsing? (default=false)
.collect_links(inputs) // base url or directory
.collect::<Result<Vec<_>>>()
.collect::<Result<Vec<_>, _>>()
.await?;

dbg!(links);
Expand Down
69 changes: 41 additions & 28 deletions lychee-bin/src/commands/check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ use reqwest::Url;
use tokio::sync::mpsc;
use tokio_stream::wrappers::ReceiverStream;

use lychee_lib::InputSource;
use lychee_lib::RequestError;
use lychee_lib::archive::Archive;
use lychee_lib::{Client, ErrorKind, Request, Response, Uri};
use lychee_lib::{InputSource, Result};
use lychee_lib::{ResponseBody, Status};

use crate::formatters::get_response_formatter;
Expand All @@ -27,9 +28,9 @@ use super::CommandParams;

pub(crate) async fn check<S>(
params: CommandParams<S>,
) -> Result<(ResponseStats, Arc<Cache>, ExitCode)>
) -> Result<(ResponseStats, Arc<Cache>, ExitCode), ErrorKind>
where
S: futures::Stream<Item = Result<Request>>,
S: futures::Stream<Item = Result<Request, RequestError>>,
{
// Setup
let (send_req, recv_req) = mpsc::channel(params.cfg.max_concurrency);
Expand Down Expand Up @@ -176,36 +177,36 @@ async fn suggest_archived_links(
// the show_results_task to finish
async fn send_inputs_loop<S>(
requests: S,
send_req: mpsc::Sender<Result<Request>>,
send_req: mpsc::Sender<Result<Request, RequestError>>,
bar: Option<ProgressBar>,
) -> Result<()>
) -> Result<(), ErrorKind>
where
S: futures::Stream<Item = Result<Request>>,
S: futures::Stream<Item = Result<Request, RequestError>>,
{
tokio::pin!(requests);
while let Some(request) = requests.next().await {
let request = request?;
if let Some(pb) = &bar {
pb.inc_length(1);
pb.set_message(request.to_string());
match &request {
Ok(x) => pb.set_message(x.to_string()),
Err(e) => pb.set_message(e.to_string()),
}
}
send_req
.send(Ok(request))
.await
.expect("Cannot send request");
send_req.send(request).await.expect("Cannot send request");
}
Ok(())
}

/// Reads from the request channel and updates the progress bar status
async fn progress_bar_task(
mut recv_resp: mpsc::Receiver<Response>,
mut recv_resp: mpsc::Receiver<Result<Response, ErrorKind>>,
verbose: Verbosity,
pb: Option<ProgressBar>,
formatter: Box<dyn ResponseFormatter>,
mut stats: ResponseStats,
) -> Result<(Option<ProgressBar>, ResponseStats)> {
) -> Result<(Option<ProgressBar>, ResponseStats), ErrorKind> {
while let Some(response) = recv_resp.recv().await {
let response = response?;
show_progress(
&mut io::stderr(),
pb.as_ref(),
Expand All @@ -232,8 +233,8 @@ fn init_progress_bar(initial_message: &'static str) -> ProgressBar {
}

async fn request_channel_task(
recv_req: mpsc::Receiver<Result<Request>>,
send_resp: mpsc::Sender<Response>,
recv_req: mpsc::Receiver<Result<Request, RequestError>>,
send_resp: mpsc::Sender<Result<Response, ErrorKind>>,
max_concurrency: usize,
client: Client,
cache: Arc<Cache>,
Expand All @@ -243,8 +244,7 @@ async fn request_channel_task(
StreamExt::for_each_concurrent(
ReceiverStream::new(recv_req),
max_concurrency,
|request: Result<Request>| async {
let request = request.expect("cannot read request");
|request: Result<Request, RequestError>| async {
let response = handle(
&client,
cache.clone(),
Expand Down Expand Up @@ -277,19 +277,32 @@ async fn check_url(client: &Client, request: Request) -> Response {
Response::new(
uri.clone(),
Status::Error(ErrorKind::InvalidURI(uri.clone())),
source,
source.into(),
)
})
}

/// Handle a single request
///
/// # Errors
///
/// An Err is returned if and only if there was an error while loading
/// a *user-provided* input argument. Other errors, including errors in
/// link resolution and in resolved inputs, will be returned as Ok with
/// a failed response.
async fn handle(
client: &Client,
cache: Arc<Cache>,
cache_exclude_status: HashSet<u16>,
request: Request,
request: Result<Request, RequestError>,
accept: HashSet<u16>,
) -> Response {
) -> Result<Response, ErrorKind> {
// Note that the RequestError cases bypass the cache.
let request = match request {
Ok(x) => x,
Err(e) => return e.into_response(),
};

let uri = request.uri.clone();
if let Some(v) = cache.get(&uri) {
// Found a cached request
Expand All @@ -304,7 +317,7 @@ async fn handle(
// code.
Status::from_cache_status(v.value().status, &accept)
};
return Response::new(uri.clone(), status, request.source);
return Ok(Response::new(uri.clone(), status, request.source.into()));
}

// Request was not cached; run a normal check
Expand All @@ -318,11 +331,11 @@ async fn handle(
// - Skip caching links for which the status code has been explicitly excluded from the cache.
let status = response.status();
if ignore_cache(&uri, status, &cache_exclude_status) {
return response;
return Ok(response);
}

cache.insert(uri, status.into());
response
Ok(response)
}

/// Returns `true` if the response should be ignored in the cache.
Expand Down Expand Up @@ -351,7 +364,7 @@ fn show_progress(
response: &Response,
formatter: &dyn ResponseFormatter,
verbose: &Verbosity,
) -> Result<()> {
) -> Result<(), ErrorKind> {
// In case the log level is set to info, we want to show the detailed
// response output. Otherwise, we only show the essential information
// (typically the status code and the URL, but this is dependent on the
Expand Down Expand Up @@ -402,7 +415,7 @@ mod tests {
use crate::{formatters::get_response_formatter, options};
use http::StatusCode;
use log::info;
use lychee_lib::{CacheStatus, ClientBuilder, ErrorKind, ResolvedInputSource, Uri};
use lychee_lib::{CacheStatus, ClientBuilder, ErrorKind, Uri};

use super::*;

Expand All @@ -412,7 +425,7 @@ mod tests {
let response = Response::new(
Uri::try_from("http://127.0.0.1").unwrap(),
Status::Cached(CacheStatus::Ok(200)),
ResolvedInputSource::Stdin,
InputSource::Stdin,
);
let formatter = get_response_formatter(&options::OutputMode::Plain);
show_progress(
Expand All @@ -434,7 +447,7 @@ mod tests {
let response = Response::new(
Uri::try_from("http://127.0.0.1").unwrap(),
Status::Cached(CacheStatus::Ok(200)),
ResolvedInputSource::Stdin,
InputSource::Stdin,
);
let formatter = get_response_formatter(&options::OutputMode::Plain);
show_progress(
Expand Down
19 changes: 15 additions & 4 deletions lychee-bin/src/commands/dump.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use log::error;
use log::warn;
use lychee_lib::Request;
use lychee_lib::Result;
use lychee_lib::RequestError;
use std::fs;
use std::io::{self, Write};
use tokio_stream::StreamExt;
Expand All @@ -11,9 +12,9 @@ use crate::verbosity::Verbosity;
use super::CommandParams;

/// Dump all detected links to stdout without checking them
pub(crate) async fn dump<S>(params: CommandParams<S>) -> Result<ExitCode>
pub(crate) async fn dump<S>(params: CommandParams<S>) -> lychee_lib::Result<ExitCode>
where
S: futures::Stream<Item = Result<Request>>,
S: futures::Stream<Item = Result<Request, RequestError>>,
{
let requests = params.requests;
tokio::pin!(requests);
Expand All @@ -25,7 +26,17 @@ where
let mut writer = super::create_writer(params.cfg.output)?;

while let Some(request) = requests.next().await {
let mut request = request?;
if let Err(e @ RequestError::UserInputContent { .. }) = request {
return Err(e.into_error());
}

let mut request = match request {
Ok(x) => x,
Err(e) => {
warn!("{e}");
continue;
}
};

// Apply URI remappings (if any)
params.client.remap(&mut request.uri)?;
Expand Down
6 changes: 3 additions & 3 deletions lychee-bin/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ use std::sync::Arc;

use crate::cache::Cache;
use crate::options::Config;
use lychee_lib::Result;
use lychee_lib::RequestError;
use lychee_lib::{Client, Request};

/// Parameters passed to every command
pub(crate) struct CommandParams<S: futures::Stream<Item = Result<Request>>> {
pub(crate) struct CommandParams<S: futures::Stream<Item = Result<Request, RequestError>>> {
pub(crate) client: Client,
pub(crate) cache: Arc<Cache>,
pub(crate) requests: S,
Expand All @@ -30,7 +30,7 @@ pub(crate) struct CommandParams<S: futures::Stream<Item = Result<Request>>> {
/// # Errors
///
/// Returns an error if the output file cannot be opened.
fn create_writer(output: Option<PathBuf>) -> Result<Box<dyn Write>> {
fn create_writer(output: Option<PathBuf>) -> lychee_lib::Result<Box<dyn Write>> {
Ok(match output {
Some(path) => Box::new(fs::OpenOptions::new().append(true).open(path)?),
None => Box::new(io::stdout().lock()),
Expand Down
4 changes: 3 additions & 1 deletion lychee-bin/src/formatters/response/color.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ impl ColorFormatter {
| Status::Unsupported(_)
| Status::Cached(CacheStatus::Excluded | CacheStatus::Unsupported) => &DIM,
Status::UnknownStatusCode(_) | Status::Timeout(_) => &YELLOW,
Status::Error(_) | Status::Cached(CacheStatus::Error(_)) => &PINK,
Status::Error(_) | Status::RequestError(_) | Status::Cached(CacheStatus::Error(_)) => {
&PINK
}
}
}

Expand Down
4 changes: 3 additions & 1 deletion lychee-bin/src/formatters/response/emoji.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ impl EmojiFormatter {
| Status::Cached(CacheStatus::Excluded | CacheStatus::Unsupported) => "🚫",
Status::Redirected(_, _) => "β†ͺ️",
Status::UnknownStatusCode(_) | Status::Timeout(_) => "⚠️",
Status::Error(_) | Status::Cached(CacheStatus::Error(_)) => "❌",
Status::Error(_) | Status::RequestError(_) | Status::Cached(CacheStatus::Error(_)) => {
"❌"
}
}
}
}
Expand Down
9 changes: 3 additions & 6 deletions lychee-bin/src/formatters/stats/markdown.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,10 +163,7 @@ impl StatsFormatter for Markdown {
#[cfg(test)]
mod tests {
use http::StatusCode;
use lychee_lib::{
CacheStatus, InputSource, Redirects, ResolvedInputSource, Response, ResponseBody, Status,
Uri,
};
use lychee_lib::{CacheStatus, InputSource, Redirects, Response, ResponseBody, Status, Uri};
use reqwest::Url;

use crate::formatters::suggestion::Suggestion;
Expand Down Expand Up @@ -228,7 +225,7 @@ mod tests {
stats.add(Response::new(
Uri::try_from("http://127.0.0.1").unwrap(),
Status::Cached(CacheStatus::Error(Some(404))),
ResolvedInputSource::Stdin,
InputSource::Stdin,
));

// Add suggestion
Expand All @@ -252,7 +249,7 @@ mod tests {
Url::parse("http://redirected.dev").unwrap(),
]),
),
ResolvedInputSource::Stdin,
InputSource::Stdin,
));

let summary = MarkdownResponseStats(stats);
Expand Down
20 changes: 7 additions & 13 deletions lychee-bin/src/formatters/stats/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,14 @@ where
mod tests {
use super::*;

use lychee_lib::{ErrorKind, ResolvedInputSource, Response, Status, Uri};
use lychee_lib::{ErrorKind, Response, Status, Uri};
use url::Url;

fn make_test_url(url: &str) -> Url {
Url::parse(url).expect("Expected valid Website URI")
}

fn make_test_response(url_str: &str, source: ResolvedInputSource) -> Response {
fn make_test_response(url_str: &str, source: InputSource) -> Response {
let uri = Uri::from(make_test_url(url_str));

Response::new(uri, Status::Error(ErrorKind::EmptyUrl), source)
Expand All @@ -74,18 +74,12 @@ mod tests {

// Sorted list of test sources
let test_sources = vec![
ResolvedInputSource::RemoteUrl(Box::new(make_test_url("https://example.com/404"))),
ResolvedInputSource::RemoteUrl(Box::new(make_test_url("https://example.com/home"))),
ResolvedInputSource::RemoteUrl(Box::new(make_test_url("https://example.com/page/1"))),
ResolvedInputSource::RemoteUrl(Box::new(make_test_url("https://example.com/page/10"))),
InputSource::RemoteUrl(Box::new(make_test_url("https://example.com/404"))),
InputSource::RemoteUrl(Box::new(make_test_url("https://example.com/home"))),
InputSource::RemoteUrl(Box::new(make_test_url("https://example.com/page/1"))),
InputSource::RemoteUrl(Box::new(make_test_url("https://example.com/page/10"))),
];

let unresolved_test_sources: Vec<InputSource> = test_sources
.iter()
.map(Clone::clone)
.map(Into::<InputSource>::into)
.collect();

// Sorted list of test responses
let test_response_urls = vec![
"https://example.com/",
Expand All @@ -110,7 +104,7 @@ mod tests {
.collect();

// Check that the input sources are sorted
assert_eq!(unresolved_test_sources, sorted_sources);
assert_eq!(test_sources, sorted_sources);

// Check that the responses are sorted
for (_, response_bodies) in sorted_errors {
Expand Down
Loading