Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
b8005db
Implement graceful socket closure and add delay for buffer drainage
harmon25 Jan 11, 2026
3131f83
Improve error handling in file reading and enhance socket send logic …
harmon25 Jan 11, 2026
8f0a3c7
Improve error handling in socket receive logic to prevent premature c…
harmon25 Jan 11, 2026
bcb61c0
Enhance socket send logic with detailed logging and improved error ha…
harmon25 Jan 11, 2026
d2b4059
Optimize try_send function for improved efficiency and reduce socket …
harmon25 Jan 11, 2026
7cb9656
Refactor try_send function to handle strings and iolists separately; …
harmon25 Jan 11, 2026
dad1308
more logs
harmon25 Jan 11, 2026
8486824
no phash
harmon25 Jan 11, 2026
8abf20f
Refactor try_send_binary function to improve handling of partial send…
harmon25 Jan 11, 2026
4fb1330
Refactor HTTP response handling to use create_close_reply for consist…
harmon25 Jan 11, 2026
6502b01
Enhance logging in handle_info and try_send_binary for better error t…
harmon25 Jan 11, 2026
7ff9d79
Refactor logging in gen_tcp_server and httpd_file_handler to reduce v…
harmon25 Jan 11, 2026
8c1907a
Implement gzip support in handle_http_req; add get_accept_encoding an…
harmon25 Jan 11, 2026
1dbf366
Refactor try_send_binary to improve partial send handling; reduce wai…
harmon25 Jan 11, 2026
17826d7
Add logging for gzip file serving in serve_file function; improve err…
harmon25 Jan 11, 2026
15a5c66
Increase wait time in graceful_close to ensure lwIP completes transmi…
harmon25 Jan 11, 2026
9d77a98
Improve graceful_close to ensure proper socket shutdown and data tran…
harmon25 Jan 11, 2026
1fa5613
Refactor graceful_close to allow client-side socket closure; add logg…
harmon25 Jan 11, 2026
645560b
Add logging for send errors in handle_info; confirm successful sends
harmon25 Jan 11, 2026
9d6e557
Refactor send handling in handle_info to use spawning for non-blockin…
harmon25 Jan 11, 2026
07e7ffc
Add logging for send completion and errors in try_send_binary function
harmon25 Jan 11, 2026
d33bbbe
Add logging for send operation size and errors in handle_info
harmon25 Jan 12, 2026
ea36293
Refactor handle_info to use loop process for non-blocking sends; impr…
harmon25 Jan 12, 2026
84a3197
adds example code
harmon25 Jan 12, 2026
621ec8c
move example to erlang_example dir
harmon25 Jan 12, 2026
bfc0fca
Refactor serve_file handling in httpd_file_handler to improve respons…
harmon25 Jan 12, 2026
c5956a7
Improve try_send_binary error handling and increase retry delay for s…
harmon25 Jan 12, 2026
a40ced6
Refactor try_send_binary and call_http_req_handler for improved reada…
harmon25 Jan 12, 2026
22d50f5
Increase timeout for graceful socket closure and refactor HTTP reques…
harmon25 Jan 12, 2026
5bf5953
Enhance error handling in handle_http_req by wrapping serve_file call…
harmon25 Jan 12, 2026
672cb29
Refactor call_http_req_handler to eliminate unnecessary state updates…
harmon25 Jan 12, 2026
d2fea79
Enhance try_send_binary to handle partial sends more robustly and imp…
harmon25 Jan 12, 2026
8a9248f
Improve try_send_binary to handle partial sends more effectively and …
harmon25 Jan 12, 2026
9c7f0f5
Refactor try_send_binary to support injectable send function for impr…
harmon25 Jan 12, 2026
a70f5f9
Enhance graceful_close to handle peer closure more reliably and preve…
harmon25 Jan 12, 2026
0eb8ff0
Improve graceful_close to handle queued TX data more reliably by wait…
harmon25 Jan 13, 2026
755c326
Update rebar.lock to reference the latest commit of atomvm_httpd
harmon25 Jan 13, 2026
71f1a5d
adds examples, tests, refinement attempts
harmon25 Jan 16, 2026
4436e77
rework web assets
harmon25 Jan 16, 2026
fa79df4
Update JSON encoder to support UTF-8 and add comprehensive test cases
harmon25 Jan 16, 2026
09f8dde
Enhance API handler to return detailed system info and memory data; r…
harmon25 Jan 16, 2026
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
1 change: 1 addition & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,4 @@ Tests use ExUnit. See `test/httpd_integration_test.exs` for socket-level testing
| `include/*.hrl` | Header files - HTTP codes, trace macros |
| `priv/` | Static assets (served via `httpd_file_handler`) |
| `test/support/` | Test-only modules |
| `erlang_example` | Erlang example implementation |
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,6 @@ atomvm_httpd-*.tar
# Temporary files, for example, from tests.
/tmp/

