nanonext provides high-performance HTTP/WebSocket client and server capabilities built on NNG.
1. HTTP Client
ncurl: Basic Requests
ncurl() is a minimalist HTTP(S) client. Basic usage
requires only a URL.
ncurl("https://postman-echo.com/get")
#> $status
#> [1] 200
#>
#> $headers
#> NULL
#>
#> $data
#> [1] "{\"args\":{},\"headers\":{\"host\":\"postman-echo.com\",\"accept-encoding\":\"gzip, br\",\"x-forwarded-proto\":\"https\"},\"url\":\"https://postman-echo.com/get\"}"Advanced usage supports all HTTP methods (POST, PUT, DELETE, etc.), custom headers, and request bodies.
ncurl("https://postman-echo.com/post",
method = "POST",
headers = c(`Content-Type` = "application/json", Authorization = "Bearer APIKEY"),
data = '{"key": "value"}',
response = "date")
#> $status
#> [1] 200
#>
#> $headers
#> $headers$date
#> [1] "Fri, 06 Feb 2026 11:40:34 GMT"
#>
#>
#> $data
#> [1] "{\"args\":{},\"data\":{\"key\":\"value\"},\"files\":{},\"form\":{},\"headers\":{\"host\":\"postman-echo.com\",\"accept-encoding\":\"gzip, br\",\"x-forwarded-proto\":\"https\",\"content-type\":\"application/json\",\"authorization\":\"Bearer APIKEY\",\"content-length\":\"16\"},\"json\":{\"key\":\"value\"},\"url\":\"https://postman-echo.com/post\"}"Specify response = TRUE to return all response
headers.
ncurl("https://postman-echo.com/get",
response = TRUE)
#> $status
#> [1] 200
#>
#> $headers
#> $headers$Date
#> [1] "Fri, 06 Feb 2026 11:40:35 GMT"
#>
#> $headers$`Content-Type`
#> [1] "application/json; charset=utf-8"
#>
#> $headers$`Content-Length`
#> [1] "143"
#>
#> $headers$Connection
#> [1] "close"
#>
#> $headers$`CF-RAY`
#> [1] "9c9a5f9f49da1991-LHR"
#>
#> $headers$etag
#> [1] "W/\"8f-7zN8nSad8A9WlFJjKQZB04z5nHE\""
#>
#> $headers$vary
#> [1] "Accept-Encoding"
#>
#> $headers$`Set-Cookie`
#> [1] "sails.sid=s%3AG3rHD5JFh7xQGUWN5uKuZy_VAx8K3jK4.Xri5sb2DFpVzmoUSCvFw94mJ43QzXCamX%2BwID26YXto; Path=/; HttpOnly, __cf_bm=H5EueP1cHEOyPVaqT8oVWyH_uv7aytH6JhNh2rCG404-1770378035-1.0.1.1-ANKBTHHSsgxGVLTOHVCJzq.UIh0IiDdIxmhIzLvqpME_Ro11PPGcm5s2JmnC5Z48xSIlvWQSu7a91nUWgqBuK7vSnj95x_c7DcOnlVeZxYk; path=/; expires=Fri, 06-Feb-26 12:10:35 GMT; domain=.postman-echo.com; HttpOnly; Secure, _cfuvid=QGAx0msEdEdqhBGVmwTk7S6xsOpPf_hffh1Lj7mR0PY-1770378035182-0.0.1.1-604800000; path=/; domain=.postman-echo.com; HttpOnly; Secure; SameSite=None"
#>
#> $headers$`x-envoy-upstream-service-time`
#> [1] "5"
#>
#> $headers$`cf-cache-status`
#> [1] "DYNAMIC"
#>
#> $headers$Server
#> [1] "cloudflare"
#>
#>
#> $data
#> [1] "{\"args\":{},\"headers\":{\"host\":\"postman-echo.com\",\"accept-encoding\":\"gzip, br\",\"x-forwarded-proto\":\"https\"},\"url\":\"https://postman-echo.com/get\"}"ncurl_aio: Async Requests
ncurl_aio() performs asynchronous requests, returning
immediately with an ‘ncurlAio’ object that resolves when the response
arrives.
res <- ncurl_aio("https://postman-echo.com/post",
method = "POST",
headers = c(`Content-Type` = "application/json"),
data = '{"async": true}',
response = "date")
res
#> < ncurlAio | $status $headers $data >
call_aio(res)$headers
#> $date
#> [1] "Fri, 06 Feb 2026 11:40:35 GMT"
res$status
#> [1] 200
res$data
#> [1] "{\"args\":{},\"data\":{\"async\":true},\"files\":{},\"form\":{},\"headers\":{\"host\":\"postman-echo.com\",\"content-type\":\"application/json\",\"accept-encoding\":\"gzip, br\",\"x-forwarded-proto\":\"https\",\"content-length\":\"15\"},\"json\":{\"async\":true},\"url\":\"https://postman-echo.com/post\"}"ncurl_session: Persistent Connections
ncurl_session() creates a reusable connection for
efficient repeated requests to an API endpoint. Use
transact() to send requests over the session.
sess <- ncurl_session("https://postman-echo.com/get",
convert = FALSE,
headers = c(`Content-Type` = "application/json"),
response = c("Date", "Content-Type"))
sess
#> < ncurlSession > - transact() to return data
transact(sess)
#> $status
#> [1] 200
#>
#> $headers
#> $headers$Date
#> [1] "Fri, 06 Feb 2026 11:40:35 GMT"
#>
#> $headers$`Content-Type`
#> [1] "application/json; charset=utf-8"
#>
#>
#> $data
#> [1] 7b 22 61 72 67 73 22 3a 7b 7d 2c 22 68 65 61 64 65 72 73 22 3a 7b 22 68 6f 73 74 22 3a 22 70 6f
#> [33] 73 74 6d 61 6e 2d 65 63 68 6f 2e 63 6f 6d 22 2c 22 61 63 63 65 70 74 2d 65 6e 63 6f 64 69 6e 67
#> [65] 22 3a 22 67 7a 69 70 2c 20 62 72 22 2c 22 78 2d 66 6f 72 77 61 72 64 65 64 2d 70 72 6f 74 6f 22
#> [97] 3a 22 68 74 74 70 73 22 2c 22 63 6f 6e 74 65 6e 74 2d 74 79 70 65 22 3a 22 61 70 70 6c 69 63 61
#> [129] 74 69 6f 6e 2f 6a 73 6f 6e 22 7d 2c 22 75 72 6c 22 3a 22 68 74 74 70 73 3a 2f 2f 70 6f 73 74 6d
#> [161] 61 6e 2d 65 63 68 6f 2e 63 6f 6d 2f 67 65 74 22 7d
close(sess)2. WebSocket Client
stream() provides a low-level byte stream interface for
communicating with WebSocket servers and other non-NNG endpoints.
Use textframes = TRUE for servers that expect text
frames (most WebSocket servers).
s <- stream(dial = "wss://echo.websocket.org/", textframes = TRUE)
s
#> < nanoStream >
#> - mode: dialer text frames
#> - state: opened
#> - url: wss://echo.websocket.org/send() and recv(), along with their async
counterparts send_aio() and recv_aio(), work
on Streams just like Sockets.
3. HTTP Server
http_server() creates servers that handle HTTP requests
and WebSocket connections using NNG’s high-performance architecture.
server <- http_server(
url = "http://127.0.0.1:8080",
handlers = list(
handler("/", function(req) {
list(status = 200L, body = "Hello from nanonext!")
}),
handler("/api/data", function(req) {
list(
status = 200L,
headers = c("Content-Type" = "application/json"),
body = '{"value": 42}'
)
}, method = "GET")
)
)
server$start()
# Process requests: repeat later::run_now(Inf)
server$close()Request Handlers
handler() creates HTTP route handlers. The callback
receives a request list with method, uri,
headers, and body, and returns a response list
with status, optional headers, and
body.
# GET endpoint
h1 <- handler("/hello", function(req) {
list(status = 200L, body = "Hello!")
})
# POST endpoint echoing the request body
h2 <- handler("/echo", function(req) {
list(status = 200L, body = req$body)
}, method = "POST")
# Catch-all for any method under a path prefix
h3 <- handler("/api", function(req) {
list(
status = 200L,
headers = c("Content-Type" = "application/json"),
body = sprintf('{"method":"%s","uri":"%s"}', req$method, req$uri)
)
}, method = "*", prefix = TRUE)Static Content Handlers
nanonext provides specialized handlers for serving static content.
# Serve a single file
h_file <- handler_file("/favicon.ico", "path/to/favicon.ico")
# Serve a directory tree (automatic MIME type detection)
h_dir <- handler_directory("/static", "www/assets")
# Serve inline content
h_inline <- handler_inline("/robots.txt", "User-agent: *\nDisallow:",
content_type = "text/plain")
# Redirect requests
h_redirect <- handler_redirect("/old-page", "/new-page", status = 301L)4. WebSocket Server
handler_ws() creates WebSocket handlers that manage
bidirectional connections. NNG handles the HTTP upgrade handshake and
WebSocket framing automatically.
clients <- list()
server <- http_server(
url = "http://127.0.0.1:8080",
handlers = list(
handler_ws(
"/chat",
on_message = function(ws, data) {
# Broadcast to all connected clients
for (client in clients) client$send(data)
},
on_open = function(ws) {
clients[[as.character(ws$id)]] <<- ws
},
on_close = function(ws) {
clients[[as.character(ws$id)]] <<- NULL
},
textframes = TRUE
)
)
)
server$start()The ws connection object provides:
-
ws$send(data)- Send a message to the client -
ws$close()- Close the connection -
ws$id- Unique connection identifier
5. HTTP Streaming
handler_stream() enables HTTP streaming using chunked
transfer encoding. This supports Server-Sent Events (SSE),
newline-delimited JSON (NDJSON), and custom streaming formats.
conns <- list()
server <- http_server(
url = "http://127.0.0.1:8080",
handlers = list(
# SSE endpoint
handler_stream("/events",
on_request = function(conn, req) {
conn$set_header("Content-Type", "text/event-stream")
conn$set_header("Cache-Control", "no-cache")
conns[[as.character(conn$id)]] <<- conn
conn$send(format_sse(data = "connected", id = "1"))
},
on_close = function(conn) {
conns[[as.character(conn$id)]] <<- NULL
}
),
# Trigger broadcast via POST
handler("/broadcast", function(req) {
msg <- format_sse(data = rawToChar(req$body), event = "message")
lapply(conns, function(c) c$send(msg))
list(status = 200L, body = "sent")
}, method = "POST")
)
)
server$start()Server-Sent Events
format_sse() formats messages according to the SSE
specification for browser EventSource clients.
format_sse(data = "Hello")
#> [1] "data: Hello\n\n"
format_sse(data = "Update available", event = "notification", id = "42")
#> [1] "event: notification\nid: 42\ndata: Update available\n\n"
format_sse(data = "Line 1\nLine 2")
#> [1] "data: Line 1\ndata: Line 2\n\n"The streaming connection object provides:
-
conn$send(data)- Send a data chunk -
conn$close()- Close the connection -
conn$set_status(code)- Set HTTP status (before first send) -
conn$set_header(name, value)- Set response header (before first send) -
conn$id- Unique connection identifier
6. Secure Connections (TLS)
All web utilities support TLS for secure HTTPS/WSS connections via
tls_config().
# Generate self-signed certificate for testing
cert <- write_cert(cn = "127.0.0.1")
# Server TLS configuration
ser <- tls_config(server = cert$server)
# Client TLS configuration
cli <- tls_config(client = cert$client)Use the configurations with servers and clients:
# HTTPS server
server <- http_server(
url = "https://127.0.0.1:0",
handlers = list(
handler("/", function(req) list(status = 200L, body = "Secure!"))
),
tls = ser
)
server$start()
server
#> < nanoServer >
#> - url: https://127.0.0.1:64230
#> - state: started
# HTTPS client request
aio <- ncurl_aio(paste0(server$url, "/"), tls = cli)
while (unresolved(aio)) later::run_now(1)
#> {"args":{},"headers":{"host":"postman-echo.com","accept-encoding":"gzip, br","x-forwarded-proto":"https"},"url":"https://postman-echo.com/get"}
aio$status
#> [1] 200
aio$data
#> [1] "Secure!"
server$close()7. Complete Example: Shiny ExtendedTask
This example demonstrates using ncurl_aio() with Shiny’s
ExtendedTask for non-blocking HTTP requests.
library(shiny)
library(bslib)
library(nanonext)
ui <- page_fluid(
p("The time is ", textOutput("current_time", inline = TRUE)),
hr(),
input_task_button("btn", "Fetch data"),
verbatimTextOutput("result")
)
server <- function(input, output, session) {
output$current_time <- renderText({
invalidateLater(1000)
format(Sys.time(), "%H:%M:%S %p")
})
task <- ExtendedTask$new(
function() ncurl_aio("https://postman-echo.com/get", response = TRUE)
) |> bind_task_button("btn")
observeEvent(input$btn, task$invoke())
output$result <- renderPrint(task$result()$headers)
}
shinyApp(ui, server)8. Complete Example: Chat Application
This example demonstrates combining HTTP and WebSocket handlers for a simple chat application.
clients <- list()
server <- http_server(
url = "http://127.0.0.1:8080",
handlers = list(
# Serve the chat page
handler("/", function(req) {
list(
status = 200L,
headers = c("Content-Type" = "text/html"),
body = '<!DOCTYPE html>
<html>
<body>
<div id="messages"></div>
<input id="msg" type="text">
<button onclick="send()">Send</button>
<script>
const ws = new WebSocket("ws://127.0.0.1:8080/ws");
ws.onmessage = (e) => {
document.getElementById("messages").innerHTML += "<p>" + e.data + "</p>";
};
function send() {
ws.send(document.getElementById("msg").value);
document.getElementById("msg").value = "";
}
</script>
</body>
</html>'
)
}),
# WebSocket endpoint for real-time messaging
handler_ws("/ws",
on_message = function(ws, data) {
# Broadcast to all clients
for (client in clients) {
client$send(data)
}
},
on_open = function(ws) {
clients[[as.character(ws$id)]] <<- ws
},
on_close = function(ws) {
clients[[as.character(ws$id)]] <<- NULL
},
textframes = TRUE
)
)
)
server$start()
# The server runs non-blocking in the background via the 'later' event loop.
# server$close()