Request stream (#1463)

* split request/response

* stream api

* fix: global variable (#1464)

* fix global variable

* fix global variable

* Add httpc.head

Co-authored-by: JTrancender <jie-email@jie-trancender.org>
This commit is contained in:
云风
2021-08-30 10:00:57 +08:00
committed by GitHub
parent 6ee8d23ac4
commit 54e733a76f
4 changed files with 254 additions and 40 deletions

View File

@@ -141,9 +141,29 @@ function M.recvchunkedbody(readbytes, bodylimit, header, body)
return result, header
end
local function recvbody(interface, code, header, body)
local length = header["content-length"]
if length then
length = tonumber(length)
end
if length then
if #body >= length then
body = body:sub(1,length)
else
local padding = interface.read(length - #body)
body = body .. padding
end
elseif code == 204 or code == 304 or code < 200 then
body = ""
-- See https://stackoverflow.com/questions/15991173/is-the-content-length-header-required-for-a-http-1-0-response
else
-- no content-length, read all
body = body .. interface.readall()
end
return body
end
function M.request(interface, method, host, url, recvheader, header, content)
local is_ws = interface.websocket
local read = interface.read
local write = interface.write
local header_content = ""
@@ -181,11 +201,10 @@ function M.request(interface, method, host, url, recvheader, header, content)
if not header then
error("Invalid HTTP response header")
end
return code, body, header
end
local length = header["content-length"]
if length then
length = tonumber(length)
end
function M.response(interface, code, body, header)
local mode = header["transfer-encoding"]
if mode then
if mode ~= "identity" and mode ~= "chunked" then
@@ -194,32 +213,172 @@ function M.request(interface, method, host, url, recvheader, header, content)
end
if mode == "chunked" then
body, header = M.recvchunkedbody(read, nil, header, body)
body, header = M.recvchunkedbody(interface.read, nil, header, body)
if not body then
error("Invalid response body")
end
else
-- identity mode
if length then
if #body >= length then
body = body:sub(1,length)
body = recvbody(interface, code, header, body)
end
return body
end
local stream = {}; stream.__index = stream
function stream:close()
if self._onclose then
self._onclose(self)
self._onclose = nil
end
end
function stream:padding()
return self._reading(self), self
end
stream.__close = stream.close
stream.__call = stream.padding
local function stream_nobody(stream)
stream._reading = stream.close
stream.connected = nil
return ""
end
local function stream_length(length)
return function(stream)
local body = stream._body
if body == nil then
local ret, padding = interface.read()
if not ret then
-- disconnected
body = padding
stream.connected = false
else
local padding = read(length - #body)
body = body .. padding
body = ret
end
elseif code == 204 or code == 304 or code < 200 then
body = ""
-- See https://stackoverflow.com/questions/15991173/is-the-content-length-header-required-for-a-http-1-0-response
elseif is_ws and code == 101 then
-- if websocket handshake success
return code, body
end
local n = #body
if n >= length then
stream._reading = stream.close
stream.connected = nil
return (body:sub(1,length))
else
-- no content-length, read all
body = body .. interface.readall()
length = length - n
stream._body = nil
if not stream.connected then
stream._reading = stream.close
end
return body
end
end
end
local function stream_read(stream)
local ret, padding = stream._interface.read()
if ret == "" or not ret then
stream.connected = nil
stream:close()
if padding == "" then
return
end
return padding
end
return ret
end
local function stream_all(stream)
local body = stream._body
stream._body = nil
stream._reading = stream_read
return body
end
local function stream_chunked(stream)
local read = stream._interface.read
local sz, body = chunksize(read, stream._body)
if not sz then
stream.connected = false
stream:close()
return
end
if sz == 0 then
-- last chunk
local tmpline = {}
body = M.recvheader(read, tmpline, body)
if not body then
stream.connected = false
stream:close()
return
end
M.parseheader(tmpline,1, stream.header)
stream._reading = stream.close
stream.connected = nil
return ""
end
local n = #body
local remain
if n >= sz then
remain = body:sub(sz+1)
body = body:sub(1,sz)
else
body = body .. read(sz - n)
remain = ""
end
remain = readcrln(read, remain)
if not remain then
stream.connected = false
stream:close()
return
end
stream._body = remain
return body
end
function M.response_stream(interface, code, body, header)
local mode = header["transfer-encoding"]
if mode then
if mode ~= "identity" and mode ~= "chunked" then
error ("Unsupport transfer-encoding")
end
end
return code, body
local read_func
if mode == "chunked" then
readfunc = stream_chunked
else
-- identity mode
local length = header["content-length"]
if length then
length = tonumber(length)
end
if length then
readfunc = stream_length(length)
elseif code == 204 or code == 304 or code < 200 then
readfunc = stream_nobody
else
readfunc = stream_all
end
end
-- todo: timeout
return setmetatable({
status = code,
_body = body,
_interface = interface,
_reading = readfunc,
header = header,
connected = true,
}, stream)
end
return M