Skip to content

Commit 0c9898b

Browse files
ref: parse multiple requests when redirected (fix #235)
1 parent 314aa24 commit 0c9898b

File tree

5 files changed

+265
-67
lines changed

5 files changed

+265
-67
lines changed

lua/rest-nvim/client/curl/cli.lua

+109-31
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,65 @@ end
4848
---@private
4949
local parser = {}
5050

51+
---@class rest.Result
52+
---@field requests rest.Response_[]
53+
---@field statistics table<string,string> Response statistics
54+
55+
---@class rest.Response_
56+
---@field request rest.RequestCore
57+
---@field response rest.Response
58+
59+
---@class rest.RequestCore
60+
---@field method string
61+
---@field url string
62+
---@field http_version string
63+
---@field headers table<string,string[]>
64+
5165
---@package
5266
---@param str string
53-
---@return rest.Response.status
54-
function parser.parse_verbose_status(str)
55-
local version, code, text = str:match("^(%S+) (%d+) ?(.*)")
67+
---@return rest.RequestCore?
68+
function parser.parse_req_info(str)
69+
local method, url, version = str:match("^([A-Z]+) (.+) (HTTP/[%d%.]+)")
70+
if not method then
71+
return
72+
end
73+
return {
74+
method = method,
75+
url = url,
76+
http_version = version,
77+
headers = {},
78+
}
79+
end
80+
81+
function parser.parse_req_header(str, requests)
82+
local info = parser.parse_req_info(str)
83+
if info then
84+
table.insert(requests, {
85+
request = info,
86+
response = {},
87+
})
88+
return
89+
end
90+
local req = requests[#requests].request
91+
local key, value = parser.parse_header_pair(str)
92+
if key then
93+
if not req.headers[key] then
94+
req.headers[key] = {}
95+
end
96+
table.insert(req.headers[key], value)
97+
else
98+
log.error("Error while parsing verbose curl output header:", str)
99+
end
100+
end
101+
102+
---@package
103+
---@param str string
104+
---@return rest.Response.status?
105+
function parser.parse_res_status(str)
106+
local version, code, text = str:match("^(HTTP/[%d%.]+) (%d+) ?(.*)")
107+
if not version then
108+
return
109+
end
56110
return {
57111
version = version,
58112
code = tonumber(code),
@@ -73,22 +127,49 @@ function parser.parse_header_pair(str)
73127
end
74128

75129
---@package
130+
---@param str string
131+
function parser.parse_res_header(str, requests)
132+
local status = parser.parse_res_status(str)
133+
if status then
134+
-- reset response object
135+
requests[#requests].response = {
136+
status = status,
137+
headers = {},
138+
}
139+
return
140+
end
141+
local res = requests[#requests].response
142+
local key, value = parser.parse_header_pair(str)
143+
if key then
144+
if not res.headers[key] then
145+
res.headers[key] = {}
146+
end
147+
table.insert(res.headers[key], value)
148+
else
149+
log.error("Error while parsing verbose curl output header:", str)
150+
end
151+
end
152+
153+
---@package
154+
---@param idx number
76155
---@param line string
77-
---@return {prefix:string,str:string?}|nil
78-
function parser.parse_verbose_line(line)
156+
---@return {idx:number,prefix:string,str:string?}|nil
157+
function parser.lex_verbose_line(idx, line)
79158
local prefix, str = line:match("(.) ?(.*)")
159+
log.debug("line", idx, line)
80160
if not prefix then
81-
log.error("Error while parsing verbose curl output:\n" .. line)
161+
log.error(("Error while parsing verbose curl output at line %d:"):format(idx), line)
82162
return
83163
end
84164
return {
165+
idx = idx,
85166
prefix = prefix,
86167
str = str,
87168
}
88169
end
89170

90171
local _VERBOSE_PREFIX_META = "*"
91-
local _VERBOSE_PREFIX_REQ_HEADER = ">"
172+
local VERBOSE_PREFIX_REQ_HEADER = ">"
92173
local _VERBOSE_PREFIX_REQ_BODY = "}"
93174
local VERBOSE_PREFIX_RES_HEADER = "<"
94175
-- NOTE: we don't parse response body with trace output. response body will
@@ -114,35 +195,32 @@ function parser.parse_stat_pair(str)
114195
end
115196

116197
---@param lines string[]
117-
---@return rest.Response
198+
---@return rest.Result
118199
function parser.parse_verbose(lines)
119-
local response = {
120-
headers = {},
200+
---@type rest.Result
201+
local result = {
202+
---@type rest.Response_[]
203+
requests = {},
204+
---@type table<string,string> Response statistics
121205
statistics = {},
122206
}
123-
vim.iter(lines):map(parser.parse_verbose_line):each(function(ln)
124-
if ln.prefix == VERBOSE_PREFIX_RES_HEADER then
125-
if not response.status then
126-
-- response status
127-
response.status = parser.parse_verbose_status(ln.str)
128-
else
129-
-- response header
130-
local key, value = parser.parse_header_pair(ln.str)
131-
if key then
132-
if not response.headers[key] then
133-
response.headers[key] = {}
134-
end
135-
table.insert(response.headers[key], value)
136-
end
137-
end
207+
-- ignore last newline
208+
if lines[#lines] == "" then
209+
lines[#lines] = nil
210+
end
211+
vim.iter(lines):enumerate():map(parser.lex_verbose_line):each(function(ln)
212+
if ln.prefix == VERBOSE_PREFIX_REQ_HEADER then
213+
parser.parse_req_header(ln.str, result.requests)
214+
elseif ln.prefix == VERBOSE_PREFIX_RES_HEADER then
215+
parser.parse_res_header(ln.str, result.requests)
138216
elseif ln.prefix == VERBOSE_PREFIX_STAT then
139217
local key, value = parser.parse_stat_pair(ln.str)
140218
if key then
141-
response.statistics[key] = value
219+
result.statistics[key] = value
142220
end
143221
end
144222
end)
145-
return response
223+
return result
146224
end
147225

148226
--- Builder ---
@@ -330,7 +408,7 @@ end
330408

331409
---Send request via `curl` cli
332410
---@param request rest.Request Request data to be passed to cURL
333-
---@return nio.control.Future future Future containing rest.Response
411+
---@return nio.control.Future future Future containing rest.Result
334412
function curl.request(request)
335413
local progress_handle = progress.handle.create({
336414
title = "Executing",
@@ -352,9 +430,9 @@ function curl.request(request)
352430
progress_handle:report({
353431
message = "Parsing response...",
354432
})
355-
local response = parser.parse_verbose(vim.split(sc.stderr, "\n"))
356-
response.body = sc.stdout
357-
future.set(response)
433+
local result = parser.parse_verbose(vim.split(sc.stderr, "\n"))
434+
result.requests[#result.requests].response.body = sc.stdout
435+
future.set(result)
358436
progress_handle:report({
359437
message = "Success",
360438
})

lua/rest-nvim/client/curl/libcurl.lua

+22-4
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ end
5656
---Execute an HTTP request using cURL
5757
---return return nil if execution failed
5858
---@param req rest.Request Request data to be passed to cURL
59-
---@return rest.Response? info The request information (url, method, headers, body, etc)
59+
---@return rest.Result?
6060
function client.request(req)
6161
logger.info("sending request to: " .. req.url)
6262
if not found_curl then
@@ -162,8 +162,13 @@ function client.request(req)
162162
logger.error("Something went wrong when making the request with cURL:\n" .. curl_utils.curl_error(err:no()))
163163
return
164164
end
165+
local status_str = table.remove(res_raw_headers, 1)
165166
---@diagnostic disable-next-line: invisible
166-
local status = curl_cli.parser.parse_verbose_status(table.remove(res_raw_headers, 1))
167+
local status = curl_cli.parser.parse_res_status(status_str)
168+
if not status then
169+
logger.error("can't parse response status:", status_str)
170+
return
171+
end
167172
local res_headers = {}
168173
for _, header in ipairs(res_raw_headers) do
169174
---@diagnostic disable-next-line: invisible
@@ -180,12 +185,25 @@ function client.request(req)
180185
status = status,
181186
headers = res_headers,
182187
body = table.concat(res_result),
183-
statistics = get_stats(req_, {}),
184188
}
185189
logger.debug(vim.inspect(res.headers))
186190
res.status.text = vim.trim(res.status.text)
187191
req_:close()
188-
return res
192+
---@type rest.Result
193+
return {
194+
requests = {
195+
{
196+
request = {
197+
method = req.method,
198+
url = req.url,
199+
http_version = req.http_version or "HTTP/1.1",
200+
headers = req.headers,
201+
},
202+
response = res,
203+
},
204+
},
205+
statistics = get_stats(req_, {}),
206+
}
189207
end
190208

191209
return client

lua/rest-nvim/request.lua

+6-5
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ local Context = require("rest-nvim.context").Context
3131
---@field status rest.Response.status Status information from response
3232
---@field body string? Raw response body
3333
---@field headers table<string,string[]> Response headers
34-
---@field statistics table<string,string> Response statistics
3534

3635
---@class rest.Response.status
3736
---@field code number
@@ -69,28 +68,30 @@ local function run_request(req)
6968
vim.notify("request failed. See `:Rest logs` for more info", vim.log.levels.ERROR, { title = "rest.nvim" })
7069
return
7170
end
72-
---@cast res rest.Response
71+
---@cast res rest.Result
7372
logger.info("request success")
7473

74+
local last_response = res.requests[#res.requests].response
75+
7576
-- run request handler scripts
7677
vim.iter(req.handlers):each(function(f)
7778
f(res)
7879
end)
7980
logger.info("handler done")
8081

8182
_G.rest_request = req
82-
_G.rest_response = res
83+
_G.rest_response = last_response
8384
vim.api.nvim_exec_autocmds("User", {
8485
pattern = { "RestResponse", "RestResponsePre" },
8586
})
8687
_G.rest_request = nil
8788
_G.rest_response = nil
8889

8990
-- update cookie jar
90-
jar.update_jar(req.url, res)
91+
jar.update_jar(req.url, last_response)
9192

9293
-- update result UI
93-
ui.update({ response = res })
94+
ui.update({ response = last_response, statistics = res.statistics })
9495
end)
9596
-- FIXME(boltless): return future to pass the command state
9697
end

lua/rest-nvim/ui/result.lua

+9-16
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ local data = {
3434
request = nil,
3535
---@type rest.Response?
3636
response = nil,
37+
---@type table<string,string>?
38+
statistics = nil,
3739
}
3840

3941
---@param req rest.Request
@@ -76,8 +78,9 @@ local panes = {
7678
)
7779
)
7880
local content_type = data.response.headers["content-type"]
81+
table.insert(lines, "")
82+
table.insert(lines, "# @_RES")
7983
local body = vim.split(data.response.body, "\n")
80-
local body_meta = {}
8184
if content_type then
8285
local base_type, res_type = content_type[1]:match("(.*)/([^;]+)")
8386
if base_type == "image" then
@@ -86,19 +89,9 @@ local panes = {
8689
body = { "Binary answer" }
8790
elseif config.response.hooks.format then
8891
-- NOTE: format hook runs here because it should be done last.
89-
local ok
90-
body, ok = utils.gq_lines(body, res_type)
91-
if ok then
92-
table.insert(body_meta, "formatted")
93-
end
92+
body = utils.gq_lines(body, res_type)
9493
end
9594
end
96-
local meta_str = ""
97-
if #body_meta > 0 then
98-
meta_str = " (" .. table.concat(body_meta, ",") .. ")"
99-
end
100-
table.insert(lines, "")
101-
table.insert(lines, "# @_RES" .. meta_str)
10295
vim.list_extend(lines, body)
10396
table.insert(lines, "# @_END")
10497
else
@@ -166,14 +159,14 @@ local panes = {
166159
return
167160
end
168161
local lines = {}
169-
if not data.response.statistics then
162+
if not data.statistics then
170163
set_lines(self.bufnr, { "No Statistics" })
171164
return
172165
end
173166
syntax_highlight(self.bufnr, "http_stat")
174167
for _, style in ipairs(config.clients.curl.statistics) do
175168
local title = style.title or style.id
176-
local value = data.response.statistics[style.id] or ""
169+
local value = data.statistics[style.id] or ""
177170
table.insert(lines, ("%s: %s"):format(title, value))
178171
end
179172
set_lines(self.bufnr, lines)
@@ -191,7 +184,7 @@ winbar = winbar .. "%#RestText#Press %#Keyword#?%#RestText# for help%#Normal# "
191184
---@return string
192185
function ui.stat_winbar()
193186
local content = ""
194-
if not data.response then
187+
if not data.statistics then
195188
return "Loading...%#Normal#"
196189
end
197190
for _, style in ipairs(config.clients.curl.statistics) do
@@ -200,7 +193,7 @@ function ui.stat_winbar()
200193
if title ~= "" then
201194
title = title .. ": "
202195
end
203-
local value = data.response.statistics[style.id] or ""
196+
local value = data.statistics[style.id] or ""
204197
content = content .. " %#RestText#" .. title .. "%#Normal#" .. value
205198
end
206199
end

0 commit comments

Comments
 (0)