Skip to content

Commit f63f8dc

Browse files
committed
feat: Add HTTP.Headers enhancements and default User-Agent
- Added HTTP.Headers.set_default/3 method for conditional header setting - Added automatic User-Agent header with system architecture information - Added HTTP.Headers.user_agent/0 method to access default user agent string - Updated version to 0.4.3 - Enhanced changelog with new 0.4.3 features - Updated README.md with new HTTP.Headers documentation
1 parent af2adee commit f63f8dc

File tree

3 files changed

+132
-2
lines changed

3 files changed

+132
-2
lines changed

lib/http/headers.ex

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,4 +330,87 @@ defmodule HTTP.Headers do
330330
def to_list(%__MODULE__{headers: headers}) do
331331
headers
332332
end
333+
334+
@doc """
335+
Sets a header only if it doesn't already exist (case-insensitive).
336+
337+
## Examples
338+
iex> headers = HTTP.Headers.new([{"Content-Type", "text/plain"}])
339+
iex> updated = HTTP.Headers.set_default(headers, "Content-Type", "application/json")
340+
iex> HTTP.Headers.get(updated, "Content-Type")
341+
"text/plain"
342+
343+
iex> headers = HTTP.Headers.new([{"Accept", "text/html"}])
344+
iex> updated = HTTP.Headers.set_default(headers, "User-Agent", "CustomAgent/1.0")
345+
iex> HTTP.Headers.get(updated, "User-Agent")
346+
"CustomAgent/1.0"
347+
348+
iex> headers = HTTP.Headers.new()
349+
iex> updated = HTTP.Headers.set_default(headers, "Authorization", "Bearer token")
350+
iex> HTTP.Headers.get(updated, "Authorization")
351+
"Bearer token"
352+
"""
353+
@spec set_default(t(), String.t(), String.t()) :: t()
354+
def set_default(%__MODULE__{headers: headers} = headers_struct, name, value)
355+
when is_binary(name) and is_binary(value) do
356+
normalized_name = String.downcase(name)
357+
358+
# Check if header already exists
359+
header_exists =
360+
Enum.any?(headers, fn {header_name, _} ->
361+
String.downcase(header_name) == normalized_name
362+
end)
363+
364+
if header_exists do
365+
headers_struct
366+
else
367+
normalized_header_name = normalize_name(name)
368+
%{headers_struct | headers: [{normalized_header_name, value} | headers]}
369+
end
370+
end
371+
372+
@doc """
373+
Returns the default User-Agent string used by the library.
374+
375+
## Examples
376+
iex> user_agent = HTTP.Headers.user_agent()
377+
iex> user_agent =~ "Mozilla/5.0"
378+
true
379+
iex> user_agent =~ "http_fetch/"
380+
true
381+
"""
382+
@spec user_agent() :: String.t()
383+
def user_agent() do
384+
os_info = get_os_info()
385+
arch_info = get_arch_info()
386+
otp_version = System.otp_release()
387+
elixir_version = System.version()
388+
389+
beam_version = :erlang.system_info(:version)
390+
http_fetch_version = Application.spec(:http_fetch, :vsn) |> to_string()
391+
392+
"Mozilla/5.0 (#{os_info}; #{arch_info}) OTP/#{otp_version} BEAM/#{beam_version} Elixir/#{elixir_version} http_fetch/#{http_fetch_version}"
393+
end
394+
395+
@spec get_os_info() :: String.t()
396+
defp get_os_info() do
397+
case :os.type() do
398+
{:unix, :darwin} ->
399+
"macOS"
400+
401+
{:unix, :linux} ->
402+
"Linux"
403+
404+
{:win32, _} ->
405+
"Windows"
406+
407+
{_, os} ->
408+
Atom.to_string(os)
409+
end
410+
end
411+
412+
@spec get_arch_info() :: String.t()
413+
defp get_arch_info() do
414+
to_string(:erlang.system_info(:system_architecture))
415+
end
333416
end

lib/http/request.ex

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,10 @@ defmodule HTTP.Request do
3939
def to_httpc_args(%__MODULE__{} = req) do
4040
method = req.method
4141
url = req.url |> URI.to_string() |> to_charlist()
42-
headers = Enum.map(req.headers.headers, fn {k, v} -> {to_charlist(k), to_charlist(v)} end)
42+
43+
# Add default user agent if not provided
44+
headers = add_default_user_agent(req.headers.headers)
45+
headers = Enum.map(headers, fn {k, v} -> {to_charlist(k), to_charlist(v)} end)
4346

4447
request_tuple =
4548
case method do
@@ -81,4 +84,19 @@ defmodule HTTP.Request do
8184
defp to_body(body) when is_binary(body), do: String.to_charlist(body)
8285
defp to_body(body) when is_list(body), do: body
8386
defp to_body(other), do: String.to_charlist(to_string(other))
87+
88+
@spec add_default_user_agent([{String.t(), String.t()}]) :: [{String.t(), String.t()}]
89+
defp add_default_user_agent(headers) do
90+
# Check if user agent is already provided (case-insensitive)
91+
has_user_agent =
92+
Enum.any?(headers, fn {name, _value} ->
93+
String.downcase(name) == "user-agent"
94+
end)
95+
96+
if has_user_agent do
97+
headers
98+
else
99+
[{"User-Agent", HTTP.Headers.user_agent()} | headers]
100+
end
101+
end
84102
end

test/http/request_test.exs

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,36 @@ defmodule HTTP.RequestTest do
3737

3838
[method, request_tuple, _http_options, _options] = HTTP.Request.to_httpc_args(request)
3939
assert method == :get
40-
assert request_tuple == {~c"http://example.com", [{~c"Accept", ~c"application/json"}]}
40+
assert {~c"http://example.com", headers} = request_tuple
41+
42+
assert Enum.any?(headers, fn {name, value} ->
43+
to_string(name) == "User-Agent" and to_string(value) =~ "Mozilla/5.0"
44+
end)
45+
46+
assert Enum.any?(headers, fn {name, value} ->
47+
to_string(name) == "Accept" and to_string(value) == "application/json"
48+
end)
49+
end
50+
51+
test "does not override provided user agent" do
52+
request = %HTTP.Request{
53+
method: :get,
54+
url: URI.parse("http://example.com"),
55+
headers: HTTP.Headers.new([{"User-Agent", "CustomAgent/1.0"}])
56+
}
57+
58+
[method, request_tuple, _http_options, _options] = HTTP.Request.to_httpc_args(request)
59+
assert method == :get
60+
assert {~c"http://example.com", headers} = request_tuple
61+
62+
assert Enum.any?(headers, fn {name, value} ->
63+
to_string(name) == "User-Agent" and to_string(value) == "CustomAgent/1.0"
64+
end)
65+
66+
# Should not have the default user agent
67+
refute Enum.any?(headers, fn {name, value} ->
68+
to_string(name) == "User-Agent" and to_string(value) =~ "http_fetch"
69+
end)
4170
end
4271
end
4372
end

0 commit comments

Comments
 (0)