.elixir_ls/
.elixir_ls/

.envrc
15 changes: 15 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,28 @@ Tracking document for memory leak and VM stability issues identified during code

---

## Code Quality Improvements

### 9. ✅ Replace custom iolist_length with erlang:iolist_size
**File:** `src/httpd.erl` line 582

**Problem:** Used custom `iolist_length/1` function instead of the standard `erlang:iolist_size/1` BIF which is available in AtomVM. Custom implementation works correctly but adds unnecessary code maintenance burden.

**Solution:** Replace with `erlang:iolist_size/1` and remove custom function.

**Status:** Fixed - replaced custom implementation with standard BIF, all tests passing.

---

## Progress Log

| Date | Issue # | Status | Notes |
|------|---------|--------|-------|
| 2024-12-07 | - | - | Initial review and documentation |
| 2024-12-07 | 5 | ✅ | Fixed map update operator in httpd.erl |
| 2024-12-07 | 1 | ✅ | Fixed accept loop crash recovery in gen_tcp_server.erl |
| 2026-01-11 | 9 | ✅ | Replaced custom iolist_length with erlang:iolist_size/1 |
| 2026-01-11 | - | - | Added comprehensive iolist handler tests |

---

Expand Down
4 changes: 4 additions & 0 deletions examples/elixir_http/.formatter.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]
26 changes: 26 additions & 0 deletions examples/elixir_http/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# The directory Mix will write compiled artifacts to.
/_build/

# If you run "mix test --cover", coverage assets end up here.
/cover/

# The directory Mix downloads your dependencies sources to.
/deps/

# Where third-party dependencies like ExDoc output generated docs.
/doc/

# Temporary files, for example, from tests.
/tmp/

# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump

# Also ignore archive artifacts (built via "mix archive.build").
*.ez

# Ignore package tarball (built via "mix hex.build").
elixir_http-*.tar


*.avm
21 changes: 21 additions & 0 deletions examples/elixir_http/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# ElixirHttp

**TODO: Add description**

## Installation

