goServeR

This package provides an interface to a simple HTTP file server written in go.

The server supports range requests and unbounded CORS. It uses the cgo package to call Go functions from R using the R C extension mechanisms. This is an experimentation with the R C extension mechanism without the very convenient Rcpp as well as filling a need.

The server was very insecure but useful for my use case of serving local BCF/BAM files to an ambiorix app using igv.js (because {httpuv} does not support range requests as of now). The package can be installed from r-universe and requires a go installation. We’ve added TLS support, basic authentication and asynchronous processing of logging in R since the initial version.

INSTALL


## install golang via apt/yum/brew or binary and put it in the path
## to build from source

go || sudo apt-get install --yes golang
## clone the repo and install
git clone https://github.com/sounkou-bioinfo/goServeR.git
cd goServeR/
R CMD INSTALL  .
# or github via remotes
Rscript -e 'remotes::install_github("sounkou-bioinfo/goServeR")'
# or via r-universe 
Rscript -e "install.packages('goserveR', repos = c('https://sounkou-bioinfo.r-universe.dev'))"
# CRAN to be coming

Usage Example

From the command line, you can start a server in the background and test it with curl

# Start the server in the background
Rscript -e "goserveR::runServer(addr = '0.0.0.0:8080', blocking = FALSE); Sys.sleep(10000)" &
pid=$!

sleep 2

curl -L http://0.0.0.0:8080/${PWD} 2> /dev/null \
 | head -5

sleep 2

kill -9 $pid
#> <pointer: 0x646362ac1d60>
#> [goserveR] 2026/04/13 21:48:24.063293 Registered handler for directory "/root/goServeR" at prefix "/root/goServeR"
#> [goserveR] 2026/04/13 21:48:24.063421 Serving 1 directories on http://0.0.0.0:8080
#> [goserveR] 2026/04/13 21:48:25.941343 GET /root/goServeR/ 127.0.0.1:36034 107.87µs
#> <pre>
#> <a href=".Rbuildignore">.Rbuildignore</a>
#> <a href=".Rinstignore">.Rinstignore</a>
#> <a href=".git/">.git/</a>

R starts a blocking server (blocks R session) with

library(goserveR)
# set timeout to 5 seconds for demo purpose
# this will stop the server after 5 seconds
setTimeLimit(elapsed = 5, transient = TRUE)
runServer(dir = ".", addr = "0.0.0.0:8080", silent = TRUE)
#> Server started in blocking mode. Press Ctrl+C to interrupt.
#> Server address: 0.0.0.0:8080
#> Static files directories: 1 paths
#>   1: /root/goServeR ->
setTimeLimit()

To start a background server and get a handle

h <- runServer(dir = ".", addr = "0.0.0.0:8080", blocking = FALSE, silent = TRUE)
listServers() |> str()
#> List of 1
#>  $ :List of 10
#>   ..$ directory      : chr "/root/goServeR"
#>   ..$ address        : chr "0.0.0.0:8080"
#>   ..$ prefix         : chr ""
#>   ..$ protocol       : chr "HTTP"
#>   ..$ logging        : chr "silent"
#>   ..$ log_handler    : chr "none"
#>   ..$ log_destination: chr "none"
#>   ..$ log_function   : chr "none"
#>   ..$ authentication : chr "disabled"
#>   ..$ auth_keys      : chr "none"
#>   ..- attr(*, "class")= chr "server_info"
#>  - attr(*, "class")= chr "server_list"
currentDir <- normalizePath(".")
readLines(paste0("http://0.0.0.0:8080/", currentDir)) |>
  head(10)
#>  [1] "<pre>"                                                          
#>  [2] "<a href=\".Rbuildignore\">.Rbuildignore</a>"                    
#>  [3] "<a href=\".Rinstignore\">.Rinstignore</a>"                      
#>  [5] "<a href=\".git/\">.git/</a>"                                    
#>  [6] "<a href=\".github/\">.github/</a>"                              
#>  [7] "<a href=\".gitignore\">.gitignore</a>"                          
#>  [8] "<a href=\".lintr\">.lintr</a>"                                  
#>  [9] "<a href=\".pre-commit-config.yaml\">.pre-commit-config.yaml</a>"
#> [10] "<a href=\".vscode/\">.vscode/</a>"
shutdownServer(h)

