Skip to content

Commit 6b5fda0

Browse files
authored
add headers handling (#10)
1 parent 44b5d38 commit 6b5fda0

File tree

15 files changed

+513
-16
lines changed

15 files changed

+513
-16
lines changed

CHANGELOG.md

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,30 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

88

9+
## [1.4.4] 2025-09-10
10+
### Added
11+
* Project root management for file searching operations:
12+
* New command `:HttpSetProjectRoot [path]` to set the project root for file searching
13+
* New command `:HttpGetProjectRoot` to display the current project root
14+
* New command `:HttpDebugEnv` to debug environment and project root settings
15+
* New keybindings: `<leader>hg` to set project root, `<leader>hgg` to get project root
16+
* Enhanced `find_files` function to accept optional project root parameter
17+
* Automatic fallback to current directory when no project root is set
18+
* Relative path handling for environment files using project root
19+
* Custom User-Agent header support:
20+
* Automatic User-Agent header (`heilgar/nvim-http-client`) added to all requests
21+
* Configurable via `user_agent` option in setup
22+
* Only added if no User-Agent header is explicitly set in the request
23+
* Enhanced response handler features:
24+
* Added `response.headers.valueOf(headerName)` method for case-insensitive header lookup
25+
* Improved header parsing to handle different formats from plenary.curl
26+
* Better header object creation with support for both array and key-value formats
27+
28+
### Fixed
29+
* Fixed keybinding typo in configuration (corrected `<header>hs` to `<leader>hs`)
30+
* Improved health check initialization to properly load configuration
31+
* Enhanced environment file path handling for relative paths
32+
933
## [1.4.3] 2025-04-26
1034
### Added
1135
* Automatic file extension detection when saving responses:
@@ -259,4 +283,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
259283
### Notes
260284
- The plugin requires Neovim 0.5 or later
261285
- Dependencies: plenary.nvim, telescope.nvim (optional for enhanced environment selection)
262-
286+
1

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ Copy this complete configuration into your Lazy.nvim setup:
4747
request_timeout = 30000,
4848
split_direction = "right",
4949
create_keybindings = true,
50+
user_agent = "heilgar/nvim-http-client", -- Custom User-Agent header
5051

5152
-- Profiling (timing metrics for requests)
5253
profiling = {
@@ -66,6 +67,8 @@ Copy this complete configuration into your Lazy.nvim setup:
6667
dry_run = "<leader>hd",
6768
copy_curl = "<leader>hc",
6869
save_response = "<leader>hs",
70+
set_project_root = "<leader>hg",
71+
get_project_root = "<leader>hgg",
6972
},
7073
})
7174

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

103108
### Keybindings
104109

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

117124
## Features
118125

@@ -128,6 +135,7 @@ The plugin comes with the following default keybindings (if `create_keybindings`
128135
- Request profiling with detailed timing metrics
129136
- Telescope integration for environment selection
130137
- Autocompletion for HTTP methods, headers and environment variables
138+
- Custom User-Agent header (`heilgar/nvim-http-client` by default)
131139
- 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)
132140

133141
### Feature Comparison

doc/response-handling.md

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ Within a response handler, you have access to:
4545

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

