@@ -12,6 +12,7 @@ defmodule HTTP do
1212 - **Automatic streaming**: Responses >5MB or with unknown Content-Length automatically stream
1313 - **Request cancellation**: Via `HTTP.AbortController` for aborting in-flight requests
1414 - **Promise chaining**: JavaScript-like promise interface with `then/3` support
15+ - **Unix Domain Sockets**: Support for HTTP over Unix sockets (Docker daemon, systemd, etc.)
1516 - **Telemetry integration**: Comprehensive event emission for monitoring and observability
1617 - **Zero external dependencies**: Uses only Erlang/OTP built-in modules (except telemetry)
1718
@@ -34,6 +35,12 @@ defmodule HTTP do
3435 ])
3536 |> HTTP.Promise.await()
3637
38+ # Unix Domain Socket request (e.g., Docker daemon)
39+ {:ok, response} =
40+ HTTP.fetch("http://localhost/version",
41+ unix_socket: "/var/run/docker.sock")
42+ |> HTTP.Promise.await()
43+
3744 ## Architecture
3845
3946 The library is structured around these core modules:
@@ -97,6 +104,8 @@ defmodule HTTP do
97104 (e.g., `sync: false`, `body_format: :binary`). Overrides `Request` defaults.
98105 - `:signal`: An `HTTP.AbortController` PID. If provided, the request can be aborted
99106 via this controller.
107+ - `:unix_socket`: Path to a Unix Domain Socket file (e.g., "/var/run/docker.sock").
108+ When provided, the request is sent over the Unix socket instead of TCP/IP.
100109
101110 Returns:
102111 - `%HTTP.Promise{}`: A Promise struct. The caller should `HTTP.Promise.await(promise_struct)` to get the final
@@ -227,6 +236,21 @@ defmodule HTTP do
227236 IO.puts "Request was likely aborted. Reason: \# {inspect(reason)}"
228237 end
229238 end
239+
240+ # Unix Domain Socket request to Docker daemon
241+ docker_promise = HTTP.fetch("http://localhost/version", unix_socket: "/var/run/docker.sock")
242+ case HTTP.Promise.await(docker_promise) do
243+ %HTTP.Response{status: 200} = response ->
244+ case HTTP.Response.json(response) do
245+ {:ok, json} ->
246+ IO.puts "Docker Version: \# {json["Version"]}"
247+ IO.puts "API Version: \# {json["ApiVersion"]}"
248+ {:error, reason} ->
249+ IO.inspect reason, label: "JSON Parse Error"
250+ end
251+ {:error, reason} ->
252+ IO.inspect reason, label: "Docker Request Error"
253+ end
230254 """
231255 @ spec fetch ( String . t ( ) | URI . t ( ) , Keyword . t ( ) | map ( ) ) :: % HTTP.Promise { }
232256 def fetch ( url , init \\ [ ] ) do
@@ -245,8 +269,9 @@ defmodule HTTP do
245269 options: Keyword . merge ( Request . __struct__ ( ) . options , options . opts )
246270 }
247271
248- # Extract AbortController PID from FetchOptions
272+ # Extract AbortController PID and unix_socket from FetchOptions
249273 abort_controller_pid = options . signal
274+ unix_socket_path = options . unix_socket
250275
251276 # Emit telemetry event for request start
252277 HTTP.Telemetry . request_start ( request . method , request . url , request . headers )
@@ -257,7 +282,7 @@ defmodule HTTP do
257282 :http_fetch_task_supervisor ,
258283 HTTP ,
259284 :handle_async_request ,
260- [ request , self ( ) , abort_controller_pid ]
285+ [ request , self ( ) , abort_controller_pid , unix_socket_path ]
261286 )
262287
263288 # Wrap the task in our new Promise struct
@@ -385,11 +410,49 @@ defmodule HTTP do
385410 @ spec handle_async_request (
386411 Request . t ( ) ,
387412 pid ( ) ,
388- pid ( ) | nil
413+ pid ( ) | nil ,
414+ String . t ( ) | nil
389415 ) :: Response . t ( ) | { :error , term ( ) }
390- def handle_async_request ( request , _calling_pid , abort_controller_pid ) do
416+ def handle_async_request ( request , _calling_pid , abort_controller_pid , unix_socket_path \\ nil ) do
391417 start_time = System . monotonic_time ( :microsecond )
392418
419+ # If unix_socket_path is provided, use Unix socket transport
420+ if unix_socket_path do
421+ handle_unix_socket_request ( request , unix_socket_path , start_time )
422+ else
423+ handle_httpc_request ( request , abort_controller_pid , start_time )
424+ end
425+ end
426+
427+ # Handle Unix Domain Socket requests
428+ defp handle_unix_socket_request ( request , socket_path , start_time ) do
429+ try do
430+ # Get timeout from request options
431+ timeout = Keyword . get ( request . http_options , :timeout , 30_000 )
432+
433+ case HTTP.UnixSocket . request ( socket_path , request , timeout ) do
434+ { :ok , response } ->
435+ # Emit telemetry event for request completion
436+ duration = System . monotonic_time ( :microsecond ) - start_time
437+ body_size = if is_binary ( response . body ) , do: byte_size ( response . body ) , else: 0
438+ HTTP.Telemetry . request_stop ( response . status , request . url , body_size , duration )
439+ response
440+
441+ { :error , reason } ->
442+ duration = System . monotonic_time ( :microsecond ) - start_time
443+ HTTP.Telemetry . request_exception ( request . url , reason , duration )
444+ throw ( reason )
445+ end
446+ catch
447+ reason ->
448+ duration = System . monotonic_time ( :microsecond ) - start_time
449+ HTTP.Telemetry . request_exception ( request . url , reason , duration )
450+ { :error , reason }
451+ end
452+ end
453+
454+ # Handle regular HTTP/HTTPS requests via :httpc
455+ defp handle_httpc_request ( request , abort_controller_pid , start_time ) do
393456 # Use a try/catch block to convert `throw` from handle_httpc_response into an {:error, reason} tuple
394457 try do
395458 case Request . to_httpc_args ( request ) do
0 commit comments