Skip to content

Commit

Permalink
feat(parser): huge req bodies + don't assume blobs
Browse files Browse the repository at this point in the history
closes #298 and
superseeds #299
  • Loading branch information
gorillamoe committed Nov 1, 2024
1 parent 623a26a commit e5e3e80
Show file tree
Hide file tree
Showing 4 changed files with 89 additions and 49 deletions.
36 changes: 36 additions & 0 deletions docs/docs/usage/huge-request-body.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Huge Request Body

If you try to create a request with a large body,

an error might occur due to a shell limitation of arg list size.

To avoid this error, you can use the `@write-body-to-temporary-file`
meta tag in the request section.

This tells Kulala to write the request body to a
temporary file and use the file as the request body.

:::note

For `Content-Type: multipart/form-data` this isn't not necessary,
because Kulala enforces the use of temporary files for this content type.

:::

```http title="huge-request-body.http"
# @write-body-to-temporary-file
POST https://httpbin.org/post HTTP/1.1
Content-Type: application/json
Accept: application/json
{
"name": "John",
"age": 30,
"address": "123 Main St, Springfield, IL 62701",
"phone": "555-555-5555",
"email": ""
}
```

In the example above, the request body is written to a temporary file.
1 change: 1 addition & 0 deletions docs/sidebars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const sidebars: SidebarsConfig = {
items: [
"usage/public-methods",
"usage/api",
"usage/huge-request-body",
"usage/authentication",
"usage/automatic-response-formatting",
"usage/dotenv-and-http-client.env.json-support",
Expand Down
74 changes: 41 additions & 33 deletions lua/kulala/parser/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,7 @@ M.scripts.javascript = require("kulala.parser.scripts.javascript")
---@param variables table -- The variables defined in the document
---@param env table -- The environment variables
---@param silent boolean|nil -- Whether to suppress not found variable warnings
---@param skip_blobs boolean|nil -- Whether to skip blobs, but still replace them with a placeholder
local function parse_string_variables(str, variables, env, silent, skip_blobs)
local function parse_string_variables(str, variables, env, silent)
local function replace_placeholder(variable_name)
local value
-- If the variable name contains a `$` symbol then try to parse it as a dynamic variable
Expand All @@ -32,15 +31,7 @@ local function parse_string_variables(str, variables, env, silent, skip_blobs)
value = variable_value
end
elseif variables[variable_name] then
if FS.is_blob(variables[variable_name]) then
if skip_blobs then
value = "[[binary file skipped]]"
else
value = variables[variable_name]
end
else
value = parse_string_variables(variables[variable_name], variables, env)
end
value = parse_string_variables(variables[variable_name], variables, env)
elseif env[variable_name] then
value = env[variable_name]
elseif REQUEST_VARIABLES.parse(variable_name) then
Expand Down Expand Up @@ -137,7 +128,7 @@ local function parse_body_display(body_display, variables, env, silent)
end
variables = variables or {}
env = env or {}
return parse_string_variables(body_display, variables, env, silent, true)
return parse_string_variables(body_display, variables, env, silent)
end

local function split_by_block_delimiters(text)
Expand Down Expand Up @@ -335,25 +326,16 @@ M.get_document = function()
request.body = ""
request.body_display = ""
end
-- Skip the line if it is a binary file, but add a placeholder
-- binary files should be skipped and used in a --data-binary @file notation
-- which is handled by the curl command and not sent as part of the request body string
if FS.is_blob(line) then
request.body = request.body .. "[[binary file skipped]]\r\n"
request.body_display = request.body_display .. "[[binary file skipped]]\r\n"
elseif line:find("^<") then
if line:find("^<") then
if content_type_header_value ~= nil and content_type_header_value:find("^multipart/form%-data") then
request.body = request.body .. line .. "\r\n"
request.body_display = request.body_display .. line .. "\r\n"
else
local file_path = vim.trim(line:sub(2))
local contents = FS.read_file(file_path)
if contents ~= nil and FS.is_blob(contents) then
request.body = request.body .. "[[binary file skipped]]\r\n"
request.body_display = request.body_display .. "[[binary file skipped]]\r\n"
elseif contents ~= nil then
if contents ~= nil then
request.body = request.body .. contents .. "\r\n"
request.body_display = request.body_display .. contents .. "\r\n"
request.body_display = request.body_display .. "[[external file skipped]]\r\n"
else
Logger.warn("The file '" .. file_path .. "' was not found. Skipping ...")
end
Expand Down Expand Up @@ -698,9 +680,20 @@ M.parse = function(start_request_linenr)
if is_graphql then
local gql_json = GRAPHQL_PARSER.get_json(res.body)
if gql_json then
table.insert(res.cmd, "--data")
table.insert(res.cmd, gql_json)
res.headers[content_type_header_name] = "application/json"
if PARSER_UTILS.contains_meta_tag(res, "write-body-to-temporary-file") then
local tmp_file = FS.get_temp_file(res.body)
if tmp_file ~= nil then
table.insert(res.cmd, "--data")
table.insert(res.cmd, "@" .. tmp_file)
res.headers[content_type_header_name] = "application/json"
else
Logger.error("Failed to create a temporary file for the request body")
end
else
table.insert(res.cmd, "--data")
table.insert(res.cmd, gql_json)
res.headers[content_type_header_name] = "application/json"
end
end
elseif content_type_header_value:find("^multipart/form%-data") then
local tmp_file = FS.get_binary_temp_file(res.body)
Expand All @@ -711,18 +704,33 @@ M.parse = function(start_request_linenr)
Logger.error("Failed to create a temporary file for the binary request body")
end
else
table.insert(res.cmd, "--data")
table.insert(res.cmd, res.body)
if PARSER_UTILS.contains_meta_tag(res, "write-body-to-temporary-file") then
local tmp_file = FS.get_temp_file(res.body)
if tmp_file ~= nil then
table.insert(res.cmd, "--data")
table.insert(res.cmd, "@" .. tmp_file)
else
Logger.error("Failed to create a temporary file for the request body")
end
else
table.insert(res.cmd, "--data")
table.insert(res.cmd, res.body)
end
end
else -- no content type supplied
-- check if we are a graphql query
if is_graphql then
local gql_json = GRAPHQL_PARSER.get_json(res.body)
if gql_json then
table.insert(res.cmd, "--data")
table.insert(res.cmd, gql_json)
res.headers["content-type"] = "application/json"
res.body_computed = gql_json
local tmp_file = FS.get_temp_file(res.body)
if tmp_file ~= nil then
table.insert(res.cmd, "--data")
table.insert(res.cmd, "@" .. tmp_file)
res.headers["content-type"] = "application/json"
res.body_computed = gql_json
else
Logger.error("Failed to create a temporary file for the request body")
end
end
end
end
Expand Down
27 changes: 11 additions & 16 deletions lua/kulala/utils/fs.lua
Original file line number Diff line number Diff line change
Expand Up @@ -312,22 +312,6 @@ M.get_plugin_path = function(paths)
return M.get_plugin_root_dir() .. M.ps .. table.concat(paths, M.ps)
end

---Check if a string is a blob
---@param s string
---@return boolean
M.is_blob = function(s)
-- Loop through each character in the string
for i = 1, #s do
local byte = s:byte(i)
-- Check if the byte is outside the printable ASCII range (32-126)
-- Allow tab (9), newline (10), and carriage return (13) as exceptions
if (byte < 32 or byte > 126) and byte ~= 9 and byte ~= 10 and byte ~= 13 then
return true -- If any non-printable character is found, it's likely a blob
end
end
return false -- If no non-printable characters are found, it's not a blob
end

---Read a file
---@param filename string
---@param is_binary boolean|nil
Expand All @@ -344,6 +328,17 @@ M.read_file = function(filename, is_binary)
return content
end

M.get_temp_file = function(content)
local tmp_file = vim.fn.tempname()
local f = io.open(tmp_file, "w")
if f == nil then
return nil
end
f:write(content)
f:close()
return tmp_file
end

M.get_binary_temp_file = function(content)
local tmp_file = vim.fn.tempname()
local f = io.open(tmp_file, "wb")
Expand Down

0 comments on commit e5e3e80

Please sign in to comment.