Authentication and TLS/HTTPS Support

The package supports both API key authentication and TLS/HTTPS connections. You can use them separately or together for secure authenticated file serving. Background servers provide runtime key CRUD through addAuthKey(), listAuthKeys(), updateAuthKey(), removeAuthKey(), and clearAuthKeys().


# Generate an in-memory PEM certificate/key pair and temporary files.
tls_credentials <- createTLSCertificate(cn = "127.0.0.1")
# write test file
writeLines("Hello from goServeR!", "test.txt")

# HTTP server with authentication
h_http_auth <- runServer(dir = ".", addr = "127.0.0.1:8090", prefix = "/", blocking = FALSE, 
                         auth_keys = c("secret123", "token456"), silent = TRUE)

length(listServers())
#> [1] 1
# Test authentication 
temp_file <- tempfile()
download.file("http://127.0.0.1:8090/test.txt", 
              destfile = temp_file,
              headers = c("X-API-Key" = "secret123"),
              quiet = TRUE)


readLines(temp_file)
#> [1] "Hello from goServeR!"

# Add, inspect, and rotate a key without restarting the server.
addAuthKey(h_http_auth, "newkey789")
listAuthKeys(h_http_auth)
#> [1] "secret123"  "token456"  "newkey789"
updateAuthKey(h_http_auth, "newkey789", "rotated_key_789")

# Test with the replacement key.
download.file("http://127.0.0.1:8090/test.txt", 
              destfile = temp_file,
              headers = c("X-API-Key" = "rotated_key_789"),
              quiet = TRUE)
readLines(temp_file)
#> [1] "Hello from goServeR!"
removeAuthKey(h_http_auth, "rotated_key_789")

unlink(temp_file)
# Test wrong key should fail
temp_file_fail <- tempfile()
tryCatch({
  download.file("http://127.0.0.1:8090/test.txt", 
                destfile = temp_file_fail,
                headers = c("X-API-Key" = "wrong_key"),
                quiet = TRUE)
}, error = function(e) {
  message("Expected error occurred: ", e$message)
})
#> Warning in download.file("http://127.0.0.1:8090/test.txt", destfile =
#> temp_file_fail, : downloaded length 0 != reported length 13
#> Warning in download.file("http://127.0.0.1:8090/test.txt", destfile =
#> temp_file_fail, : cannot open URL 'http://127.0.0.1:8090/test.txt': HTTP status
#> was '401 Unauthorized'
#> Expected error occurred: cannot open URL 'http://127.0.0.1:8090/test.txt'

# HTTPS server with authentication
h_https_auth <- runServer(
  dir = ".", 
  addr = "127.0.0.1:8444", 
  tls = tls_credentials,
  prefix = "/",
  auth_keys = c("secure_key_123"),
  blocking = FALSE,
  silent = TRUE
)

# Give HTTPS server time to start up and verify it's running
length(listServers())
#> [1] 2
# Test HTTPS with authentication (ssl_verifypeer=0 for self-signed certs)
temp_file_https <- tempfile()

h <- curl::new_handle(ssl_verifypeer = 0L, ssl_verifyhost = 0L)
curl::handle_setheaders(h, "X-API-Key" = "secure_key_123")
curl::curl_download("https://127.0.0.1:8444/test.txt", 
                     destfile = temp_file_https,
                     handle = h)
  
readLines(temp_file_https) 
#> [1] "Hello from goServeR!"

listServers() |> str()
#> List of 2
#>  $ :List of 10
#>   ..$ directory      : chr "/root/goServeR"
#>   ..$ address        : chr "127.0.0.1:8090"
#>   ..$ prefix         : chr "/"
#>   ..$ protocol       : chr "HTTP"
#>   ..$ logging        : chr "silent"
#>   ..$ log_handler    : chr "none"
#>   ..$ log_destination: chr "none"
#>   ..$ log_function   : chr "none"
#>   ..$ authentication : chr "enabled"
#>   ..$ auth_keys      : chr "enabled"
#>   ..- attr(*, "class")= chr "server_info"
#>  $ :List of 10
#>   ..$ directory      : chr "/root/goServeR"
#>   ..$ address        : chr "127.0.0.1:8444"
#>   ..$ prefix         : chr "/"
#>   ..$ protocol       : chr "HTTPS"
#>   ..$ logging        : chr "silent"
#>   ..$ log_handler    : chr "none"
#>   ..$ log_destination: chr "none"
#>   ..$ log_function   : chr "none"
#>   ..$ authentication : chr "enabled"
#>   ..$ auth_keys      : chr "enabled"
#>   ..- attr(*, "class")= chr "server_info"
#>  - attr(*, "class")= chr "server_list"

