Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,30 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).


## [1.4.4] 2025-09-10
### Added
* Project root management for file searching operations:
* New command `:HttpSetProjectRoot [path]` to set the project root for file searching
* New command `:HttpGetProjectRoot` to display the current project root
* New command `:HttpDebugEnv` to debug environment and project root settings
* New keybindings: `<leader>hg` to set project root, `<leader>hgg` to get project root
* Enhanced `find_files` function to accept optional project root parameter
* Automatic fallback to current directory when no project root is set
* Relative path handling for environment files using project root
* Custom User-Agent header support:
* Automatic User-Agent header (`heilgar/nvim-http-client`) added to all requests
* Configurable via `user_agent` option in setup
* Only added if no User-Agent header is explicitly set in the request
* Enhanced response handler features:
* Added `response.headers.valueOf(headerName)` method for case-insensitive header lookup
* Improved header parsing to handle different formats from plenary.curl
* Better header object creation with support for both array and key-value formats

### Fixed
* Fixed keybinding typo in configuration (corrected `<header>hs` to `<leader>hs`)
* Improved health check initialization to properly load configuration
* Enhanced environment file path handling for relative paths

## [1.4.3] 2025-04-26
### Added
* Automatic file extension detection when saving responses:
Expand Down Expand Up @@ -259,4 +283,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Notes
- The plugin requires Neovim 0.5 or later
- Dependencies: plenary.nvim, telescope.nvim (optional for enhanced environment selection)

1
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ Copy this complete configuration into your Lazy.nvim setup:
request_timeout = 30000,
split_direction = "right",
create_keybindings = true,
user_agent = "heilgar/nvim-http-client", -- Custom User-Agent header

-- Profiling (timing metrics for requests)
profiling = {
Expand All @@ -66,6 +67,8 @@ Copy this complete configuration into your Lazy.nvim setup:
dry_run = "<leader>hd",
copy_curl = "<leader>hc",
save_response = "<leader>hs",
set_project_root = "<leader>hg",
get_project_root = "<leader>hgg",
},
})

