From 79567b3e18e469c064d37fa65dd207abfd65d1eb Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Tue, 27 Oct 2026 13:15:36 +0100 Subject: [PATCH] fix: a loop through the site's absolute URL lists each address once MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The loop and chain path of a redirect notice appended the final target when it was an absolute URL of the site itself, so a loop read "/a → /b → /a → https://example.test/a". The site URL now counts as internal, as it does when the chain is followed. --- classes/issues.go | 2 +- classes/redirect_graph.go | 14 ++++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/classes/issues.go b/classes/issues.go index 46748b2..bcddcee 100644 --- a/classes/issues.go +++ b/classes/issues.go @@ -170,7 +170,7 @@ func (s *Service) Issues(ctx context.Context) ([]Issue, error) { continue } out = append(out, Issue{Kind: IssueRedirectChain, URL: r.Source, Tool: toolUpdateRedirect, Details: map[string]any{ - "redirect_id": r.ID, "hops": len(ch.Hops), "final": displayURL(site, ch.Final), "path": displayPath(r, ch), + "redirect_id": r.ID, "hops": len(ch.Hops), "final": displayURL(site, ch.Final), "path": displayPath(site, r, ch), }}) } diff --git a/classes/redirect_graph.go b/classes/redirect_graph.go index c253cbc..14b790f 100644 --- a/classes/redirect_graph.go +++ b/classes/redirect_graph.go @@ -203,11 +203,11 @@ func checkRedirectRules(site string, rules []models.Redirect, r models.Redirect) return check, err } if ch.Loop { - check.Loop, check.Path = true, displayPath(r, ch) + check.Loop, check.Path = true, displayPath(site, r, ch) return check, nil } if len(ch.Hops) > 1 && ch.Hops[0].Rule.ID == r.ID { - check.Chain, check.Path, check.Hops = true, displayPath(r, ch), len(ch.Hops) + check.Chain, check.Path, check.Hops = true, displayPath(site, r, ch), len(ch.Hops) check.Source, check.Final = r.Source, displayURL(site, ch.Final) return check, nil } @@ -233,15 +233,17 @@ func checkRedirectRules(site string, rules []models.Redirect, r models.Redirect) return check, err } check.Chain, check.Hops = true, len(ch.Hops)+1 - check.Path = append([]string{other.Source}, displayPath(r, ch)...) + check.Path = append([]string{other.Source}, displayPath(site, r, ch)...) check.Source, check.Final = other.Source, displayURL(site, ch.Final) return check, nil } return check, nil } -// displayPath lists a chain's URLs for a notice, the source of r first. -func displayPath(r models.Redirect, ch Chain) []string { +// displayPath lists a chain's URLs for a notice, the source of r first. A +// final target outside the site ends the list; one on the site (a path or +// the site's absolute URL) is already its last visited path. +func displayPath(site string, r models.Redirect, ch Chain) []string { out := make([]string, 0, len(ch.Visited)) for i, v := range ch.Visited { if i == 0 && r.Match != models.RedirectExact && len(ch.Hops) > 0 && ch.Hops[0].Rule.ID == r.ID { @@ -251,7 +253,7 @@ func displayPath(r models.Redirect, ch Chain) []string { } if len(ch.Hops) > 0 && ch.Final != "" { if last := out[len(out)-1]; last != ch.Final { - if _, _, internal := internalTarget("", ch.Final); !internal { + if _, _, internal := internalTarget(site, ch.Final); !internal { out = append(out, ch.Final) } }