Skip to content

Commit 45d303a

Browse files
authored
Don't assume the only error in getObject is an ArgumentError (#47)
* Don't assume the only error in getObject is an ArgumentError * Use consistent error messages
1 parent 0229c4c commit 45d303a

File tree

4 files changed

+262
-4
lines changed

4 files changed

+262
-4
lines changed

Diff for: Project.toml

+5-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"
88
CloudBase = "85eb1798-d7c4-4918-bb13-c944d38e27ed"
99
CodecZlib = "944b1d66-785c-5afd-91f1-9de20f533193"
1010
CodecZlibNG = "642d12eb-acb5-4437-bcfc-a25e07ad685c"
11+
ExceptionUnwrapping = "460bff9d-24e4-43bc-9d9f-a8973cb893f4"
1112
HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3"
1213
Mmap = "a63ad114-7e13-5084-954f-fe012c677804"
1314
TranscodingStreams = "3bb67fe8-82b1-5028-8e26-92a6c54297fa"
@@ -19,16 +20,19 @@ Base64 = "1"
1920
CloudBase = "1"
2021
CodecZlib = "0.7"
2122
CodecZlibNG = "0.1"
23+
ExceptionUnwrapping = "0.1"
2224
HTTP = "1.7"
2325
Mmap = "1"
2426
TranscodingStreams = "0.9.12"
27+
Sockets = "1"
2528
WorkerUtilities = "1.1"
2629
XMLDict = "0.4"
2730
julia = "1.6"
2831

2932
[extras]
3033
CodecZlib = "944b1d66-785c-5afd-91f1-9de20f533193"
34+
Sockets = "6462fe0b-24de-5631-8697-dd941f90decc"
3135
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
3236

3337
[targets]
34-
test = ["CodecZlib", "Test"]
38+
test = ["CodecZlib", "Sockets", "Test"]

Diff for: src/CloudStore.jl

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export Object, PrefetchedDownloadStream, ResponseBodyType, RequestBodyType,
1212
using HTTP, CodecZlib, CodecZlibNG, Mmap
1313
import WorkerUtilities: OrderedSynchronizer
1414
import CloudBase: AbstractStore
15+
using ExceptionUnwrapping
1516