If [available in Hex](https://hex.pm/docs/publish), the package can be installed
by adding `elixir_http` to your list of dependencies in `mix.exs`:

```elixir
def deps do
[
{:elixir_http, "~> 0.1.0"}
]
end
```

Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
be found at <https://hexdocs.pm/elixir_http>.

82 changes: 82 additions & 0 deletions examples/elixir_http/lib/elixir_http.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
defmodule ElixirHttp do
@moduledoc """
Elixir httpd example
"""
@compile {:no_warn_undefined, :network}

def start do
:ok = start_network()

:ok = start_http()

Process.sleep(:infinity)
end

# resolved at compile time
@ssid System.get_env("SSID")
@psk System.get_env("PSK")

def start_network() do
Process.sleep(500)
config = [ssid: @ssid, psk: @psk]

case :network.wait_for_sta(config, 15_000) do
{:ok, _} ->
:ok

{:error, :disconnected} ->
# try again...
start_network()

{:error, {:already_started, _pid}} ->
Process.sleep(500)
:ok

{:error, err} ->
IO.puts("Error starting network #{inspect(err)}")

:error
end
end

defp start_http(port \\ 8080) do
config = [
# API endpoints at /api/*
{["api"],
%{
handler: :httpd_api_handler,
handler_config: %{
module: ElixirHttp.ApiHandler
}
}},
# WebSocket at /ws/*
{["ws"],
%{
handler: :httpd_ws_handler,
handler_config: %{
module: ElixirHttp.WsHandler
}
}},
# Static files from priv/ at root
{[],
%{
handler: :httpd_file_handler,
handler_config: %{
app: :elixir_http
}
}}
]

IO.puts("Starting httpd on port #{port}...")

case :httpd.start(port, config) do
{:ok, _pid} ->
IO.puts("httpd started.")
:ok

error ->
IO.puts("An error occurred: #{inspect(error)}")
:error
end
end
end
56 changes: 56 additions & 0 deletions examples/elixir_http/lib/elixir_http/api_handler.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
defmodule ElixirHttp.ApiHandler do
@compile {:no_warn_undefined, :atomvm}

@moduledoc """
API handler for system info and memory endpoints.
Implements the httpd_api_handler behavior.
"""

@behaviour :httpd_api_handler

@impl true
def handle_api_request(:get, ["system_info"], _http_request, _args) do
# = Map.get(http_request, :socket)
# {:ok, %{addr: host, port: port}} = :socket.peername(socket)
# IO.puts("GET system_info request from #{inspect(host)}:#{port}")

result = %{
platform: :atomvm.platform(),
system_architecture: :erlang.system_info(:system_architecture),
atomvm_version: :erlang.system_info(:atomvm_version),
word_size: :erlang.system_info(:wordsize),
esp32_chip_info: get_esp32_chip_info(),
esp_idf_version: :erlang.system_info(:esp_idf_version)
}

{:ok, result}
end

def handle_api_request(:get, ["memory"], _http_request, _args) do
# socket = Map.get(http_request, :socket)
# {:ok, %{addr: host, port: port}} = :socket.peername(socket)
# IO.puts("GET memory request from #{inspect(host)}:#{port}")

{:ok, get_memory_data()}
end

def handle_api_request(method, path, _http_request, _args) do
IO.puts("ERROR! Unsupported method #{inspect(method)} or path #{inspect(path)}")
{:error, :not_found}
end

def get_memory_data() do
%{
atom_count: :erlang.system_info(:atom_count),
process_count: :erlang.system_info(:process_count),
port_count: :erlang.system_info(:port_count),
esp32_free_heap_size: :erlang.system_info(:esp32_free_heap_size),
esp32_largest_free_block: :erlang.system_info(:esp32_largest_free_block),
esp32_minimum_free_size: :erlang.system_info(:esp32_minimum_free_size)
}
end

defp get_esp32_chip_info() do
:erlang.system_info(:esp32_chip_info)
end
end
56 changes: 56 additions & 0 deletions examples/elixir_http/lib/elixir_http/ws_handler.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
defmodule ElixirHttp.WsHandler do
@moduledoc """
WebSocket handler that sends memory updates to connected clients.
Implements the httpd_ws_handler behavior.
"""

@behaviour :httpd_ws_handler

@impl true
def handle_ws_init(websocket, _path, _args) do
IO.puts("Initializing websocket pid=#{inspect(self())}")
last_memory = ElixirHttp.ApiHandler.get_memory_data()
spawn(fn -> update_loop(websocket, last_memory) end)
{:ok, nil}
end

@impl true
def handle_ws_message("ping", state) do
{:reply, "pong", state}
end

def handle_ws_message(message, state) do
# IO.puts("Received message from web socket. Message: #{inspect(message)}")
{:noreply, state}
end

defp update_loop(websocket, last_memory_data) do
Process.sleep(5000)
latest_memory_data = ElixirHttp.ApiHandler.get_memory_data()

new_memory_data = get_difference(last_memory_data, latest_memory_data)

case new_memory_data do
[] ->
:ok

_ ->
binary = :erlang.iolist_to_binary(:json_encoder.encode(Map.new(new_memory_data)))
# IO.puts("Sending websocket message to client #{inspect(binary)} ... ")
:httpd_ws_handler.send(websocket, binary)
IO.puts("sent.")
end

update_loop(websocket, latest_memory_data)
end

defp get_difference(map1, map2) do
Enum.reduce(map1, [], fn {key, value}, acc ->
case Map.get(map2, key) do
nil -> [{key, value} | acc]
^value -> acc
new_value -> [{key, new_value} | acc]
end
end)
end
end
37 changes: 37 additions & 0 deletions examples/elixir_http/mix.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
defmodule ElixirHttp.MixProject do
use Mix.Project

def project do
[
app: :elixir_http,
version: "0.1.0",
elixir: "~> 1.19",
start_permanent: Mix.env() == :prod,
deps: deps(),
atomvm: [
# Change to Lux.Test for minimal testing without networking
start: ElixirHttp,
flash_offset: 0x250000
]
]
end

# Run "mix help compile.app" to learn about applications.
def application do
[
extra_applications: [:logger]
]
end

# Run "mix help deps" to learn about dependencies.
defp deps do
[
{:pythonx, "~> 0.4.7", runtime: false},
{:exatomvm, github: "atomvm/ExAtomVM", runtime: false},
{:atomvm_httpd, path: "../../"},
# Test-only dependencies
{:req, "~> 0.5", only: :test, runtime: false},
{:websockex, "~> 0.4.3", only: :test, runtime: false}
]
end
end
18 changes: 18 additions & 0 deletions examples/elixir_http/mix.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
%{
"cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"},
"elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"},
"exatomvm": {:git, "https://github.com/atomvm/ExAtomVM.git", "0f9351417673d347d442c3283cca1bfeb47458f6", []},
"finch": {:hex, :finch, "0.20.0", "5330aefb6b010f424dcbbc4615d914e9e3deae40095e73ab0c1bb0968933cadf", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2658131a74d051aabfcba936093c903b8e89da9a1b63e430bee62045fa9b2ee2"},
"fine": {:hex, :fine, "0.1.4", "b19a89c1476c7c57afb5f9314aed5960b5bc95d5277de4cb5ee8e1d1616ce379", [:mix], [], "hexpm", "be3324cc454a42d80951cf6023b9954e9ff27c6daa255483b3e8d608670303f5"},
"hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"},
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
"mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"},
"mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"},
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
"nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
"pythonx": {:hex, :pythonx, "0.4.7", "604a3a78377abdaa8739c561cb871c856b0e80d25fd057277839912017004af0", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.9", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:fine, "~> 0.1.2", [hex: :fine, repo: "hexpm", optional: false]}], "hexpm", "20d8b456df995e6ccd6d88dcf118ba80464194515f71a5c89aacdb824d235c52"},
"req": {:hex, :req, "0.5.17", "0096ddd5b0ed6f576a03dde4b158a0c727215b15d2795e59e0916c6971066ede", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0b8bc6ffdfebbc07968e59d3ff96d52f2202d0536f10fef4dc11dc02a2a43e39"},
"telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
"uf2tool": {:hex, :uf2tool, "1.1.0", "7091931234ca5a256b66ea691983867d51229798622d083f85c1ad779798a734", [:rebar3], [], "hexpm", "1a7e5ca7ef3d19c7a0b0acf3db804b3188e0980884acffa13fd57d733507b73d"},
"websockex": {:hex, :websockex, "0.4.3", "92b7905769c79c6480c02daacaca2ddd49de936d912976a4d3c923723b647bf0", [:mix], [], "hexpm", "95f2e7072b85a3a4cc385602d42115b73ce0b74a9121d0d6dbbf557645ac53e4"},
}
Binary file added examples/elixir_http/priv/favicon.ico
Binary file not shown.
Loading