Skip to content

Commit bda4ef2

Browse files
authored
Don't retry on internal exceptions (#1110)
* Don't retry on internal exceptions * Make isrecoverable opt-in again
1 parent cefae7b commit bda4ef2

File tree

3 files changed

+112
-11
lines changed

3 files changed

+112
-11
lines changed

src/clientlayers/RetryRequest.jl

+8-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module RetryRequest
22

3-
using Sockets, LoggingExtras, MbedTLS, OpenSSL
3+
using Sockets, LoggingExtras, MbedTLS, OpenSSL, ExceptionUnwrapping
44
using ..IOExtras, ..Messages, ..Strings, ..ExceptionRequest, ..Exceptions
55

66
export retrylayer
@@ -76,9 +76,10 @@ function retrylayer(handler)
7676
end
7777
end
7878

79-
isrecoverable(ex) = true
80-
isrecoverable(ex::CapturedException) = isrecoverable(ex.ex)
81-
isrecoverable(ex::ConnectError) = isrecoverable(ex.error)
79+
isrecoverable(ex) = is_wrapped_exception(ex) ? isrecoverable(unwrap_exception(ex)) : false
80+
isrecoverable(::Union{Base.EOFError, Base.IOError, MbedTLS.MbedException, OpenSSL.OpenSSLError}) = true
81+
isrecoverable(ex::ArgumentError) = ex.msg == "stream is closed or unusable"
82+
isrecoverable(ex::CompositeException) = all(isrecoverable, ex.exceptions)
8283
# Treat all DNS errors except `EAI_AGAIN`` as non-recoverable
8384
# Ref: https://github.com/JuliaLang/julia/blob/ec8df3da3597d0acd503ff85ac84a5f8f73f625b/stdlib/Sockets/src/addrinfo.jl#L108-L112
8485
isrecoverable(ex::Sockets.DNSError) = (ex.code == Base.UV_EAI_AGAIN)
@@ -92,9 +93,11 @@ end
9293

9394
function no_retry_reason(ex, req)
9495
buf = IOBuffer()
96+
unwrapped_ex = unwrap_exception(ex)
9597
show(IOContext(buf, :compact => true), req)
9698
print(buf, ", ",
97-
ex isa StatusError ? "HTTP $(ex.status): " :
99+
unwrapped_ex isa StatusError ? "HTTP $(ex.status): " :
100+
!isrecoverable(unwrapped_ex) ? "unrecoverable exception: " :
98101
!isbytes(req.body) ? "request streamed, " : "",
99102
!isbytes(req.response.body) ? "response streamed, " : "",
100103
!isidempotent(req) ? "$(req.method) non-idempotent" : "")

test/client.jl

+82-6
Original file line numberDiff line numberDiff line change
@@ -645,6 +645,53 @@ end
645645
end
646646
end
647647

648+
@testset "Don't retry on internal exceptions" begin
649+
kws = (retry_delays = [1, 2, 3], retries=3) # ~ 6 secs
650+
max_wait = 3
651+
652+
function test_finish_within(f, secs)
653+
timedout = Ref(false)
654+
t = Timer((t)->(timedout[] = true), secs)
655+
try
656+
f()
657+
finally
658+
close(t)
659+
end
660+
@test !timedout[]
661+
end
662+
663+
expected = ErrorException("request")
664+
test_finish_within(max_wait) do
665+
@test_throws expected ErrorRequest.get("https://$httpbin/ip"; request_exception=expected, kws...)
666+
end
667+
expected = ArgumentError("request")
668+
test_finish_within(max_wait) do
669+
@test_throws expected ErrorRequest.get("https://$httpbin/ip"; request_exception=expected, kws...)
670+
end
671+
672+
test_finish_within(max_wait) do
673+
expected = ErrorException("stream")
674+
e = try
675+
ErrorRequest.get("https://$httpbin/ip"; stream_exception=expected, kws...)
676+
catch e
677+
e
678+
end
679+
@assert e isa HTTP.RequestError
680+
@test e.error == expected
681+
end
682+
683+
test_finish_within(max_wait) do
684+
expected = ArgumentError("stream")
685+
e = try
686+
ErrorRequest.get("https://$httpbin/ip"; stream_exception=expected, kws...)
687+
catch e
688+
e
689+
end
690+
@assert e isa HTTP.RequestError
691+
@test e.error == expected
692+
end
693+
end
694+
648695
@testset "Retry with ConnectError" begin
649696
mktemp() do path, io
650697
redirect_stdout(io) do
@@ -668,12 +715,41 @@ end
668715
end
669716