5152
- `client` - The HTTP client object
@@ -101,6 +102,36 @@ if (adminUser) {
101102
GET {{host}}/api/users/{{adminId}}
102103
```
103104

105+
### Example: Extracting Headers
106+
107+
```http
108+
### Login with Session
109+
POST {{host}}/api/login
110+
Content-Type: application/json
111+
112+
{
113+
"username": "{{username}}",
114+
"password": "{{password}}"
115+
}
116+
117+
> {%
118+
// Extract session ID from response headers using valueOf
119+
const sessionId = response.headers.valueOf("mcp-session-id");
120+
if (sessionId) {
121+
client.global.set("session-id", sessionId);
122+
console.log("Session ID extracted: " + sessionId);
123+
} else {
124+
console.log("No session ID found in response headers");
125+
}
126+
%}
127+
128+
### Use Session in Next Request
129+
GET {{host}}/api/protected
130+
X-Session-ID: {{session-id}}
131+
```
132+
133+
**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`.
134+
104135
## Saving Responses
105136

106137
You can save the current response body to a file using:

lua/http_client/commands/utils.lua

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
local M = {}
22

3-
local vvv = require('http_client.utils.verbose')
43
local parser = require('http_client.core.parser')
54
local environment = require('http_client.core.environment')
6-
local http_client = require('http_client.core.http_client')
75
local curl_generator = require('http_client.core.curl_generator')
6+
local file_utils = require('http_client.utils.file_utils')
87

98
M.copy_curl = function()
109
local request = parser.get_request_under_cursor()
@@ -21,5 +20,41 @@ M.copy_curl = function()
2120
print('Curl command copied to clipboard')
2221
end
2322

24-
return M
23+
M.set_project_root = function(root_path)
24+
if root_path and root_path ~= "" then
25+
file_utils.set_project_root(root_path)
26+
print(string.format("Project root set to: %s", root_path))
27+
else
28+
-- Prompt user for the project root path
29+
local input = vim.fn.input("Enter project root path (or press Enter to reset to current directory): ")
30+
if input and input ~= "" then
31+
file_utils.set_project_root(input)
32+
print(string.format("Project root set to: %s", input))
33+
else
34+
file_utils.set_project_root()
35+
print(string.format("Project root reset to current directory: %s", vim.fn.getcwd()))
36+
end
37+
end
38+
end
39+
40+
M.get_project_root = function()
41+
local root = file_utils.get_project_root()
42+
print(string.format("Current project root: %s", root))
43+
return root
44+
end
45+
46+
M.debug_env = function()
47+
local root = file_utils.get_project_root()
48+
local env_file = environment.get_current_env_file()
2549

50+
print(string.format("Project root: %s", root))
51+
print(string.format("Current working directory: %s", vim.fn.getcwd()))
52+
print(string.format("Environment file: %s", env_file or "None"))
53+
54+
if env_file then
55+
local exists = vim.fn.filereadable(env_file) == 1
56+
print(string.format("Environment file exists: %s", exists and "Yes" or "No"))
57+
end
58+
end
59+
60+
return M

lua/http_client/config.lua

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ M.defaults = {
55
request_timeout = 30000, -- 30 seconds
66
split_direction = "right",
77
create_keybindings = true,
8+
user_agent = "heilgar/nvim-http-client", -- Default User-Agent header
89
profiling = {
910
enabled = true,
1011
show_in_response = true,
@@ -18,8 +19,10 @@ M.defaults = {
1819
dry_run = "<leader>hd",
1920
toggle_verbose = "<leader>hv",
2021
copy_curl = "<leader>hc",
21-
save_response = "<header>hs",
22+
save_response = "<leader>hs",
2223
toggle_profiling = "<leader>hp",
24+
set_project_root = "<leader>hg",
25+
get_project_root = "<leader>hgg",
2326
},
2427
}
2528

lua/http_client/core/environment.lua

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ local current_env = {}
77
local global_variables = {}
88

99
M.set_env_file = function(file_path)
10+
-- If the path is relative, make it absolute using the project root
11+
if not file_path:match("^/") then
12+
file_path = file_utils.get_project_root() .. "/" .. file_path
13+
end
14+
1015
current_env_file = file_path
1116
-- Set the private environment file path
1217
current_private_env_file = file_path:gsub("%.env%.json$", ".private.env.json")
@@ -109,7 +114,7 @@ M.get_global_variables = function()
109114
return global_variables
110115
end
111116

112-
M.env_variables_needed = function (request)
117+
M.env_variables_needed = function(request)
113118
local function check_for_placeholders(str)
114119
return str and str:match("{{.-}}")
115120
end
@@ -132,4 +137,3 @@ M.env_variables_needed = function (request)
132137
end
133138

134139
return M
135-

lua/http_client/core/parser.lua

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,15 @@ M.parse_request = function(lines)
160160

161161
request.response_handler = response_handler
162162

163+
-- Add default User-Agent header if not present
164+
if not request.headers['User-Agent'] and not request.headers['user-agent'] then
165+
local config = require('http_client.config')
166+
local user_agent = config.get('user_agent')
167+
if user_agent then
168+
request.headers['User-Agent'] = user_agent
169+
end
170+
end
171+
163172
return request
164173
end
165174

lua/http_client/core/response_handler.lua

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,63 @@ local client = {
1111
}
1212
}
1313

14+
local function create_headers_object(headers)
15+
local headers_table = {}
16+
17+
-- Handle different header formats from plenary.curl
18+
if headers then
19+
for key, value in pairs(headers) do
20+
if type(key) == "number" and type(value) == "string" then
21+
-- Headers are in array format: ["Header-Name: value", ...]
22+
local header_key, header_value = value:match("^(.-):%s*(.*)")
23+
if header_key and header_value then
24+
headers_table[header_key] = header_value
25+
end
26+
elseif type(key) == "string" then
27+
-- Headers are already in key-value format
28+
headers_table[key] = value
29+
end
30+
end
31+
end
32+
33+
-- Create a headers object with valueOf method
34+
local headers_obj = {}
35+
36+
-- Copy all header key-value pairs
37+
for key, value in pairs(headers_table) do
38+
headers_obj[key] = value
39+
end
40+
41+
-- Add valueOf method
42+
headers_obj.valueOf = function(header_name)
43+
if not header_name then
44+
return nil
45+
end
46+
47+
-- Try exact match first (case-sensitive)
48+
if headers_table[header_name] then
49+
return headers_table[header_name]
50+
end
51+
52+
-- Try case-insensitive match
53+
for key, value in pairs(headers_table) do
54+
if string.lower(key) == string.lower(header_name) then
55+
return value
56+
end
57+
end
58+
59+
return nil
60+
end
61+
62+
return headers_obj
63+
end
64+
1465
local function create_sandbox(response)
1566
return {
1667
client = client,
1768
response = {
1869
body = response.body or {},
19-
headers = response.headers or {},
70+
headers = create_headers_object(response.headers),
2071
status = response.status or nil
2172
}
2273
}
@@ -35,5 +86,7 @@ M.execute = function(script, response)
3586
end
3687
end
3788

38-
return M
89+
-- Expose create_sandbox for testing
90+
M.create_sandbox = create_sandbox
3991

92+
return M

lua/http_client/health.lua

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
local health = vim.health or require("health")
2+
local config = require("http_client.config")
23

34
local M = {}
45

56
M.check = function()
7+
local cfg = M.config or config
8+
if cfg.setup then
9+
cfg.setup()
10+
end
611
health.start("http_client")
712

813
-- Check if required dependencies are available
@@ -57,7 +62,7 @@ M.check = function()
5762
end
5863

5964
-- Check if profiling is enabled
60-
local profiling_config = M.config.get('profiling')
65+
local profiling_config = cfg.get('profiling')
6166
if profiling_config and profiling_config.enabled then
6267
health.ok('Profiling: enabled')
6368
else

lua/http_client/init.lua

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ local function set_keybindings()
3636
vim.keymap.set("n", keybindings.copy_curl, ":HttpCopyCurl<CR>", opts)
3737
vim.keymap.set("n", keybindings.save_response, ":HttpSaveResponse<CR>", opts)
3838
vim.keymap.set("n", keybindings.toggle_profiling, ":HttpProfiling<CR>", opts)
39+
vim.keymap.set("n", keybindings.set_project_root, ":HttpSetProjectRoot<CR>", opts)
40+
vim.keymap.set("n", keybindings.get_project_root, ":HttpGetProjectRoot<CR>", opts)
3941
end,
4042
})
4143
end
@@ -137,6 +139,25 @@ M.setup = function(opts)
137139
desc = "Open the latest HTTP response buffer in a new tab",
138140
})
139141