1617
"""
1718
Controls the automatic use of concurrency when downloading/uploading.

Diff for: src/get.jl

+27-3
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,28 @@ function Base.getindex(b::BufferBatch, i::Int)
7373
end
7474
end
7575

76+
# For smaller object, we don't do a multipart download, but instead just do a single GET request.
77+
# This changes the exception we get when the provided buffer is too small, as for the multipart
78+
# case, we do a HEAD request first to know the size of the object, which gives us the opportunity
79+
# to throw an ArgumentError. But for the single GET case, we don't know the size of the object
80+
# until we get the response, which would return as a HTTP.RequestError from within HTTP.jl.
81+
# The idea here is to unwrap the HTTP.RequestError and check if it's an ArgumentError, and if so,
82+
# throw that instead, so we same exception type is thrown in this case.
83+
function _check_buffer_too_small_exception(@nospecialize(e::Exception))
84+
if e isa HTTP.RequestError
85+
request_error = e.error
86+
if request_error isa CompositeException
87+
length(request_error.exceptions) == 1 || return e
88+
request_error = request_error.exceptions[1]
89+
end
90+
request_error = unwrap_exception(request_error)
91+
if request_error isa ArgumentError
92+
return request_error
93+
end
94+
end
95+
return e
96+
end
97+
7698
function getObjectImpl(x::AbstractStore, key::String, out::ResponseBodyType=nothing;
7799
multipartThreshold::Int=MULTIPART_THRESHOLD,
78100
partSize::Int=MULTIPART_SIZE,
@@ -124,8 +146,9 @@ function getObjectImpl(x::AbstractStore, key::String, out::ResponseBodyType=noth
124146
elseif out isa AbstractVector{UInt8}
125147
resp = try
126148
getObject(x, url, headers; response_stream=out, kw...)
127-
catch
128-
throw(ArgumentError("provided output buffer (length = $(length(out))) is too small for actual cloud object size"))
149+
catch e
150+
e = _check_buffer_too_small_exception(e)
151+
rethrow(e)
129152
end
130153
elseif out isa String
131154
if decompress
@@ -172,7 +195,8 @@ function getObjectImpl(x::AbstractStore, key::String, out::ResponseBodyType=noth
172195
res = body = Vector{UInt8}(undef, contentLength)
173196
elseif out isa AbstractVector{UInt8}
174197
# user-provided buffer is allowed to be larger than actual object size, but not smaller
175-
length(out) < contentLength && throw(ArgumentError("out ($(length(out))) must at least be of length $contentLength"))
198+
# NOTE: wording of the error message matches what HTTP.jl throws when the buffer is too small
199+
length(out) < contentLength && throw(ArgumentError("Unable to grow response stream IOBuffer $(length(out)) large enough for response body size: $(contentLength)"))
176200
res = out
177201
body = view(out, 1:contentLength)
178202
elseif out isa String

Diff for: test/runtests.jl

+229
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
using Test, CloudStore, CloudBase.CloudTest
22
import CloudStore: S3, Blobs
33
using CodecZlib
4+
using HTTP: ConnectError, StatusError
5+
using Sockets: DNSError
6+
using ExceptionUnwrapping: unwrap_exception
47

58
bytes(x) = codeunits(x)
69

@@ -134,6 +137,119 @@ check(x, y) = begin; reset!(x); reset!(y); z = read(x) == read(y); reset!(x); re
134137
end
135138
end
136139
end
140+
141+
@testset "Exceptions" begin
142+
# conf, p = Minio.run(; debug=true)
143+
Minio.with(; debug=true) do conf
144+
credentials, bucket = conf
145+
global _stale_bucket = bucket
146+
csv = "a,b,c\n1,2,3\n4,5"
147+
obj = S3.put(bucket, "test.csv", bytes(csv); credentials)
148+
@assert obj.size == sizeof(csv)
149+
150+
@testset "Insufficient output buffer size" begin
151+
out = zeros(UInt8, sizeof(csv) - 1)
152+
try
153+
S3.get(bucket, "test.csv", out; credentials, allowMultipart=false) # single request
154+
@test false # Should have thrown an error
155+
catch e
156+
@test e isa ArgumentError
157+
@test e.msg == "Unable to grow response stream IOBuffer $(sizeof(out)) large enough for response body size: $(sizeof(csv))"
158+
end
159+
160+
try
161+
S3.get(bucket, "test.csv", out; credentials, allowMultipart=true, multipartThreshold=1) # multipart request
162+
@test false # Should have thrown an error
163+
catch e
164+
@test e isa ArgumentError
165+
@test e.msg == "Unable to grow response stream IOBuffer $(sizeof(out)) large enough for response body size: $(sizeof(csv))"
166+
end
167+
end
168+
169+
@testset "Missing credentials" begin
170+
try
171+
S3.get(bucket, "test.csv") # single request
172+
@test false # Should have thrown an error
173+
catch e
174+
@test e isa StatusError
175+
@test e.status == 403
176+
end
177+
178+
try
179+
S3.get(bucket, "test.csv"; allowMultipart=true, multipartThreshold=1) # multipart request
180+
@test false # Should have thrown an error
181+
catch e
182+
@test e isa StatusError
183+
@test e.status == 403
184+
end
185+
186+
try
187+
S3.put(bucket, "test2.csv", csv)
188+
@test false # Should have thrown an error
189+
catch e
190+
@test e isa StatusError
191+
@test e.status == 403
192+
end
193+
end
194+
195+
@testset "Non-existing file" begin
196+
try
197+
S3.get(bucket, "doesnt_exist.csv"; credentials, allowMultipart=false) # single request
198+
@test false # Should have thrown an error
199+
catch e
200+
@test e isa StatusError
201+
@test e.status == 404
202+
end
203+
204+
try
205+
S3.get(bucket, "doesnt_exist.csv"; credentials, allowMultipart=true, multipartThreshold=1) # multipart request
206+
@test false # Should have thrown an error
207+
catch e
208+
@test e isa StatusError
209+
@test e.status == 404
210+
end
211+
end
212+
213+
@testset "Connection error: DNSError" begin
214+
non_existent_bucket_name = string(bucket.name, "doesntexist")
215+
non_existent_baseurl = replace(bucket.baseurl, bucket.name => non_existent_bucket_name)
216+
non_existent_bucket = S3.Bucket(non_existent_bucket_name, non_existent_baseurl)
217+
try
218+
S3.get(non_existent_bucket, "doesnt_exist.csv"; credentials)
219+
@test false # Should have thrown an error
220+
catch e
221+
@test e isa ConnectError
222+
@test unwrap_exception(e.error) isa DNSError
223+
end
224+
225+
try
226+
S3.put(non_existent_bucket, "doesnt_exist.csv", csv; credentials)
227+
@test false # Should have thrown an error
228+
catch e
229+
@test e isa ConnectError
230+
@test unwrap_exception(e.error) isa DNSError
231+
end
232+
end
233+
end
234+
# Minio doesn't run at this point
235+
@testset "Connection error: IOError" begin
236+
try
237+
S3.get(_stale_bucket, "doesnt_exist.csv")
238+
@test false # Should have thrown an error
239+
catch e
240+
@test e isa ConnectError
241+
@test unwrap_exception(e.error) isa Base.IOError
242+
end
243+
244+
try
245+
S3.put(_stale_bucket, "doesnt_exist.csv", "my,da,ta")
246+
@test false # Should have thrown an error
247+
catch e
248+
@test e isa ConnectError
249+
@test unwrap_exception(e.error) isa Base.IOError
250+
end
251+
end
252+
end
137253
end
138254

139255
@time @testset "Blobs" begin
@@ -235,6 +351,119 @@ end
235351
end
236352
end
237353
end
354+
355+
@testset "Exceptions" begin
356+
# conf, p = Azurite.run(; debug=true)
357+
Azurite.with(; debug=true) do conf
358+
credentials, container = conf
359+
global _stale_container = container
360+
csv = "a,b,c\n1,2,3\n4,5"
361+
obj = Blobs.put(container, "test.csv", bytes(csv); credentials)
362+
@assert obj.size == sizeof(csv)
363+
364+
@testset "Insufficient output buffer size" begin
365+
out = zeros(UInt8, sizeof(csv) - 1)
366+
try
367+
Blobs.get(container, "test.csv", out; credentials, allowMultipart=false) # single request
368+
@test false # Should have thrown an error
369+
catch e
370+
@test e isa ArgumentError
371+
@test e.msg == "Unable to grow response stream IOBuffer $(sizeof(out)) large enough for response body size: $(sizeof(csv))"
372+
end
373+
374+
try
375+
Blobs.get(container, "test.csv", out; credentials, allowMultipart=true, multipartThreshold=1) # multipart request
376+
@test false # Should have thrown an error
377+
catch e
378+
@test e isa ArgumentError
379+
@test e.msg == "Unable to grow response stream IOBuffer $(sizeof(out)) large enough for response body size: $(sizeof(csv))"
380+
end
381+
end
382+
383+
@testset "Missing credentials" begin
384+
try
385+
Blobs.get(container, "test.csv") # single request
386+
@test false # Should have thrown an error
387+
catch e
388+
@test e isa StatusError
389+
@test e.status == 403
390+
end
391+
392+
try
393+
Blobs.get(container, "test.csv"; allowMultipart=true, multipartThreshold=1) # multipart request
394+
@test false # Should have thrown an error
395+
catch e
396+
@test e isa StatusError
397+
@test e.status == 403
398+
end
399+
400+
try
401+
Blobs.put(container, "test2.csv", csv)
402+
@test false # Should have thrown an error
403+
catch e
404+
@test e isa StatusError
405+
@test e.status == 403
406+
end
407+
end
408+
409+
@testset "Non-existing file" begin
410+
try
411+
Blobs.get(container, "doesnt_exist.csv"; credentials, allowMultipart=false) # single request
412+
@test false # Should have thrown an error
413+
catch e
414+
@test e isa StatusError
415+
@test e.status == 404
416+
end
417+
418+
try
419+
Blobs.get(container, "doesnt_exist.csv"; credentials, allowMultipart=true, multipartThreshold=1) # multipart request
420+
@test false # Should have thrown an error
421+
catch e
422+
@test e isa StatusError
423+
@test e.status == 404
424+
end
425+
end
426+
427+
@testset "Connection error: DNSError" begin
428+
non_existent_container_name = string(container.name, "doesntexist")
429+
non_existent_baseurl = replace(container.baseurl, container.name => non_existent_container_name)
430+
non_existent_container = Blobs.Container(non_existent_container_name, non_existent_baseurl)
431+
try
432+
Blobs.get(non_existent_container, "doesnt_exist.csv"; credentials)
433+
@test false # Should have thrown an error
434+
catch e
435+
@test e isa ConnectError
436+
@test unwrap_exception(e.error) isa DNSError
437+
end
438+
439+
try
440+
Blobs.put(non_existent_container, "doesnt_exist.csv", csv; credentials)
441+
@test false # Should have thrown an error
442+
catch e
443+
@test e isa ConnectError
444+
@test unwrap_exception(e.error) isa DNSError
445+
end
446+
end
447+
end
448+
# Azurite is not running at this point
449+
@testset "Connection error: IOError" begin
450+
try
451+
Blobs.get(_stale_container, "doesnt_exist.csv")
452+
@test false # Should have thrown an error
453+
catch e
454+
@test e isa ConnectError
455+
@test unwrap_exception(e.error) isa Base.IOError
456+
end
457+
458+
try
459+
Blobs.put(_stale_container, "doesnt_exist.csv", "my,da,ta")
460+
@test false # Should have thrown an error
461+
catch e
462+
@test e isa ConnectError
463+
@test unwrap_exception(e.error) isa Base.IOError
464+
end
465+
end
466+
end
238467
end
239468

240469
@testset "URL Parsing Unit Tests" begin

0 commit comments

Comments
 (0)