- Odin 92.8%
- Shell 6.5%
- Dockerfile 0.4%
- Go 0.2%
|
Some checks failed
Deploy docs to GitHub pages / docs (push) Waiting to run
CI / check (ubuntu-latest) (push) Failing after 4s
CI / unit (ubuntu-latest) (push) Failing after 3s
CI / fuzz (push) Failing after 4s
CI / h2spec (push) Failing after 4s
CI / interop (push) Failing after 4s
CI / adversity (push) Failing after 3s
CI / soak (push) Has been skipped
CI / check (macos-15-intel) (push) Has been cancelled
CI / check (macos-latest) (push) Has been cancelled
CI / check (windows-latest) (push) Has been cancelled
CI / unit (macos-latest) (push) Has been cancelled
Listen_IO.Nbio only selects the nbio poll_once backend; accept/drive use Tcp_Conn on Runtime like Demux. Multi-protocol with Nbio readiness is one Runtime (no dual-engine Nbio_H1). listen_and_serve_nbio is a thin alias. |
||
|---|---|---|
| .github/workflows | ||
| boringssl | ||
| client | ||
| comparisons | ||
| docs | ||
| examples | ||
| hpack | ||
| http2 | ||
| http3 | ||
| huffman | ||
| openssl | ||
| qpack | ||
| quic | ||
| server | ||
| tests | ||
| tls_server | ||
| vendor | ||
| .dockerignore | ||
| .editorconfig | ||
| .gitignore | ||
| body.odin | ||
| cookie.odin | ||
| handlers.odin | ||
| headers.odin | ||
| http.odin | ||
| LICENSE | ||
| mimes.odin | ||
| mod.pkg | ||
| odinfmt.json | ||
| README.md | ||
| request.odin | ||
| response.odin | ||
| responses.odin | ||
| status.odin | ||
| types.odin | ||
| url.odin | ||
Odin HTTP
A HTTP/1.1 implementation for Odin purely written in Odin (besides SSL).
See generated package documentation at odin-http.laytan.dev.
See below examples or the examples directory.
Compatibility
This is beta software, confirmed to work in my own use cases but can certainly contain edge cases and bugs that I did not catch. Please file issues for any bug or suggestion you encounter/have.
I am usually on a recent master version of Odin and commits will be made with new features if applicable, backwards compatibility or even stable version compatibility is not currently a thing.
Because this is still heavily in development, I do not hesitate to push API changes at the moment, so beware.
The package has been tested to work with Ubuntu Linux (other "normal" distros should work), MacOS (m1 and intel), and Windows 64 bit. Any other distributions or versions have not been tested and might not work.
Dependencies
HTTPS clients and TLS servers depend on OpenSSL and/or BoringSSL (see openssl/,
boringssl/, tls_server/). Windows may use bundled static libs; Linux/macOS
typically use system libraries (libssl).
Performance
Some small benchmarks have been done in the comparisons directory.
My main priority in terms of performance is currently Linux (because most servers end up there in production).
Other targets are still made to be performant, but benchmarking etc. is mostly done on Linux.
IO implementations
Although these implementation details are not exposed when using the package, these are the underlying kernel API's that are used.
- Windows: IOCP (IO Completion Ports)
- Linux: io_uring
- Darwin: KQueue
I/O lives in package server (plain HTTP/1.1, TLS ALPN h2/h1, H3 via
listen_and_serve_config). Shared types and respond_* are package http
(no socket server). Routing: server.Router. Upstream nbio baseline:
vendor/laytan/odin-http. See docs/MIGRATION.md and docs/MERGE_MAIN.md.
Server example
package main
import "core:fmt"
import "core:log"
import "core:net"
import "core:time"
import http "../.." // Change to path of package.
import "../server" // package server host + Router
main :: proc() {
context.logger = log.create_console_logger(.Info)
// Router: same shape as laytan http.Router (init → route_* → router_handler).
// Patterns use path segments: {name} params and trailing * wildcards
// (not Lua patterns). Routes are tried by specificity, not registration order.
router: server.Router
server.router_init(&router)
// /users/{user}/comments/{id} — was: /users/(%w+)/comments/(%d+)
// Params: server.url_param(req, "user") — was: req.url_params[0]
server.route_get(&router, "/users/{user}/comments/{id}", http.handler(proc(req: ^http.Request, res: ^http.Response) {
user, _ := server.url_param(req, "user")
id, _ := server.url_param(req, "id")
http.respond_plain(res, fmt.tprintf("user %s, comment: %s", user, id))
}))
server.route_get(&router, "/cookies", http.handler(cookies))
server.route_get(&router, "/api", http.handler(api))
server.route_get(&router, "/ping", http.handler(ping))
server.route_get(&router, "/index", http.handler(index))
// Catch-all remainder: /* — was: (.*)
server.route_get(&router, "/*", http.handler(static))
server.route_post(&router, "/ping", http.handler(post_ping))
s: server.Server
server.server_shutdown_on_interrupt(&s)
log.info("Listening on http://localhost:6969")
err := server.listen_and_serve(
&s,
server.router_handler(&router),
net.Endpoint{address = net.IP4_Loopback, port = 6969},
)
fmt.assertf(err == .None, "server stopped with error: %v", err)
}
cookies :: proc(req: ^http.Request, res: ^http.Response) {
append(
&res.cookies,
http.Cookie{
name = "Session",
value = "123",
expires_gmt = time.now(),
max_age_secs = 10,
http_only = true,
same_site = .Lax,
},
)
http.respond_plain(res, "Yo!")
}
api :: proc(req: ^http.Request, res: ^http.Response) {
if err := http.respond_json(res, req.line); err != nil {
log.errorf("could not respond with JSON: %s", err)
}
}
ping :: proc(req: ^http.Request, res: ^http.Response) {
http.respond_plain(res, "pong")
}
INDEX_HTML :: #load("examples/complete/static/index.html")
index :: proc(req: ^http.Request, res: ^http.Response) {
// Prefer embed; disk respond_file/dir are deprecated (docs/MIGRATION.md § Static files).
http.respond_file_content(res, "index.html", INDEX_HTML)
}
static :: proc(req: ^http.Request, res: ^http.Response) {
rest, _ := server.url_param(req, "*")
if rest == "" || rest == "index.html" {
http.respond_file_content(res, "index.html", INDEX_HTML)
return
}
http.respond_plain(res, fmt.tprintf("not found: %q", rest), .Not_Found)
}
post_ping :: proc(req: ^http.Request, res: ^http.Response) {
http.body(req, len("ping"), res, proc(res: rawptr, body: http.Body, err: http.Body_Error) {
res := cast(^http.Response)res
if err != nil {
http.respond(res, http.body_error_status(err))
return
}
if body != "ping" {
http.respond(res, http.Status.Unprocessable_Content)
return
}
http.respond_plain(res, "pong")
})
}
Router at a glance
Laytan / in-tree http.Router |
This fork server.Router |
|
|---|---|---|
| Setup | http.router_init / route_get / router_handler |
server.router_init / route_get / router_handler |
| Patterns | Lua e.g. /users/(%w+) |
Segments e.g. /users/{user} |
| Catch-all | (.*) |
/* or /static/* |
| Params | req.url_params[0] |
server.url_param(req, "user") |
Full port table: docs/MIGRATION.md § Router. Example: examples/routing.
Client example
package main
import "core:fmt"
import "../../client"
main :: proc() {
res, err := client.get("https://www.google.com/")
if err != .None {
fmt.printf("Request failed: %v\n", err)
return
}
defer client.response_destroy(&res)
fmt.printf("Status: %v version: %v body: %d bytes\n", res.status, res.version, len(res.body))
}
See also examples/client_get (h1/h2/h3 flags), examples/server_serve,
docs/LIBRARY.md, and docs/MIGRATION.md.
Upstream nbio-only baseline (for benchmarks): vendor/laytan/odin-http.