///|
test "a rate-limited POST waits as the server asks" {
let rule : @retry.RetryRule = {
..@retry.RetryRule::never(),
max_retries: 3,
statuses: [{ statuses: [Code(429)], methods: Any, }],
retry_after: {
source: Header,
cap: StopAbove(60000),
zero_means_backoff: false,
},
}
let post = @http.Request::post("https://api.example/messages").head()
let limited = @retry.Response(
status=429,
headers=@http.Headers::of([("Retry-After", "2")]),
)
let decide = attempt => {
rule.decide(
limited,
post,
attempt~,
random=0.5,
idempotent=false,
replayable=true,
)
}
assert_eq(decide(0), Wait(2000))
assert_eq(decide(3), Stop(Exhausted))
assert_eq(@retry.parse_retry_after("1.5"), Some(1500))
}pub(all) enum AttemptOutcome {
Response(status~ : Int, headers~ : Headers)
Failure(TransportError)
} derive(Debug)test {
let backoff : @retry.Backoff = {
base_ms: 500,
max_ms: 30000,
jitter: Proportional(lo=800, hi=1200),
}
assert_eq(backoff.delay_ms(2, 0.5), 2000)
assert_eq(backoff.delay_ms(0, 0.0), 400)
}pub(all) struct RetryAfter {
source : RetryAfterSource
cap : RetryAfterCap
zero_means_backoff : Bool
} derive(Eq, Debug)pub(all) struct RetryRule {
max_retries : Int
statuses : Array[StatusRule]
sent_failures : Methods
unsent_failures : Bool
override_header : String?
retry_after : RetryAfter
backoff : Backoff
} derive(Eq, Debug)test {
let rule : @retry.RetryRule = {
..@retry.RetryRule::never(),
max_retries: 2,
statuses: [{ statuses: [Code(429), Range(500, 599)], methods: Idempotent, }],
backoff: { base_ms: 500, max_ms: 8000, jitter: NoJitter, },
}
let get = @http.Request::get("/items")
let busy = @retry.Response(status=503, headers=@http.Headers::empty())
assert_eq(
rule.decide(
busy,
get.head(),
attempt=1,
random=0.0,
idempotent=true,
replayable=true,
),
Wait(1000),
)
assert_eq(
rule.decide(
busy,
get.head(),
attempt=2,
random=0.0,
idempotent=true,
replayable=true,
),
Stop(Exhausted),
)
}fn RetryRule::decide(self : RetryRule, outcome : AttemptOutcome, request : RequestHead, attempt~ : Int, random~ : Double, now_unix_ms? : Int64, remaining_ms? : Int64, server_delay_ms? : Int, idempotent~ : Bool, replayable~ : Bool) -> Decisiontest {
let rule : @retry.RetryRule = {
..@retry.RetryRule::never(),
max_retries: 2,
statuses: [{ statuses: [Code(429)], methods: Any, }],
retry_after: {
source: Header,
cap: StopAbove(60000),
zero_means_backoff: false,
},
}
let post = @http.Request::post("/messages")
let headers = @http.Headers::of([("Retry-After", "3")])
let limited = @retry.Response(status=429, headers~)
assert_eq(
rule.decide(
limited,
post.head(),
attempt=0,
random=0.5,
idempotent=false,
replayable=true,
),
Wait(3000),
)
assert_eq(
rule.decide(
limited,
post.head(),
attempt=0,
random=0.5,
remaining_ms=2000,
idempotent=false,
replayable=true,
),
Stop(Budget(3000)),
)
}test {
let rule : @retry.StatusRule = {
statuses: [Code(429), Range(500, 599)],
methods: Any,
}
assert_eq(rule.statuses.length(), 2)
}fn parse_retry_after(value : String, now_unix_ms? : Int64) -> Int?test {
assert_eq(@retry.parse_retry_after("1.2349"), Some(1234))
}Install
Download zipHTTP for MoonBit in the style of sttp, on top of gaato/http: a client with retries, rate limits and deadlines, server-sent events, multipart, cookies, endpoints and OpenAPI. Runs on every backend.