# Cleanup
shutdownServer(h_http_auth)
shutdownServer(h_https_auth)
removeTLSCertificate(tls_credentials)
unlink("test.txt")

Multiple Servers

You can run multiple servers simultaneously on different ports:

# Start multiple servers
h1 <- runServer(dir = ".", addr = "127.0.0.1:8081", blocking = FALSE, silent = TRUE)
h2 <- runServer(dir = ".", addr = "127.0.0.1:8082", blocking = FALSE, silent = TRUE)
h3 <- runServer(dir = ".", addr = "127.0.0.1:8083", blocking = FALSE, silent = TRUE)

# List all running servers
listServers() |> str()
#> List of 3
#>  $ :List of 10
#>   ..$ directory      : chr "/root/goServeR"
#>   ..$ address        : chr "127.0.0.1:8081"
#>   ..$ prefix         : chr ""
#>   ..$ protocol       : chr "HTTP"
#>   ..$ logging        : chr "silent"
#>   ..$ log_handler    : chr "none"
#>   ..$ log_destination: chr "none"
#>   ..$ log_function   : chr "none"
#>   ..$ authentication : chr "disabled"
#>   ..$ auth_keys      : chr "none"
#>   ..- attr(*, "class")= chr "server_info"
#>  $ :List of 10
#>   ..$ directory      : chr "/root/goServeR"
#>   ..$ address        : chr "127.0.0.1:8082"
#>   ..$ prefix         : chr ""
#>   ..$ protocol       : chr "HTTP"
#>   ..$ logging        : chr "silent"
#>   ..$ log_handler    : chr "none"
#>   ..$ log_destination: chr "none"
#>   ..$ log_function   : chr "none"
#>   ..$ authentication : chr "disabled"
#>   ..$ auth_keys      : chr "none"
#>   ..- attr(*, "class")= chr "server_info"
#>  $ :List of 10
#>   ..$ directory      : chr "/root/goServeR"
#>   ..$ address        : chr "127.0.0.1:8083"
#>   ..$ prefix         : chr ""
#>   ..$ protocol       : chr "HTTP"
#>   ..$ logging        : chr "silent"
#>   ..$ log_handler    : chr "none"
#>   ..$ log_destination: chr "none"
#>   ..$ log_function   : chr "none"
#>   ..$ authentication : chr "disabled"
#>   ..$ auth_keys      : chr "none"
#>   ..- attr(*, "class")= chr "server_info"
#>  - attr(*, "class")= chr "server_list"

# Access different servers

#Server 1 (port 8081) 
length(readLines(paste0("http://127.0.0.1:8081/", normalizePath("."))))
#> [1] 26
#Server 2 (port 8082)
length(readLines(paste0("http://127.0.0.1:8082/", normalizePath("."))))
#> [1] 26
#Server 3 (port 8083)
length(readLines(paste0("http://127.0.0.1:8083/", normalizePath("."))))
#> [1] 26

# Shutdown all servers
shutdownServer(h1)
shutdownServer(h2)
shutdownServer(h3)

# Verify cleanup
length(listServers())
#> [1] 0

Multiple Directories from Single Server

You can serve multiple directories from the same server instance by providing vectors of directories and prefixes:

# Create test directories
dir.create("test_data", showWarnings = FALSE)
dir.create("test_docs", showWarnings = FALSE)
writeLines("Sample data content", "test_data/sample.txt")
writeLines("Documentation content", "test_docs/doc.txt")

# Start server with multiple directories
h_multi <- runServer(
    dir = c("test_data", "test_docs", "."),
    prefix = c("/api/data", "/docs", "/files"),
    addr = "127.0.0.1:8090",
    blocking = FALSE,
    silent = TRUE
)