670717
# isrecoverable tests
671-
@test HTTP.RetryRequest.isrecoverable(nothing)
672-
@test HTTP.RetryRequest.isrecoverable(ErrorException(""))
673-
@test HTTP.RetryRequest.isrecoverable(Sockets.DNSError("localhost", Base.UV_EAI_AGAIN))
674-
@test !HTTP.RetryRequest.isrecoverable(Sockets.DNSError("localhost", Base.UV_EAI_NONAME))
675-
@test HTTP.RetryRequest.isrecoverable(HTTP.Exceptions.ConnectError("http://localhost", Sockets.DNSError("localhost", Base.UV_EAI_AGAIN)))
676-
@test !HTTP.RetryRequest.isrecoverable(HTTP.Exceptions.ConnectError("http://localhost", Sockets.DNSError("localhost", Base.UV_EAI_NONAME)))
718+
@test !HTTP.RetryRequest.isrecoverable(nothing)
719+
720+
@test !HTTP.RetryRequest.isrecoverable(ErrorException(""))
721+
@test !HTTP.RetryRequest.isrecoverable(ArgumentError("yikes"))
722+
@test HTTP.RetryRequest.isrecoverable(ArgumentError("stream is closed or unusable"))
723+
724+
@test HTTP.RetryRequest.isrecoverable(HTTP.RequestError(nothing, ArgumentError("stream is closed or unusable")))
725+
@test !HTTP.RetryRequest.isrecoverable(HTTP.RequestError(nothing, ArgumentError("yikes")))
726+
727+
@test HTTP.RetryRequest.isrecoverable(CapturedException(ArgumentError("stream is closed or unusable"), Any[]))
728+
729+
recoverable_dns_error = Sockets.DNSError("localhost", Base.UV_EAI_AGAIN)
730+
unrecoverable_dns_error = Sockets.DNSError("localhost", Base.UV_EAI_NONAME)
731+
@test HTTP.RetryRequest.isrecoverable(recoverable_dns_error)
732+
@test !HTTP.RetryRequest.isrecoverable(unrecoverable_dns_error)
733+
@test HTTP.RetryRequest.isrecoverable(HTTP.Exceptions.ConnectError("http://localhost", recoverable_dns_error))
734+
@test !HTTP.RetryRequest.isrecoverable(HTTP.Exceptions.ConnectError("http://localhost", unrecoverable_dns_error))
735+
@test HTTP.RetryRequest.isrecoverable(CompositeException([
736+
recoverable_dns_error,
737+
HTTP.Exceptions.ConnectError("http://localhost", recoverable_dns_error),
738+
]))
739+
@test HTTP.RetryRequest.isrecoverable(CompositeException([
740+
recoverable_dns_error,
741+
HTTP.Exceptions.ConnectError("http://localhost", recoverable_dns_error),
742+
CompositeException([recoverable_dns_error])
743+
]))
744+
@test !HTTP.RetryRequest.isrecoverable(CompositeException([
745+
recoverable_dns_error,
746+
HTTP.Exceptions.ConnectError("http://localhost", unrecoverable_dns_error),
747+
]))
748+
@test !HTTP.RetryRequest.isrecoverable(CompositeException([
749+
recoverable_dns_error,
750+
HTTP.Exceptions.ConnectError("http://localhost", recoverable_dns_error),
751+
CompositeException([unrecoverable_dns_error])
752+
]))
677753
end
678754

679755
findnewline(bytes) = something(findfirst(==(UInt8('\n')), bytes), 0)

test/resources/TestRequest.jl

+22
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,25 @@ end
5555
HTTP.@client (first=[testouterrequestlayer], last=[testinnerrequestlayer]) (first=[testouterstreamlayer], last=[testinnerstreamlayer])
5656

5757
end
58+
59+
module ErrorRequest
60+
61+
using HTTP
62+
63+
function throwingrequestlayer(handler)
64+
return function(req; request_exception=nothing, kw...)
65+
!isnothing(request_exception) && throw(request_exception)
66+
return handler(req; kw...)
67+
end
68+
end
69+
70+
function throwingstreamlayer(handler)
71+
return function(stream; stream_exception=nothing, kw...)
72+
!isnothing(stream_exception) && throw(stream_exception)
73+
return handler(stream; kw...)
74+
end
75+
end
76+
77+
HTTP.@client (throwingrequestlayer,) (throwingstreamlayer,)
78+
79+
end

0 commit comments

Comments
 (0)