@@ -2,11 +2,13 @@ package name.valery1707.problem
22
33import java.net.URI
44import java.net.http.HttpClient
5+ import java.net.http.HttpHeaders
56import java.net.http.HttpRequest
67import java.net.http.HttpResponse
78import java.nio.file.Path
89import java.time.Duration
910import java.time.Instant
11+ import java.time.format.DateTimeFormatter
1012import java.time.temporal.ChronoField.NANO_OF_SECOND
1113import kotlin.io.path.ExperimentalPathApi
1214import kotlin.io.path.PathWalkOption
@@ -115,19 +117,7 @@ class LinkChecker(private val root: Path) {
115117 // Rate limiting: wait and retry
116118 in HTTP_RATE_LIMIT -> {
117119 val now = Instant .now()
118- val await = response.headers()
119-
120- // todo Extract to method
121- // https://docs.github.com/en/rest/overview/resources-in-the-rest-api?apiVersion=2022-11-28#checking-your-rate-limit-status
122- .map()[" x-ratelimit-reset" ]
123- ?.asSequence()
124- ?.map(String ::toLong)?.map(Instant ::ofEpochSecond)
125- ?.map { Duration .between(now.with (NANO_OF_SECOND , 0 ), it) }
126- ?.map(Duration ::toMillis)
127- ?.filter { it >= 0 }
128- ?.firstOrNull()
129-
130- ? : 500
120+ val await = response.headers().rateLimitAwait(now) ? : 500
131121
132122 logger.debug(" Await: $await ms" )
133123 Thread .sleep(await)
@@ -143,6 +133,40 @@ class LinkChecker(private val root: Path) {
143133 }
144134
145135 private val HTTP_REDIRECT = setOf (301 , 302 , 307 , 308 )
146- private val HTTP_RATE_LIMIT = setOf (403 )
136+ private val HTTP_RATE_LIMIT = setOf (403 , 429 )
137+
138+ private fun HttpHeaders.rateLimitAwait (now : Instant ): Long? {
139+ val map = map()
140+ return HTTP_RATE_LIMIT_EXTRACTORS
141+ .flatMap { map[it.key]?.asSequence()?.map { v -> it.value(v.trim(), now) } ? : emptySequence() }
142+ .filterNotNull()
143+ .firstOrNull { it >= 0 }
144+ }
145+
146+ private val HTTP_RATE_LIMIT_EXTRACTORS : Map <String , (String , Instant ) - > Long? > = mapOf (
147+ // https://docs.github.com/en/rest/overview/resources-in-the-rest-api?apiVersion=2022-11-28#checking-your-rate-limit-status
148+ " x-ratelimit-reset" to { value, now ->
149+ value
150+ .toLong()
151+ .let (Instant ::ofEpochSecond)
152+ .let { Duration .between(now.with (NANO_OF_SECOND , 0 ), it) }
153+ .let (Duration ::toMillis)
154+ },
155+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
156+ " Retry-After" to { value, now ->
157+ if (value.isDigit()) value.toLong()
158+ else HTTP_DATE_FORMAT
159+ .parse(value, Instant ::from)
160+ .let { Duration .between(now.with (NANO_OF_SECOND , 0 ), it) }
161+ .let (Duration ::toMillis)
162+ },
163+ )
164+
165+ /* *
166+ * @see <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Date">Specification</a>
167+ */
168+ internal val HTTP_DATE_FORMAT = DateTimeFormatter .RFC_1123_DATE_TIME
169+
170+ private fun String.isDigit (): Boolean = this .all { it.isDigit() }
147171 }
148172}
0 commit comments