http-nu
lets you attach a Nushell closure to an HTTP
interface. If you prefer POSIX to Nushell, this
project has a cousin called http-sh.
cargo install http-nu --locked
$ http-nu :3001 '{|req| "Hello world"}'
$ curl -s localhost:3001
Hello world
You can listen to UNIX domain sockets as well
$ http-nu ./sock '{|req| "Hello world"}'
$ curl -s --unix-socket ./sock localhost
Hello world
$ http-nu :3001 '{|req| $in}'
$ curl -s -d Hai localhost:3001
Hai
The Request metadata is passed as an argument to the closure.
$ http-nu :3001 '{|req| $req}'
$ curl -s 'localhost:3001/segment?foo=bar&abc=123' # or
$ http get 'http://localhost:3001/segment?foo=bar&abc=123'
─────────────┬───────────────────────────────
proto │ HTTP/1.1
method │ GET
uri │ /segment?foo=bar&abc=123
path │ /segment
remote_ip │ 127.0.0.1
remote_port │ 52007
│ ────────────┬────────────────
headers │ host │ localhost:3001
│ user-agent │ curl/8.7.1
│ accept │ */*
│ ────────────┴────────────────
│ ─────┬─────
query │ abc │ 123
│ foo │ bar
│ ─────┴─────
─────────────┴───────────────────────────────
$ http-nu :3001 '{|req| $"hello: ($req.path)"}'
$ http get 'http://localhost:3001/yello'
hello: /yello
You can set the Response metadata using the .response
custom command.
.response {
status: <number> # Optional, HTTP status code (default: 200)
headers: { # Optional, HTTP headers
<key>: <value>
}
}
$ http-nu :3001 '{|req| .response {status: 404}; "sorry, eh"}'
$ curl -si localhost:3001
HTTP/1.1 404 Not Found
transfer-encoding: chunked
date: Fri, 31 Jan 2025 08:20:28 GMT
sorry, eh
Content-type is determined in the following order of precedence:
-
Headers set via
.response
command:.response { headers: { "Content-Type": "text/plain" } }
-
Pipeline metadata content-type (e.g., from
to yaml
) -
For Record values with no content-type, defaults to
application/json
-
Otherwise defaults to
text/html; charset=utf-8
Examples:
# 1. Explicit header takes precedence
{|req| .response {headers: {"Content-Type": "text/plain"}}; {foo: "bar"} } # Returns as text/plain
# 2. Pipeline metadata
{|req| ls | to yaml } # Returns as application/x-yaml
# 3. Record auto-converts to JSON
{|req| {foo: "bar"} } # Returns as application/json
# 4. Default
{|req| "Hello" } # Returns as text/html; charset=utf-8
Values returned by streaming pipelines (like generate
) are sent to the client
immediately as HTTP chunks. This allows real-time data transmission without
waiting for the entire response to be ready.
$ http-nu :3001 '{|req|
.response {status: 200}
generate {|_|
sleep 1sec
{out: (date now | to text | $in + "\n") next: true }
} true
}'
$ curl -s localhost:3001
Fri, 31 Jan 2025 03:47:59 -0500 (now)
Fri, 31 Jan 2025 03:48:00 -0500 (now)
Fri, 31 Jan 2025 03:48:01 -0500 (now)
Fri, 31 Jan 2025 03:48:02 -0500 (now)
Fri, 31 Jan 2025 03:48:03 -0500 (now)
...
TODO: we should provide a to sse
built-in
$ http-nu :3001 '{|req|
.response {headers: {"content-type": "text/event-stream"}}
tail -F source.json | lines | each {|line| $"data: ($line)\n\n"}
}'
# simulate generating events in a seperate process
$ loop {
{date: (date now)} | to json -r | $in + "\n" | save -a source.json
sleep 1sec
}
$ curl -si localhost:3001/
HTTP/1.1 200 OK
content-type: text/event-stream
transfer-encoding: chunked
date: Fri, 31 Jan 2025 09:01:20 GMT
data: {"date":"2025-01-31 04:01:23.371514 -05:00"}
data: {"date":"2025-01-31 04:01:24.376864 -05:00"}
data: {"date":"2025-01-31 04:01:25.382756 -05:00"}
data: {"date":"2025-01-31 04:01:26.385418 -05:00"}
data: {"date":"2025-01-31 04:01:27.387723 -05:00"}
data: {"date":"2025-01-31 04:01:28.390407 -05:00"}
...