Expand Down Expand Up @@ -99,6 +102,8 @@ For full configuration options, see [Configuration Documentation](doc/configurat
- `:HttpDryRun`: Perform a dry run of the request under the cursor
- `:HttpCopyCurl`: Copy the curl command for the HTTP request under the cursor
- `:HttpSaveResponse`: Save the response body to a file
- `:HttpSetProjectRoot [path]`: Set the project root for file searching operations (use without arguments to be prompted for the path)
- `:HttpGetProjectRoot`: Display the current project root for file searching operations

### Keybindings

Expand All @@ -113,6 +118,8 @@ The plugin comes with the following default keybindings (if `create_keybindings`
- `<leader>hd`: Perform dry run
- `<leader>hc`: Copy curl command
- `<leader>hs`: Save response to file
- `<leader>hg`: Set project root for file searching
- `<leader>hpg`: Get current project root

## Features

Expand All @@ -128,6 +135,7 @@ The plugin comes with the following default keybindings (if `create_keybindings`
- Request profiling with detailed timing metrics
- Telescope integration for environment selection
- Autocompletion for HTTP methods, headers and environment variables
- Custom User-Agent header (`heilgar/nvim-http-client` by default)
- Compatible with [JetBrains HTTP Client](https://www.jetbrains.com/help/idea/http-client-in-product-code-editor.html) and [VSCode Restclient](https://github.com/Huachao/vscode-restclient)

### Feature Comparison
Expand Down
33 changes: 32 additions & 1 deletion doc/response-handling.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ Within a response handler, you have access to:

- `response` - The HTTP response object
- `response.body` - The response body (parsed as JSON if possible)
- `response.headers` - Response headers
- `response.headers` - Response headers object with additional methods
- `response.headers.valueOf(headerName)` - Get header value with case-insensitive lookup
- `response.status` - HTTP status code

- `client` - The HTTP client object
Expand Down Expand Up @@ -101,6 +102,36 @@ if (adminUser) {
GET {{host}}/api/users/{{adminId}}
```

### Example: Extracting Headers

```http
### Login with Session
POST {{host}}/api/login
Content-Type: application/json

{
"username": "{{username}}",
"password": "{{password}}"
}

> {%
// Extract session ID from response headers using valueOf
const sessionId = response.headers.valueOf("mcp-session-id");
if (sessionId) {
client.global.set("session-id", sessionId);
console.log("Session ID extracted: " + sessionId);
} else {
console.log("No session ID found in response headers");
}
%}

### Use Session in Next Request
GET {{host}}/api/protected
X-Session-ID: {{session-id}}
```

**Note:** The `valueOf` method provides case-insensitive header lookup, so `response.headers.valueOf("mcp-session-id")` will work even if the actual header is `MCP-Session-ID` or `Mcp-Session-Id`.

## Saving Responses

You can save the current response body to a file using:
Expand Down
41 changes: 38 additions & 3 deletions lua/http_client/commands/utils.lua
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
local M = {}

local vvv = require('http_client.utils.verbose')
local parser = require('http_client.core.parser')
local environment = require('http_client.core.environment')
local http_client = require('http_client.core.http_client')
local curl_generator = require('http_client.core.curl_generator')
local file_utils = require('http_client.utils.file_utils')

M.copy_curl = function()
local request = parser.get_request_under_cursor()
Expand All @@ -21,5 +20,41 @@ M.copy_curl = function()
print('Curl command copied to clipboard')
end

return M
M.set_project_root = function(root_path)
if root_path and root_path ~= "" then
file_utils.set_project_root(root_path)
print(string.format("Project root set to: %s", root_path))
else
-- Prompt user for the project root path
local input = vim.fn.input("Enter project root path (or press Enter to reset to current directory): ")
if input and input ~= "" then
file_utils.set_project_root(input)
print(string.format("Project root set to: %s", input))
else
file_utils.set_project_root()
print(string.format("Project root reset to current directory: %s", vim.fn.getcwd()))
end
end
end

M.get_project_root = function()
local root = file_utils.get_project_root()
print(string.format("Current project root: %s", root))
return root
end

M.debug_env = function()
local root = file_utils.get_project_root()
local env_file = environment.get_current_env_file()

print(string.format("Project root: %s", root))
print(string.format("Current working directory: %s", vim.fn.getcwd()))
print(string.format("Environment file: %s", env_file or "None"))

if env_file then
local exists = vim.fn.filereadable(env_file) == 1
print(string.format("Environment file exists: %s", exists and "Yes" or "No"))
end
end

return M
5 changes: 4 additions & 1 deletion lua/http_client/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ M.defaults = {
request_timeout = 30000, -- 30 seconds
split_direction = "right",
create_keybindings = true,
user_agent = "heilgar/nvim-http-client", -- Default User-Agent header
profiling = {
enabled = true,
show_in_response = true,
Expand All @@ -18,8 +19,10 @@ M.defaults = {
dry_run = "<leader>hd",
toggle_verbose = "<leader>hv",
copy_curl = "<leader>hc",
save_response = "<header>hs",
save_response = "<leader>hs",
toggle_profiling = "<leader>hp",
set_project_root = "<leader>hg",
get_project_root = "<leader>hgg",
},
}

Expand Down
8 changes: 6 additions & 2 deletions lua/http_client/core/environment.lua
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ local current_env = {}
local global_variables = {}

M.set_env_file = function(file_path)
-- If the path is relative, make it absolute using the project root
if not file_path:match("^/") then
file_path = file_utils.get_project_root() .. "/" .. file_path
end

current_env_file = file_path
-- Set the private environment file path
current_private_env_file = file_path:gsub("%.env%.json$", ".private.env.json")
Expand Down Expand Up @@ -109,7 +114,7 @@ M.get_global_variables = function()
return global_variables
end

M.env_variables_needed = function (request)
M.env_variables_needed = function(request)
local function check_for_placeholders(str)
return str and str:match("{{.-}}")
end
Expand All @@ -132,4 +137,3 @@ M.env_variables_needed = function (request)
end

return M

9 changes: 9 additions & 0 deletions lua/http_client/core/parser.lua
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,15 @@ M.parse_request = function(lines)

request.response_handler = response_handler

-- Add default User-Agent header if not present
if not request.headers['User-Agent'] and not request.headers['user-agent'] then
local config = require('http_client.config')
local user_agent = config.get('user_agent')
if user_agent then
request.headers['User-Agent'] = user_agent
end
end

return request
end

Expand Down
57 changes: 55 additions & 2 deletions lua/http_client/core/response_handler.lua
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,63 @@ local client = {
}
}

local function create_headers_object(headers)
local headers_table = {}

-- Handle different header formats from plenary.curl
if headers then
for key, value in pairs(headers) do
if type(key) == "number" and type(value) == "string" then
-- Headers are in array format: ["Header-Name: value", ...]
local header_key, header_value = value:match("^(.-):%s*(.*)")
if header_key and header_value then
headers_table[header_key] = header_value
end
elseif type(key) == "string" then
-- Headers are already in key-value format
headers_table[key] = value
end
end
end

-- Create a headers object with valueOf method
local headers_obj = {}

-- Copy all header key-value pairs
for key, value in pairs(headers_table) do
headers_obj[key] = value
end

-- Add valueOf method
headers_obj.valueOf = function(header_name)
if not header_name then
return nil
end

-- Try exact match first (case-sensitive)
if headers_table[header_name] then
return headers_table[header_name]
end

-- Try case-insensitive match
for key, value in pairs(headers_table) do
if string.lower(key) == string.lower(header_name) then
return value
end
end

return nil
end

return headers_obj
end

local function create_sandbox(response)
return {
client = client,
response = {
body = response.body or {},
headers = response.headers or {},
headers = create_headers_object(response.headers),
status = response.status or nil
}
}
Expand All @@ -35,5 +86,7 @@ M.execute = function(script, response)
end
end

return M
-- Expose create_sandbox for testing
M.create_sandbox = create_sandbox

return M
7 changes: 6 additions & 1 deletion lua/http_client/health.lua
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
local health = vim.health or require("health")
local config = require("http_client.config")

local M = {}

M.check = function()
local cfg = M.config or config
if cfg.setup then
cfg.setup()
end
health.start("http_client")

-- Check if required dependencies are available
Expand Down Expand Up @@ -57,7 +62,7 @@ M.check = function()
end

-- Check if profiling is enabled
local profiling_config = M.config.get('profiling')
local profiling_config = cfg.get('profiling')
if profiling_config and profiling_config.enabled then
health.ok('Profiling: enabled')
else
Expand Down
25 changes: 23 additions & 2 deletions lua/http_client/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ local function set_keybindings()
vim.keymap.set("n", keybindings.copy_curl, ":HttpCopyCurl<CR>", opts)
vim.keymap.set("n", keybindings.save_response, ":HttpSaveResponse<CR>", opts)
vim.keymap.set("n", keybindings.toggle_profiling, ":HttpProfiling<CR>", opts)
vim.keymap.set("n", keybindings.set_project_root, ":HttpSetProjectRoot<CR>", opts)
vim.keymap.set("n", keybindings.get_project_root, ":HttpGetProjectRoot<CR>", opts)
end,
})
end
Expand Down Expand Up @@ -137,6 +139,25 @@ M.setup = function(opts)
desc = "Open the latest HTTP response buffer in a new tab",
})

vim.api.nvim_create_user_command("HttpSetProjectRoot", function(opts)
M.commands.utils.set_project_root(opts.args)
end, {
desc = "Set the project root for file searching operations. Use without arguments to be prompted for the path.",
nargs = "?",
})

vim.api.nvim_create_user_command("HttpGetProjectRoot", function()
M.commands.utils.get_project_root()
end, {
desc = "Display the current project root for file searching operations.",
})

vim.api.nvim_create_user_command("HttpDebugEnv", function()
M.commands.utils.debug_env()
end, {
desc = "Debug environment and project root settings.",
})

setup_docs()
set_keybindings()

Expand All @@ -145,12 +166,12 @@ M.setup = function(opts)
local health = vim.health or M.health
if health.register then
-- Register the health check with the new API
health.register("http_client", M.health.check)
health.register("http_client", M.health.check)
else
-- Fallback for older Neovim versions
vim.api.nvim_create_autocmd("VimEnter", {
callback = function()
M.health.check()
M.health.check()
end,
})
end
Expand Down
Loading