22import glob
33import re
44import os
5+ import socket
56import urllib .request , urllib .error , urllib .parse
67import urllib3
8+ import urllib3 .util .connection
79import requests
810import sys
911import time
@@ -26,15 +28,29 @@ def retry_sleep(retry: int) -> None:
2628 time .sleep (backoff + random .uniform (0 , 15 )) # 0〜15秒のジッタ
2729
2830def check_url (url : str , retry : int = MAX_OUTER_LINK_RETRY ) -> tuple [bool , str ]:
31+ # 試行を重ねるごとにアクセス方法を変えて (別経路で) 再試行する。1回目で失敗する
32+ # 理由は「リンク切れ」以外に「HEAD非対応・低速サーバ」「一時的なネットワーク不調」
33+ # などがあり、方法を変えると通ることが多いため。
34+ # method : 試行ごとにHEADとGETを交互に切り替える (偶数回目=HEAD, 奇数回目=GET)。
35+ # HEADに未対応・不安定なサーバはGETで、GETが重いだけのサーバはHEADで通る。
36+ # (例: www.dre.vanderbilt.edu はHEADが失敗しGETは通るが応答が非常に遅い)。
37+ # timeout : 試行を重ねるごとに増やし、低速サーバに徐々に猶予を与える。
38+ # IPv4固定はcheck()側でallowed_gai_familyにより行う (下記コメント参照)。
39+ attempt = MAX_OUTER_LINK_RETRY - retry # 0, 1, 2, 3
40+ method = "HEAD" if attempt % 2 == 0 else "GET"
41+ # 2回目 (最初のGET) の時点で低速サーバを取りこぼさない余裕を持たせる。
42+ # 例: www.dre.vanderbilt.edu はGETに実測で最大120秒近くかかるため、2回目=240秒とする。
43+ timeout = 120.0 * (attempt + 1 ) # 120, 240, 360, 480 秒
2944 try :
3045 headers = {'User-agent' : 'Mozilla/5.0' }
31- # パフォーマンスのため本文は取得せずHEADで確認する。
32- # ただしallow_redirects=Trueでリダイレクトはrequestsに追わせ、最終的な
33- # ステータスで判定する (手動でres.urlを辿る再帰をやめ、リトライ回数が
34- # 途中でリセットされる問題を避ける)。
35- res = requests .head (url , headers = headers , verify = False , timeout = 60.0 ,
36- allow_redirects = True )
37- return res .status_code != 404 , str (res .status_code )
46+ # stream=Trueで本文はダウンロードせず、最終的なステータスで判定する。
47+ # allow_redirects=Trueでリダイレクトはrequestsに追わせる (手動でres.urlを
48+ # 辿る再帰をやめ、リトライ回数が途中でリセットされる問題を避ける)。
49+ res = requests .request (method , url , headers = headers , verify = False ,
50+ timeout = timeout , allow_redirects = True , stream = True )
51+ status = res .status_code
52+ res .close ()
53+ return status != 404 , str (status )
3854 except requests .exceptions .TooManyRedirects :
3955 # リダイレクトループ (http↔httpsを行き来する等)。ブラウザではHSTS等で
4056 # 到達できることが多く、リトライしても解消しないため、存在するものとして
@@ -183,6 +199,14 @@ def check(check_inner_link: bool, check_outer_link: bool, url: str) -> bool:
183199 found_error = True
184200
185201 if check_outer_link :
202+ # GitHub-hosted runnerはIPv6アウトバウンド経路を持たない
203+ # (actions/runner-images#668。runner上では ping6 が "Network is unreachable")。
204+ # 接続先がデュアルスタック (A+AAAA) の場合、IPv6アドレスへの接続が
205+ # [Errno 101] Network is unreachable となり、IPv4へのフォールバックが
206+ # 間に合わないと断続的に失敗する (例: www.decadent.org.uk)。
207+ # runnerで実際に到達できるIPv4に固定して、この経路起因の失敗を避ける。
208+ urllib3 .util .connection .allowed_gai_family = lambda : socket .AF_INET
209+
186210 if len (url ) > 0 :
187211 outer_link_dict [url ] = ""
188212
0 commit comments