142+
vim.api.nvim_create_user_command("HttpSetProjectRoot", function(opts)
143+
M.commands.utils.set_project_root(opts.args)
144+
end, {
145+
desc = "Set the project root for file searching operations. Use without arguments to be prompted for the path.",
146+
nargs = "?",
147+
})
148+
149+
vim.api.nvim_create_user_command("HttpGetProjectRoot", function()
150+
M.commands.utils.get_project_root()
151+
end, {
152+
desc = "Display the current project root for file searching operations.",
153+
})
154+
155+
vim.api.nvim_create_user_command("HttpDebugEnv", function()
156+
M.commands.utils.debug_env()
157+
end, {
158+
desc = "Debug environment and project root settings.",
159+
})
160+
140161
setup_docs()
141162
set_keybindings()
142163

@@ -145,12 +166,12 @@ M.setup = function(opts)
145166
local health = vim.health or M.health
146167
if health.register then
147168
-- Register the health check with the new API
148-
health.register("http_client", M.health.check)
169+
health.register("http_client", M.health.check)
149170
else
150171
-- Fallback for older Neovim versions
151172
vim.api.nvim_create_autocmd("VimEnter", {
152173
callback = function()
153-
M.health.check()
174+
M.health.check()
154175
end,
155176
})
156177
end

0 commit comments

Comments
 (0)