# List server to see multiple directories
listServers() |> str()
#> List of 1
#>  $ :List of 10
#>   ..$ directory      : chr "/root/goServeR/test_data, /root/goServeR/test_docs, /root/goServeR"
#>   ..$ address        : chr "127.0.0.1:8090"
#>   ..$ prefix         : chr "/api/data, /docs, /files"
#>   ..$ protocol       : chr "HTTP"
#>   ..$ logging        : chr "silent"
#>   ..$ log_handler    : chr "none"
#>   ..$ log_destination: chr "none"
#>   ..$ log_function   : chr "none"
#>   ..$ authentication : chr "disabled"
#>   ..$ auth_keys      : chr "none"
#>   ..- attr(*, "class")= chr "server_info"
#>  - attr(*, "class")= chr "server_list"

# Access different endpoints
# Data endpoint
readLines("http://127.0.0.1:8090/api/data/sample.txt")
#> [1] "Sample data content"

# Docs endpoint  
readLines("http://127.0.0.1:8090/docs/doc.txt")
#> [1] "Documentation content"

# Files endpoint (current directory)
length(readLines(paste0("http://127.0.0.1:8090/files/")))
#> [1] 28
# Cleanup
shutdownServer(h_multi)
unlink(c("test_data", "test_docs"), recursive = TRUE)

Background Log Handling

The package implements asynchronous log handling using R’s async handling capabilities that was adapted from Simon Urbanek’s async callback pattern. Each server can have a custom log handler:

# Default console logging
h1 <- runServer(dir = ".", addr = "127.0.0.1:8350", blocking = FALSE, silent = FALSE)

# Custom file logger
logfile <- tempfile("custom_", fileext = ".log")
file_logger <- function(handler, message, user) {
  cat(format(Sys.time(), "[%Y-%m-%d %H:%M:%S]"), message, "\n", file = logfile, append = TRUE)
}
h2 <- runServer(dir = ".", addr = "127.0.0.1:8351", blocking = FALSE, 
                silent = FALSE, log_handler = file_logger)
# read some lines to generate logs
bunk <- readLines(paste0("http://127.0.0.1:8351/", normalizePath(".")))
# Custom console logger with prefix
console_logger <- function(handler, message, user) {
  cat("\n*** [CUSTOM-SERVER] ***", message, "*** END ***\n")
  flush.console()
}
h3 <- runServer(dir = ".", addr = "127.0.0.1:8352", blocking = FALSE, 
                silent = FALSE, log_handler = console_logger)
bunk <- readLines(paste0("http://127.0.0.1:8352/", normalizePath(".")))
# Silent mode (no logs)
h4 <- runServer(dir = ".", addr = "127.0.0.1:8353", blocking = FALSE, silent = TRUE)

listServers() |> str()
#> List of 4
#>  $ :List of 10
#>   ..$ directory      : chr "/root/goServeR"
#>   ..$ address        : chr "127.0.0.1:8350"
#>   ..$ prefix         : chr ""
#>   ..$ protocol       : chr "HTTP"
#>   ..$ logging        : chr "logging"
#>   ..$ log_handler    : chr "default"
#>   ..$ log_destination: chr "console"
#>   ..$ log_function   : chr ".default_log_callback"
#>   ..$ authentication : chr "disabled"
#>   ..$ auth_keys      : chr "none"
#>   ..- attr(*, "class")= chr "server_info"
#>  $ :List of 10
#>   ..$ directory      : chr "/root/goServeR"
#>   ..$ address        : chr "127.0.0.1:8351"
#>   ..$ prefix         : chr ""
#>   ..$ protocol       : chr "HTTP"
#>   ..$ logging        : chr "logging"
#>   ..$ log_handler    : chr "custom_function"
#>   ..$ log_destination: chr "custom"
#>   ..$ log_function   : chr "<custom function>"
#>   ..$ authentication : chr "disabled"
#>   ..$ auth_keys      : chr "none"
#>   ..- attr(*, "class")= chr "server_info"
#>  $ :List of 10
#>   ..$ directory      : chr "/root/goServeR"
#>   ..$ address        : chr "127.0.0.1:8352"
#>   ..$ prefix         : chr ""
#>   ..$ protocol       : chr "HTTP"
#>   ..$ logging        : chr "logging"
#>   ..$ log_handler    : chr "custom_function"
#>   ..$ log_destination: chr "custom"
#>   ..$ log_function   : chr "<custom function>"
#>   ..$ authentication : chr "disabled"
#>   ..$ auth_keys      : chr "none"
#>   ..- attr(*, "class")= chr "server_info"
#>  $ :List of 10
#>   ..$ directory      : chr "/root/goServeR"
#>   ..$ address        : chr "127.0.0.1:8353"
#>   ..$ prefix         : chr ""
#>   ..$ protocol       : chr "HTTP"
#>   ..$ logging        : chr "silent"
#>   ..$ log_handler    : chr "none"
#>   ..$ log_destination: chr "none"
#>   ..$ log_function   : chr "none"
#>   ..$ authentication : chr "disabled"
#>   ..$ auth_keys      : chr "none"
#>   ..- attr(*, "class")= chr "server_info"
#>  - attr(*, "class")= chr "server_list"

# let's get the log by making R idle !
Sys.sleep(5)
#> [goserveR] 2026/04/13 21:48:34.379825 Registered handler for directory "/root/goServeR" at prefix "/root/goServeR"
#> 2026/04/13 21:48:34.379938 Serving 1 directories on http://127.0.0.1:8350
#> 
#> *** [CUSTOM-SERVER] *** 2026/04/13 21:48:34.382061 Registered handler for directory "/root/goServeR" at prefix "/root/goServeR"
#> 2026/04/13 21:48:34.382078 Serving 1 directories on http://127.0.0.1:8352
#> 2026/04/13 21:48:34.382453 GET /root/goServeR/ 127.0.0.1:37748 48.165µs
#>  *** END ***
shutdownServer(h1)
shutdownServer(h2)
shutdownServer(h3)
shutdownServer(h4)


# Check custom log file
if (file.exists(logfile)) {
  cat(readLines(logfile, n = 3), sep = "\n")
}
#> [2026-04-13 21:48:34] 2026/04/13 21:48:34.380738 Registered handler for directory "/root/goServeR" at prefix "/root/goServeR"
#> 2026/04/13 21:48:34.380776 Serving 1 directories on http://127.0.0.1:8351
#> 2026/04/13 21:48:34.381263 GET /root/goServeR/ 127.0.0.1:37002 63.9µs

On background log handlers

An important note is that the handler may run at unpredictable times, and are removed when the server is shutdown, so there is no guarantee that they may run when go write to the log pipe.

How it works ?

We wrote a standard Go HTTP file server, created a static library from it, and then wrote the usual R C API wrappers for the cgo (static) library. Interrupts are now handled entirely at the C level: the Go server runs in a background thread, and the main C thread periodically checks for user interrupts using the R API. If an interrupt is detected, the C code signals the Go server to shut down. This approach is robust, portable, and keeps all R session control in C, not Go. Morover logging is now handled asynchronously using asynchronous input handlers as adapted from Simon Urbanek’s async callback pattern.

On TLS Certificates

createTLSCertificate() uses nanonext::write_cert() to generate a self-signed X.509 certificate and private key directly as PEM. It writes temporary files for Go’s HTTPS server, so no external certificate-generation tool or bundled private key is needed.

credentials <- createTLSCertificate(cn = "127.0.0.1")
server <- runServer(
  dir = ".",
  addr = "127.0.0.1:8443",
  tls = credentials,
  blocking = FALSE
)
shutdownServer(server)
removeTLSCertificate(credentials)

Self-signed certificates are appropriate for tests and controlled clients but are not automatically browser-trusted. For public production HTTPS, supply certfile and keyfile issued by a trusted Certificate Authority such as Let’s Encrypt.

REFERENCES

LLM Usage Disclosure

Code and documentation in this project have been generated with the assistance of the github Copilot LLM tools. While we have reviewed and edited the generated content, we acknowledge that LLM tools were used in the creation process and accordingly (since these models are trained on GPL code and other commons + proprietary software license is fake anyway) the code is released under GPL-3. So if you use this code in any way, you must comply with the GPL-3 license.