mirror of
https://github.com/neovim/neovim.git
synced 2026-07-16 06:10:39 +00:00
feat(vim.net): request() accepts more http methods #39406
This commit is contained in:
@@ -4204,9 +4204,10 @@ Lua module: vim.net *vim.net*
|
||||
• {body} (`string`) The HTTP body of the request
|
||||
|
||||
|
||||
vim.net.request({url}, {opts}, {on_response}) *vim.net.request()*
|
||||
Makes an HTTP GET request to the given URL, asynchronously passing the
|
||||
result to the specified `on_response`, `outpath` or `outbuf`.
|
||||
*vim.net.request()*
|
||||
vim.net.request({method}, {url}, {opts}, {on_response})
|
||||
Makes an HTTP request to the given URL, asynchronously passing the result
|
||||
to the specified `on_response`, `outpath` or `outbuf`.
|
||||
|
||||
Examples: >lua
|
||||
-- Write response body to file.
|
||||
@@ -4236,11 +4237,21 @@ vim.net.request({url}, {opts}, {on_response}) *vim.net.request()*
|
||||
vim.net.request('https://neovim.io/charter/', {
|
||||
headers = { Authorization = 'Bearer XYZ' },
|
||||
})
|
||||
|
||||
-- POST request with body.
|
||||
vim.net.request('POST', 'https://example.com/api', {
|
||||
body = '{"key": "value"}',
|
||||
headers = {['Content-Type'] = 'application/json' }
|
||||
})
|
||||
<
|
||||
|
||||
Parameters: ~
|
||||
• {method} (`vim.net.HttpMethod`) (default: GET) The HTTP method
|
||||
(GET, POST, PUT, PATCH, HEAD, DELETE).
|
||||
• {url} (`string`) The URL for the request.
|
||||
• {opts} (`table?`) A table with the following fields:
|
||||
• {body}? (`string`) Request body for POST/PUT/PATCH
|
||||
requests.
|
||||
• {headers}? (`table<string, string>`) Custom headers
|
||||
to send with the request. Supports basic key/value
|
||||
headers and empty headers as supported by curl. Does
|
||||
@@ -4253,10 +4264,12 @@ vim.net.request({url}, {opts}, {on_response}) *vim.net.request()*
|
||||
• {retry}? (`integer`) Number of retries on transient
|
||||
failures (default: 3).
|
||||
• {verbose}? (`boolean`) Enables verbose output.
|
||||
• {on_response} (`fun(err: string?, response: vim.net.request.Response?)?`)
|
||||
Callback invoked on request completion. The `body`
|
||||
field in the response parameter contains the raw
|
||||
response data (text or binary).
|
||||
• {on_response} (`vim.net.request.ResponseFunc?`) Callback invoked on
|
||||
request completion.
|
||||
|
||||
Overloads: ~
|
||||
• `fun(url: string, opts: vim.net.request.Opts, response: vim.net.request.ResponseFunc)`
|
||||
• `fun(method: vim.net.HttpMethod, url: string, opts: vim.net.request.Opts, response: vim.net.request.ResponseFunc)`
|
||||
|
||||
Return: ~
|
||||
(`{ close: fun() }`) Object with `close()` method which cancels the
|
||||
|
||||
@@ -161,6 +161,7 @@ LSP
|
||||
LUA
|
||||
|
||||
• |vim.net.request()| can specify custom headers by passing `opts.headers`.
|
||||
• |vim.net.request()| can now accept `method` param overload for multiple HTTP methods.
|
||||
• |writefile()| treats Lua strings as "blob", so it can be used to write
|
||||
binary data.
|
||||
• |vim.filetype.inspect()| returns a copy of the internal tables used for
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
local M = {}
|
||||
|
||||
local http_methods = {
|
||||
GET = true,
|
||||
POST = true,
|
||||
PUT = true,
|
||||
PATCH = true,
|
||||
HEAD = true,
|
||||
DELETE = true,
|
||||
}
|
||||
|
||||
---@alias vim.net.request.ResponseFunc fun(err: string?, response: vim.net.request.Response?)
|
||||
---@alias vim.net.HttpMethod string "GET" | "POST" | "PUT" | "PACH" | "HEAD"| "DELETE
|
||||
|
||||
---@class vim.net.request.Opts
|
||||
---@inlinedoc
|
||||
---
|
||||
@@ -9,6 +21,9 @@ local M = {}
|
||||
---Number of retries on transient failures (default: 3).
|
||||
---@field retry? integer
|
||||
---
|
||||
---Request body for POST/PUT/PATCH requests.
|
||||
---@field body? string
|
||||
---
|
||||
---File path to save the response body to.
|
||||
---@field outpath? string
|
||||
---
|
||||
@@ -24,7 +39,7 @@ local M = {}
|
||||
---The HTTP body of the request
|
||||
---@field body string
|
||||
|
||||
--- Makes an HTTP GET request to the given URL, asynchronously passing the result to the specified
|
||||
--- Makes an HTTP request to the given URL, asynchronously passing the result to the specified
|
||||
--- `on_response`, `outpath` or `outbuf`.
|
||||
---
|
||||
--- Examples:
|
||||
@@ -56,20 +71,43 @@ local M = {}
|
||||
--- vim.net.request('https://neovim.io/charter/', {
|
||||
--- headers = { Authorization = 'Bearer XYZ' },
|
||||
--- })
|
||||
---
|
||||
--- -- POST request with body.
|
||||
--- vim.net.request('POST', 'https://example.com/api', {
|
||||
--- body = '{"key": "value"}',
|
||||
--- headers = {['Content-Type'] = 'application/json' }
|
||||
--- })
|
||||
--- ```
|
||||
---
|
||||
--- @param method vim.net.HttpMethod (default: GET) The HTTP method (GET, POST, PUT, PATCH, HEAD, DELETE).
|
||||
--- @param url string The URL for the request.
|
||||
--- @param opts? vim.net.request.Opts
|
||||
--- @param on_response? fun(err: string?, response: vim.net.request.Response?)
|
||||
--- Callback invoked on request completion. The `body` field in the response
|
||||
--- parameter contains the raw response data (text or binary).
|
||||
--- @param on_response? vim.net.request.ResponseFunc Callback invoked on request completion.
|
||||
--- @overload fun(url: string, opts: vim.net.request.Opts, response: vim.net.request.ResponseFunc)
|
||||
--- @overload fun(method: vim.net.HttpMethod, url: string, opts: vim.net.request.Opts, response: vim.net.request.ResponseFunc)
|
||||
--- @return { close: fun() } # Object with `close()` method which cancels the request.
|
||||
function M.request(url, opts, on_response)
|
||||
function M.request(method, url, opts, on_response)
|
||||
if type(url) ~= 'string' then
|
||||
---@diagnostic disable-next-line: cast-local-type
|
||||
on_response = opts
|
||||
---@diagnostic disable-next-line: no-unknown
|
||||
opts = url
|
||||
url = method
|
||||
method = 'GET'
|
||||
end
|
||||
opts = opts or {}
|
||||
|
||||
vim.validate('method', method, function(m)
|
||||
return http_methods[m] == true, ('invalid HTTP method: %s'):format(m)
|
||||
end)
|
||||
vim.validate('url', url, 'string')
|
||||
vim.validate('opts', opts, 'table', true)
|
||||
vim.validate('opts.headers', opts.headers, 'table', true)
|
||||
vim.validate('opts.body', opts.body, function(b)
|
||||
return (b == nil and true) or (type(b) == 'string' and not b:match('^@'))
|
||||
end, true, 'body should be string and not start with @')
|
||||
vim.validate('on_response', on_response, 'function', true)
|
||||
|
||||
opts = opts or {}
|
||||
local retry = opts.retry or 3
|
||||
|
||||
-- Build curl command
|
||||
@@ -85,8 +123,14 @@ function M.request(url, opts, on_response)
|
||||
vim.list_extend(args, { '--output', opts.outpath })
|
||||
end
|
||||
|
||||
-- curl -X HEAD does not work and advises to use --head instead
|
||||
vim.list_extend(args, method == 'HEAD' and { '--head' } or { '--request', method })
|
||||
|
||||
if vim.list_contains({ 'POST', 'PUT', 'PATCH' }, method) and opts.body then
|
||||
vim.list_extend(args, { '--data-binary', '@-' })
|
||||
end
|
||||
|
||||
if opts.headers then
|
||||
vim.validate('opts.headers', opts.headers, 'table', true)
|
||||
for key, value in pairs(opts.headers) do
|
||||
if type(key) ~= 'string' or type(value) ~= 'string' then
|
||||
error('headers keys and values must be strings')
|
||||
@@ -97,16 +141,17 @@ function M.request(url, opts, on_response)
|
||||
end
|
||||
|
||||
if value == '' then
|
||||
vim.list_extend(args, { '-H', key .. ';' })
|
||||
vim.list_extend(args, { '--header', key .. ';' })
|
||||
else
|
||||
vim.list_extend(args, { '-H', key .. ': ' .. value })
|
||||
vim.list_extend(args, { '--header', key .. ': ' .. value })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
table.insert(args, url)
|
||||
|
||||
local job = vim.system(args, {}, function(res)
|
||||
local system_opts = opts.body and { stdin = opts.body } or {}
|
||||
local job = vim.system(args, system_opts, function(res)
|
||||
---@type string?, vim.net.request.Response?
|
||||
local err, response = nil, nil
|
||||
if res.signal ~= 0 then
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
local n = require('test.functional.testnvim')()
|
||||
local t = require('test.testutil')
|
||||
local pcall_err = t.pcall_err
|
||||
local skip_integ = os.getenv('NVIM_TEST_INTEG') ~= '1'
|
||||
|
||||
local exec_lua = n.exec_lua
|
||||
@@ -21,6 +20,41 @@ local function assert_wrong_headers(expected_err, header)
|
||||
t.matches(expected_err, err)
|
||||
end
|
||||
|
||||
local function assert_wrong_body(expected_err, body)
|
||||
local err = t.pcall_err(exec_lua, [[
|
||||
return vim.net.request('POST', 'https://example.com', {
|
||||
body = ]] .. body .. [[,
|
||||
}, function() end)
|
||||
]])
|
||||
t.matches(expected_err, err)
|
||||
end
|
||||
|
||||
local function assert_invalid_method(method)
|
||||
local err = t.pcall_err(exec_lua, [[
|
||||
return vim.net.request(']] .. method .. [[', 'https://example.com', {}, function() end)
|
||||
]])
|
||||
t.matches('invalid HTTP method', err)
|
||||
end
|
||||
|
||||
local function request(method, opts)
|
||||
return exec_lua([[
|
||||
local done = false
|
||||
local result
|
||||
|
||||
vim.net.request("]] .. method .. [[", "https://httpbingo.org/anything", ]] .. opts .. [[, function(err, res)
|
||||
if err then
|
||||
result = { error = err }
|
||||
else
|
||||
result = vim.json.decode(res.body)
|
||||
end
|
||||
done = true
|
||||
end)
|
||||
|
||||
vim.wait(2000, function() return done end)
|
||||
return result
|
||||
]])
|
||||
end
|
||||
|
||||
describe('vim.net.request', function()
|
||||
before_each(function()
|
||||
n:clear()
|
||||
@@ -28,6 +62,7 @@ describe('vim.net.request', function()
|
||||
|
||||
it('fetches a URL into memory (async success)', function()
|
||||
t.skip(skip_integ, 'NVIM_TEST_INTEG not set: skipping network integration test')
|
||||
|
||||
local content = exec_lua([[
|
||||
local done = false
|
||||
local result
|
||||
@@ -207,6 +242,22 @@ describe('vim.net.request', function()
|
||||
t.eq(headers['Empty'][1], '', 'Expected Empty header')
|
||||
end)
|
||||
|
||||
it('accepts multiple HTTP methods', function()
|
||||
t.skip(skip_integ, 'NVIM_TEST_INTEG not set: skipping network integration test')
|
||||
|
||||
t.eq(request('GET', '{}').method, 'GET')
|
||||
t.eq(request('PUT', '{}').method, 'PUT')
|
||||
t.eq(request('PATCH', '{}').method, 'PATCH')
|
||||
t.eq(request('DELETE', '{}').method, 'DELETE')
|
||||
|
||||
---@diagnostic disable-next-line: no-unknown
|
||||
local post =
|
||||
request('POST', "{body='{\"a\": 1}', headers={['Content-Type'] = 'application/json'}}")
|
||||
t.eq(post.method, 'POST')
|
||||
t.eq(post.headers['Content-Type'][1], 'application/json')
|
||||
t.eq(post.json.a, 1)
|
||||
end)
|
||||
|
||||
it('validation', function()
|
||||
assert_wrong_headers('opts.headers: expected table, got number', '123')
|
||||
assert_wrong_headers('headers keys and values must be strings', "{ [123] = 'value' }")
|
||||
@@ -223,5 +274,16 @@ describe('vim.net.request', function()
|
||||
'header keys must not start with @ or end with : and ;',
|
||||
"{ ['@filename'] = '' }"
|
||||
)
|
||||
|
||||
assert_wrong_body('opts.body: expected body should be string and not start with @', '123')
|
||||
assert_wrong_body('opts.body: expected body should be string and not start with @', '{}')
|
||||
assert_wrong_body('opts.body: expected body should be string and not start with @', "'@test'")
|
||||
|
||||
-- OPTIONS is not accepted
|
||||
assert_invalid_method('OPTIONS')
|
||||
|
||||
-- lowercase methods are not accepted as well
|
||||
assert_invalid_method('options')
|
||||
assert_invalid_method('get')
|
||||
end)
|
||||
end)
|
||||
|
||||
Reference in New Issue
Block a user