Skip to content

Commit 47c24c9

Browse files
authored
Allow passing pre-allocated buffer for response body (#984)
* Allow passing pre-allocated buffer for response body This is one piece in some efforts we're going to make to drastically reduce the # of allocations required for making HTTP requests. This PR allows the user to pass a pre-allocated `Vector{UInt8}` via the `response_stream` keyword arg (previously not allowed), which will then be used directly for writing the response body. * Updates * Fixes * Fix * fix * Fix client test
1 parent 1d843b9 commit 47c24c9

File tree

4 files changed

+77
-19
lines changed

4 files changed

+77
-19
lines changed

src/Streams.jl

+19-15
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module Streams
22

3-
export Stream, closebody, isaborted, setstatus
3+
export Stream, closebody, isaborted, setstatus, readall!
44

55
using Sockets, LoggingExtras
66
using ..IOExtras, ..Messages, ..ConnectionPool, ..Conditions, ..Exceptions
@@ -252,23 +252,21 @@ function Base.read(http::Stream, ::Type{UInt8})
252252
end
253253

254254
function http_unsafe_read(http::Stream, p::Ptr{UInt8}, n::UInt)::Int
255-
256255
ntr = UInt(ntoread(http))
257-
if ntr == 0
258-
return 0
259-
end
256+
ntr == 0 && return 0
257+
# If there is spare space in `p`
258+
# read two extra bytes
259+
# (`\r\n` at end ofchunk).
260260
unsafe_read(http.stream, p, min(n, ntr + (http.readchunked ? 2 : 0)))
261-
# If there is spare space in `p`
262-
# read two extra bytes
263-
n = min(n, ntr) # (`\r\n` at end ofchunk).
261+
n = min(n, ntr)
264262
update_ntoread(http, n)
265263
return n
266264
end
267265

268266
function Base.readbytes!(http::Stream, buf::AbstractVector{UInt8},
269267
n=length(buf))
270268
@require n <= length(buf)
271-
return http_unsafe_read(http, pointer(buf), UInt(n))
269+
return GC.@preserve buf http_unsafe_read(http, pointer(buf), UInt(n))
272270
end
273271

274272
function Base.unsafe_read(http::Stream, p::Ptr{UInt8}, n::UInt)
@@ -282,14 +280,20 @@ function Base.unsafe_read(http::Stream, p::Ptr{UInt8}, n::UInt)
282280
nothing
283281
end
284282

285-
function Base.readbytes!(http::Stream, buf::IOBuffer, n=bytesavailable(http))
286-
Base.ensureroom(buf, n)
287-
unsafe_read(http, pointer(buf.data, buf.size + 1), n)
283+
@noinline bufcheck(buf, n) = ((buf.size + n) <= length(buf.data)) || throw(ArgumentError("Unable to grow response stream IOBuffer large enough for response body size"))
284+
285+
function Base.readbytes!(http::Stream, buf::Base.GenericIOBuffer, n=bytesavailable(http))
286+
Base.ensureroom(buf, buf.size + n)
287+
# check if there's enough room in buf to write n bytes
288+
bufcheck(buf, n)
289+
data = buf.data
290+
GC.@preserve data unsafe_read(http, pointer(data, buf.size + 1), n)
288291
buf.size += n
289292
end
290293

291-
function Base.read(http::Stream)
292-
buf = PipeBuffer()
294+
Base.read(http::Stream, buf::Base.GenericIOBuffer=PipeBuffer()) = take!(readall!(http, buf))
295+
296+
function readall!(http::Stream, buf::Base.GenericIOBuffer=PipeBuffer())
293297
if ntoread(http) == unknown_length
294298
while !eof(http)
295299
readbytes!(http, buf)
@@ -299,7 +303,7 @@ function Base.read(http::Stream)
299303
readbytes!(http, buf, ntoread(http))
300304
end
301305
end
302-
return take!(buf)
306+
return buf
303307
end
304308

305309
function Base.readuntil(http::Stream, f::Function)::ByteView

src/clientlayers/RetryRequest.jl

+3-3
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ e.g. `Sockets.DNSError`, `Base.EOFError` and `HTTP.StatusError`
2222
"""
2323
function retrylayer(handler)
2424
return function(req::Request; retry::Bool=true, retries::Int=4,
25-
retry_delays::ExponentialBackOff=ExponentialBackOff(n = retries), retry_check=FALSE,
25+
retry_delays::ExponentialBackOff=ExponentialBackOff(n = retries, factor=3.0), retry_check=FALSE,
2626
retry_non_idempotent::Bool=false, kw...)
2727
if !retry || retries == 0
2828
# no retry
@@ -61,8 +61,8 @@ function retrylayer(handler)
6161
@debugv 1 "🚷 No Retry: $(no_retry_reason(ex, req))"
6262
end
6363
return s, retry
64-
end)
65-
64+
end
65+
)
6666
return retry_request(req; kw...)
6767
end
6868
end

src/clientlayers/StreamRequest.jl

+27-1
Original file line numberDiff line numberDiff line change
@@ -128,10 +128,36 @@ function readbody(stream::Stream, res::Response, decompress::Union{Nothing, Bool
128128
end
129129
end
130130

131+
# 2 most common types of IOBuffers
132+
const IOBuffers = Union{IOBuffer, Base.GenericIOBuffer{SubArray{UInt8, 1, Vector{UInt8}, Tuple{UnitRange{Int64}}, true}}}
133+
131134
function readbody!(stream::Stream, res::Response, buf_or_stream)
132135
if !iserror(res)
133136
if isbytes(res.body)
134-
res.body = read(buf_or_stream)
137+
if length(res.body) > 0
138+
# user-provided buffer to read response body into
139+
# specify write=true to make the buffer writable
140+
# but also specify maxsize, which means it won't be grown
141+
# (we don't want to be changing the user's buffer for them)
142+
body = IOBuffer(res.body; write=true, maxsize=length(res.body))
143+
if buf_or_stream isa BufferStream
144+
# if it's a BufferStream, the response body was gzip encoded
145+
# so using the default write is fastest because it utilizes
146+
# readavailable under the hood, for which BufferStream is optimized
147+
write(body, buf_or_stream)
148+
elseif buf_or_stream isa Stream
149+
# for HTTP.Stream, there's already an optimized read method
150+
# that just needs an IOBuffer to write into
151+
readall!(buf_or_stream, body)
152+
else
153+
error("unreachable")
154+
end
155+
else
156+
res.body = read(buf_or_stream)
157+
end
158+
elseif (res.body isa IOBuffers || res.body isa Base.GenericIOBuffer) && buf_or_stream isa Stream
159+
# optimization for IOBuffer response_stream to avoid temporary allocations
160+
readall!(buf_or_stream, res.body)
135161
else
136162
write(res.body, buf_or_stream)
137163
end

test/client.jl

+28
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,34 @@ end
122122
x["headers"]["Host"] == y["headers"]["Host"] &&
123123
x["headers"]["User-Agent"] == y["headers"]["User-Agent"]
124124
end
125+
126+
# pass pre-allocated buffer
127+
body = zeros(UInt8, 100)
128+
r = HTTP.get("https://$httpbin/bytes/100"; response_stream=body, socket_type_tls=tls)
129+
@test body === r.body
130+
131+
# wrapping pre-allocated buffer in IOBuffer will write to buffer directly
132+
io = IOBuffer(body; write=true)
133+
r = HTTP.get("https://$httpbin/bytes/100"; response_stream=io, socket_type_tls=tls)
134+
@test body === r.body.data
135+
136+
# if provided buffer is too small, we won't grow it for user
137+
body = zeros(UInt8, 10)
138+
@test_throws HTTP.RequestError HTTP.get("https://$httpbin/bytes/100"; response_stream=body, socket_type_tls=tls, retry=false)
139+
140+
# also won't shrink it if buffer provided is larger than response body
141+
body = zeros(UInt8, 10)
142+
r = HTTP.get("https://$httpbin/bytes/5"; response_stream=body, socket_type_tls=tls)
143+
@test body === r.body
144+
@test length(body) == 10
145+
@test HTTP.header(r, "Content-Length") == "5"
146+
147+
# but if you wrap it in a writable IOBuffer, we will grow it
148+
io = IOBuffer(body; write=true)
149+
r = HTTP.get("https://$httpbin/bytes/100"; response_stream=io, socket_type_tls=tls)
150+
# same Array, though it was resized larger
151+
@test body === r.body.data
152+
@test length(body) == 100
125153
end
126154

127155
@testset "Client Body Posting - Vector{UTF8}, String, IOStream, IOBuffer, BufferStream, Dict, NamedTuple" begin

0 commit comments

Comments
 (0)