@@ -661,7 +661,7 @@ def __init__(self, extensions: List[str]):
661661 self .extensions = {ext .lower () for ext in EXTENSIONS }
662662 self .css_js_pattern = re .compile (r"\.(css|js)($|\?|#)" )
663663
664- def fetch_links_from_url (self , url : str , timeout : int = DEFAULT_HTTP_TIMEOUT ) -> List [str ]:
664+ def fetch_links_from_url (self , url : str , timeout : int = DEFAULT_HTTP_TIMEOUT ) -> Tuple [ str , List [str ] ]:
665665 """
666666 Fetch all links from a given URL.
667667
@@ -670,14 +670,19 @@ def fetch_links_from_url(self, url: str, timeout: int = DEFAULT_HTTP_TIMEOUT) ->
670670 timeout: Request timeout in seconds
671671
672672 Returns:
673- List of links found on the page
673+ Tuple of (effective URL after redirects, links found on the page).
674+ The effective URL must be used as the base for resolving relative
675+ links, since e.g. GitHub Pages 301-redirects '/repo' to '/repo/'.
674676 """
675677 try :
676678 request = urllib .request .Request (url , headers = {'User-Agent' : USER_AGENT })
677679 with urllib .request .urlopen (request , timeout = timeout ) as response :
680+ # Final URL after any redirects; relative links resolve against it.
681+ effective_url = response .geturl () or url
682+
678683 content_type = response .headers .get ('Content-Type' , '' ).split (';' )[0 ]
679684 if 'text' not in content_type and 'application' not in content_type :
680- return []
685+ return effective_url , []
681686
682687 raw_data = response .read ()
683688
@@ -688,7 +693,7 @@ def fetch_links_from_url(self, url: str, timeout: int = DEFAULT_HTTP_TIMEOUT) ->
688693 data = raw_data .decode ('latin-1' )
689694 except UnicodeDecodeError :
690695 Logger .warning (f"Unable to decode content from { url } " )
691- return []
696+ return effective_url , []
692697
693698 parser = LinkParser ()
694699 parser .feed (data )
@@ -701,20 +706,20 @@ def fetch_links_from_url(self, url: str, timeout: int = DEFAULT_HTTP_TIMEOUT) ->
701706 if not link or link .strip () == '' :
702707 continue
703708 filtered_links .append (link )
704- return filtered_links
709+ return effective_url , filtered_links
705710
706711 except urllib .error .URLError as e :
707712 if url .startswith ("mailto:" ):
708713 Logger .info (f"Found mailto link { url } " )
709714 else :
710715 Logger .error (f"Unable to open { url } Reason: { e } " )
711- return []
716+ return url , []
712717 except urllib .error .HTTPError as e :
713718 Logger .error (f"HTTP Error for URL { url } Reason: { e .code } - { e .reason } " )
714- return []
719+ return url , []
715720 except Exception as e :
716721 Logger .error (f"Unexpected error fetching { url } Reason: { e } " )
717- return []
722+ return url , []
718723
719724 def is_valid_file_link (self , link : str ) -> bool :
720725 """
@@ -883,12 +888,12 @@ def process_url(self, task: ScrapingTask, task_queue: queue.Queue) -> None:
883888
884889 self .rate_limiter .wait ()
885890
886- links = self .scraper .fetch_links_from_url (task .url )
891+ base_url , links = self .scraper .fetch_links_from_url (task .url )
887892
888893 file_links = []
889894 for link in links :
890895 if self .scraper .is_valid_file_link (link ):
891- absolute_url = urljoin (task . url , link )
896+ absolute_url = urljoin (base_url , link )
892897 file_links .append (absolute_url )
893898
894899 if self .download_dir and not self .scan :
@@ -910,7 +915,7 @@ def process_url(self, task: ScrapingTask, task_queue: queue.Queue) -> None:
910915 if not link .startswith (('http://' , 'https://' , '/' )):
911916 continue
912917
913- absolute_link = urljoin (task . url , link )
918+ absolute_link = urljoin (base_url , link )
914919 parsed_link = urlparse (absolute_link )
915920
916921 # Validate that it's an HTTP/HTTPS URL
0 commit comments