Skip to content

Split up and refactor Status enum #2097

Open
Open

Description

@katrinafyi

I feel like the Status enum has grown beyond its original purpose and has become too big and complicated.

The main problem is that it mixes up two things which I think should be separated: (1) the actual outcome of the request, and (2) how that outcome should be interpreted. Another problem is that the enum has grown data fields in the form of the Redirected variant. This history information should really be separate from the actual status and stored in a struct.

The presence of functions like Status::is_success, is_error, etc shows that the shape of the enum does not match the shape of the information you want it to store - ideally, these would all be match statements on a flat enum type. Functions like is_success_ignoring_timeouts are also desiring more flexibility than the current types can provide.

I've sketched out a rough idea for splitting it up into two parts:

  • Outcome, the objective outcome of the check, and
  • Status, the interpretation of the Outcome after taking into account --accept, etc.
/// Outcome of the checker, without any presumption of success or failure.
pub enum Outcome {
    Website {
        result: Result<StatusCode, reqwest::Error>,
        redirects: Redirects,
        fragment_check: Option<bool>,
    },
    Mail {
        result: mailify_lib::CheckResult,
    },
    File {
        result: Result<Metadata, io::Error>,
        applied_fallback_extension: Option<String>,
        applied_index_file: Option<String>,
        fragment_check: Option<bool>,
    },
    RequestError(RequestError),
    Unsupported(ErrorKind),
}

/// A check which was performed or skipped. This includes excluded and cached,
/// unlike Outcome which is for checks which actually run.
pub enum CheckResult {
    Checked(Outcome),   // we actually performed a check
    Skipped(SkipReason), // Excluded, Unsupported, etc.
    CachedSuccess, // since recently, we only cache successes
}

/// Response to a lychee Request. Bundles a CheckResult along with metadata
/// (like source location) and pre-check transformations like remaps.
pub struct Response {
    check_result: CheckResult,
    request: Request, // <-- remap_history, span is inside Request
    input_source: crate::InputSource,
    response_body: crate::ResponseBody,
}

/// An interpretation of an [`Outcome`], taking into account user-provided
/// configuration. Corresponds to counters in the CLI output.
pub enum Status {
    Ok,
    Error,
    Timeout,
    Redirected,
    Cached,
    Excluded,
    Unsupported,
    Unknown,
}

impl Status {
    /// For example.
    pub fn from_response(
        response: Response,
        accept: StatusCodeSelector,
        accept_timeouts: bool,
        other_things: MoreOptions,
    ) -> Self {
        Self::Ok
    }
}

Obviously this is all open to change. Let me know what you think.

Here's a comment where I mentioned this before: #2063 (comment)

Activity

  1. mre commented on Mar 25, 2026

    @mre
    Member

    Yes, I fully agree. The status enum does too much.

    Minor remark: I like the term Outcome but what do you think about Verdict instead of Conclusion? Then it would read Verdict::Ok, Verdict::Error. Alternatively, Resolution?

  2. katrinafyi commented on Mar 25, 2026

    @katrinafyi
    MemberAuthor

    Verdict sounds good to me! I don't like Resolution so much because we already have quite a few resolvers.

    The verdict enum could even inherit the name Status, after it's been slimmed down. I'm not too fussy about the names.

  3. mre commented on Mar 25, 2026

    @mre
    Member

    The verdict enum could even inherit the name Status, after it's been slimmed down. I'm not too fussy about the names.

    Ah, I haven't thought of that. My vote goes to that. Good idea. 👍

  4. added theissue type on Mar 26, 2026
  5. mre commented on Apr 4, 2026

    @mre
    Member

    I just now discover how much your work will contribute to recursion support as well, which is awesome!

    If this refactor lands, recursion gets structured body access without bolting anything onto the existing Response type.
    Instead, the Outcome::Website variant could naturally carry the response body:

    Outcome::Website {
        result: Result<StatusCode, reqwest::Error>,
        redirects: Redirects,
        fragment_check: Option<bool>,
        body: Option<String>,
    }

    This way, recursion gets body access "for free." I.e. we don't need a new field bolted onto the Response type (so no API pollution because the new design accommodates it). The collect_responses handler would pattern-match on Outcome::Website, extract the body, and feed it to the Extractor. This is the cleanest option for recursion support as far as I can see, so I'd very much like to see this landing soon.

  6. mre commented on Apr 4, 2026

    @mre
    Member

    One more observation is that Excluded and Unsupported are not on the same "logical level" as the other variants in Outcome.

    They're not really "outcomes of a check" like the rest but rather reasons a check didn't happen. We could model them separately from Outcome:

    pub enum CheckResult {
        Checked(Outcome),   // we actually performed a check
        Skipped(SkipReason), // Excluded, Unsupported, etc.
    }

    The downside is that we'd have to go through one more level of indirection when we want to get to the outcome, but that might even be a good thing because it could simplify some of the status logic.

  7. mre commented on Apr 4, 2026

    @mre
    Member

    Outcome::Cached requires some more thought.

    A cache hit means we didn't perform a check. We retrieved a previous result/outcome instead. The cached value is itself an Outcome (or at least a lossy summary of one).

    If the cache stored (Outcome, Status) pairs instead, a cache hit would return these directly, never constructing a new Outcome. This means Outcome doesn't need a Cached variant at all.

    I believe this is cleaner conceptually (caching is orthogonal to what happened), but it requires rethinking how the cache integrates. The current proposal keeps the current cache architecture mostly intact, so we could keep it for now, but I still wanted to put that up for discussion.

  8. mre commented on Apr 4, 2026

    @mre
    Member

    We might want Unsupported and Unknown as separate variants inside Status:

    pub enum Status {
        Ok,
        Error,
        Timeout,
        Redirected,
        Cached,
        Excluded,
        Unsupported,
        Unknown,
    }

    Currently:

    • is_unsupported() is used in ignore_cache() (in check.rs) and in formatters (dimmed color)
    • is_unknown() is used in ignore_cache() (yellow color, separate counter)
    • The stats formatter counts unsupported and unknown as distinct buckets

    If we fold these into Excluded or Error, we lose the ability to display them distinctly.

  9. katrinafyi commented on Apr 7, 2026

    @katrinafyi
    MemberAuthor

    I think I agree with all of your recent messages (sorry for the late reply).

    My plan was that the *Checker types would return Outcome. Since excluding happens before the checker, the exclude case shouldn't be in there. With caching, we could add that as a case into your CheckResult. Since we don't cache errors anymore, it is very simple and that case shouldn't need any extra data at all.

    pub enum CheckResult {
        Checked(Outcome),   // we actually performed a check
        Skipped(SkipReason), // Excluded, Unsupported, etc.
        CachedSuccess, // since recently, we only cache successes
    }

    Alternatively, it is interesting to use a "lossy summary" of Outcome as a cache value, but I think that's not necessary unless we want to start storing more information in the cache. For example, we might store whether the cache entry was created with include-fragments. But I think this is further down the line and we just keep it simple for now.

    Finally, about the Status enum, yeah, it was always my intention that this enum line up with whatever counters we want to show. Hopefully, removing all the complex Status cases makes this much easier to manage in future.

  10. mre commented on Apr 7, 2026

    @mre
    Member

    Cool, okay.

    With caching, we could add that as a case into your CheckResult.

    I like that!

    Would you like to post an updated version of the types?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions