Skip to content

arkenidar/c_lua_http_client

Repository files navigation

LuaJIT HTTP Client with libcurl

A lightweight, efficient HTTP client for LuaJIT using FFI bindings to libcurl. This library provides a simple and elegant API for making HTTP requests without external dependencies (except libcurl itself).

Features

  • Pure LuaJIT implementation using FFI (no C compilation needed)
  • Support for all common HTTP methods (GET, POST, PUT, DELETE, PATCH, HEAD)
  • Custom headers support
  • Request/response body handling
  • Timeout configuration
  • SSL/TLS support
  • Redirect following
  • JSON support (with optional lua-cjson)
  • Streaming response handling
  • Thread-safe and high-performance

Requirements

  • LuaJIT 2.0+
  • libcurl (shared library)
    • Windows: libcurl.dll or libcurl-4.dll
    • Linux: libcurl.so
    • macOS: libcurl.dylib
  • Optional: lua-cjson for JSON encoding/decoding

Installing libcurl

Windows (MSYS2/MinGW):

pacman -S mingw-w64-x86_64-curl

Linux (Debian/Ubuntu):

sudo apt-get install libcurl4-openssl-dev

Linux (Fedora/RHEL):

sudo dnf install libcurl-devel

macOS:

brew install curl

Installation

Simply copy the files to your project or add them to your LUA_PATH:

# Copy to your project
cp curl_ffi.lua http_client.lua /path/to/your/project/

# Or set LUA_PATH
export LUA_PATH="$LUA_PATH;/path/to/c_lua_http_client/?.lua"

Quick Start

local http = require("http_client")

-- Simple GET request
local response, err = http.get("https://api.example.com/users")

if response then
    print("Status: " .. response.status)
    print("Body: " .. response.body)
    print("Content-Type: " .. response.content_type)
else
    print("Error: " .. err)
end

API Reference

Module Functions

http.get(url, options)

Perform a GET request.

local response, err = http.get("https://example.com", {
    headers = {
        ["Authorization"] = "Bearer token123"
    },
    timeout = 10
})

http.post(url, options)

Perform a POST request.

local response, err = http.post("https://example.com/api", {
    body = "key=value",
    headers = {
        ["Content-Type"] = "application/x-www-form-urlencoded"
    }
})

http.put(url, options)

Perform a PUT request.

http.delete(url, options)

Perform a DELETE request.

http.patch(url, options)

Perform a PATCH request.

http.head(url, options)

Perform a HEAD request.

http.json(method, url, data, options)

Perform a request with JSON encoding/decoding (requires lua-cjson).

local response, err = http.json("POST", "https://api.example.com/users", {
    name = "John Doe",
    email = "john@example.com"
})

if response and response.json then
    print("User ID: " .. response.json.id)
end

Client Instance

Create a custom client instance with default options:

local client = http.new({
    timeout = 30,
    connect_timeout = 10,
    follow_redirects = true,
    max_redirects = 10,
    user_agent = "MyApp/1.0",
    verify_ssl = true,
    verbose = false
})

local response, err = client:get("https://example.com")

Request Options

All request methods accept an options table:

{
    -- Request body (string)
    body = "request data",

    -- Custom headers (table)
    headers = {
        ["Content-Type"] = "application/json",
        ["Authorization"] = "Bearer token"
    },

    -- Timeout in seconds
    timeout = 30,

    -- Connection timeout in seconds
    connect_timeout = 10,

    -- Follow redirects
    follow_redirects = true,

    -- Maximum number of redirects
    max_redirects = 10,

    -- Custom User-Agent
    user_agent = "MyApp/1.0",

    -- Verify SSL certificates
    verify_ssl = true,

    -- Verbose output (for debugging)
    verbose = false
}

Response Object

Successful requests return a response table:

{
    status = 200,                    -- HTTP status code
    headers = {                       -- Response headers (lowercase keys)
        ["content-type"] = "application/json",
        ["content-length"] = "1234"
    },
    body = "response body",          -- Response body as string
    content_type = "application/json", -- Content-Type header value
    url = "https://final.url",       -- Final URL (after redirects)
    json = { ... }                   -- Parsed JSON (only with http.json())
}

Failed requests return nil, error_message, partial_response.

Examples

Basic GET Request

local http = require("http_client")

local response, err = http.get("https://httpbin.org/get")
if response then
    print("Status:", response.status)
    print("Body:", response.body)
end

POST with Form Data

local response, err = http.post("https://httpbin.org/post", {
    body = "name=John&email=john@example.com",
    headers = {
        ["Content-Type"] = "application/x-www-form-urlencoded"
    }
})

POST with JSON

local response, err = http.json("POST", "https://httpbin.org/post", {
    name = "Alice",
    age = 30
})

if response and response.json then
    -- Access parsed JSON response
    print(response.json.data)
end

Custom Headers

local response, err = http.get("https://api.example.com/data", {
    headers = {
        ["Authorization"] = "Bearer your-token",
        ["Accept"] = "application/json",
        ["X-Custom-Header"] = "value"
    }
})

Timeout Handling

local response, err = http.get("https://slow-server.com", {
    timeout = 5,  -- 5 second timeout
    connect_timeout = 2
})

if not response then
    print("Request failed:", err)
end

Custom Client Instance

local client = http.new({
    timeout = 60,
    user_agent = "MyBot/2.0",
    verify_ssl = false  -- Disable SSL verification (not recommended)
})

local response = client:get("https://self-signed-cert.com")

Following Redirects

local response, err = http.get("https://httpbin.org/redirect/5", {
    follow_redirects = true,
    max_redirects = 10
})

if response then
    print("Final URL:", response.url)
    print("Status:", response.status)
end

Error Handling

Always check for errors:

local response, err, partial = http.get("https://invalid-url")

if not response then
    print("Error:", err)
    if partial then
        print("Partial response available")
        print("Status:", partial.status)
    end
else
    print("Success:", response.status)
end

Low-Level API

For advanced usage, you can use the low-level FFI bindings directly:

local curl_ffi = require("curl_ffi")

local handle = curl_ffi.init()
curl_ffi.setopt(handle, ffi.C.CURLOPT_URL, "https://example.com")
-- ... more options
local result = curl_ffi.perform(handle)
curl_ffi.cleanup(handle)

File Structure

Performance

This library uses LuaJIT's FFI which provides near-native performance for libcurl operations. The overhead is minimal compared to pure C implementations.

Thread Safety

Each request creates its own CURL handle, making it safe to use from multiple coroutines. However, the global libcurl initialization is done once at module load time.

Limitations

  • Binary file uploads require manual setup (use low-level API)
  • No built-in progress callbacks (can be added using low-level API)
  • Multipart form data requires manual construction

License

This project is provided as-is for educational and commercial use.

Contributing

Contributions are welcome. Please ensure code follows the existing style and includes examples.

Troubleshooting

"Could not load libcurl" error

Make sure libcurl is installed and in your system's library path:

Windows: Add the directory containing libcurl.dll to your PATH Linux/macOS: Use ldconfig or set LD_LIBRARY_PATH

SSL Certificate Errors

If you encounter SSL certificate verification errors:

-- Disable verification (not recommended for production)
local response = http.get(url, { verify_ssl = false })

Timeout Issues

Adjust timeout values based on your network conditions:

local response = http.get(url, {
    timeout = 60,           -- Total request timeout
    connect_timeout = 10    -- Connection timeout
})

Version

libcurl version can be checked with:

local http = require("http_client")
print(http.version())