Transport Layer
AFast supports multiple transport layers that can run simultaneously on different ports.
HTTP
HTTP server endpoints:
| Method | Path | Description |
|---|---|---|
| POST | /_api | Binary handler dispatch |
| GET | /_ws | WebSocket upgrade (merged mode) |
| GET | /code/{service}/{lang} | On-demand code gen (requires code) |
| GET | /doc | API docs index (requires doc) |
| GET | /doc/{service} | Service-specific docs (requires doc) |
| * | Ordinary routes | RESTful endpoints (requires ordinary-http) |
| GET | /path/:param | WebSocket upgrade for ordinary-ws routes (requires ordinary-ws) |
| GET | /path | SSE stream for ordinary-sse routes (requires ordinary-sse) |
HTTP Response Format:
- Success:
[0u8][0i64][data: bytes] - Error:
[1u8][code: i64][message: bytes]
WebSocket
WS frame format:
Request: [req_id: SeqId][handler_id: u32][len: Len][payload]
Push: [0: SeqId][conn_id: u32][len: Len][payload]
Heartbeat:[0xFFFFFFFF: SeqId][len: Len][conn_id1: u32]...
SeqId type is controlled by the seq64 feature (i32 or i64), Len type by the len64 feature (u32 or u64).
TCP
TCP uses 4-byte big-endian length-prefix framing with complete binary payloads per frame. Suitable for embedded devices or raw TCP scenarios.
HTTP + WS Port Merging
When ws_addr and http_addr are set to the same address, AFast merges WebSocket into the HTTP server via HTTP Upgrade:
#![allow(unused)]
fn main() {
// Same port for both HTTP and WebSocket
let app = AFast::new()
.service(svc)
.ws("0.0.0.0:5000")
.http("0.0.0.0:5000"); // Same address, auto-merged
}
TLS / HTTPS
AFast supports TLS/HTTPS via rustls with ALPN negotiation for HTTP/2.
Basic Usage
#![allow(unused)]
fn main() {
let app = AFast::new()
.service(svc)
.https("0.0.0.0:5443", "./cert.pem", "./key.pem", None);
}
Graceful Fallback
If certificate files don’t exist, the server automatically falls back to plain HTTP:
afast: TLS cert files not found, starting without encryption: [::]:5443
Hot-Reload Certificates
Reload certificates at runtime via a broadcast channel without restarting:
#![allow(unused)]
fn main() {
let (reload_tx, reload_rx) = tokio::sync::broadcast::channel(1);
let app = AFast::new()
.https("0.0.0.0:5443", "./cert.pem", "./key.pem", Some(reload_rx));
// Reload with original paths
reload_tx.send(None).unwrap();
// Reload with new paths
reload_tx.send(Some(TlsReloadMessage {
cert_path: "/new/cert.pem".into(),
key_path: "/new/key.pem".into(),
})).unwrap();
}
Ordinary HTTP (REST)
With ordinary-http, define RESTful routes using get/post/put/patch/delete inside the service! macro:
#![allow(unused)]
fn main() {
use afast::{get, Query, Param, Body, Header, Json, HttpResult};
#[derive(Deserialize)]
struct UserQuery { page: i64, size: i64 }
#[derive(Serialize)]
struct UserResponse { id: i64, name: String }
#[get(":id")]
async fn get_user(
state: State<AppState>,
param: Param<UserPath>,
query: Query<UserQuery>,
) -> HttpResult<Json<UserResponse>> {
Ok(Json(UserResponse { id: param.id, name: format!("User {}", param.id) }))
}
}
Response Types
| Type | HTTP Status | Content-Type |
|---|---|---|
Json<T> | 200 | application/json |
Text | 200 | text/plain |
Html | 200 | text/html |
File | 200 | Custom + Content-Disposition: attachment |
Status(code) | Custom | — |
Redirect::temporary(url) / Redirect::permanent(url) | 302 / 301 | Location header |
CORS
Enable CORS (Cross-Origin Resource Sharing) via AFast::cors() (requires the http feature). All HTTP endpoints — including binary /_api, ordinary HTTP routes, and code/doc endpoints — automatically include CORS headers:
#![allow(unused)]
fn main() {
use afast::{AFast, CorsConfig};
// Development: allow all origins
AFast::new()
.cors(CorsConfig::permissive())
.http("0.0.0.0:5000")
.run().await;
// Production: specific origins with credentials
AFast::new()
.cors(
CorsConfig::new(vec!["https://example.com", "https://app.example.com"])
.allow_credentials(true)
.max_age(7200)
)
.http("0.0.0.0:5000")
.run().await;
}
The server automatically:
- Responds to
OPTIONSpreflight requests with204 No Content - Injects
Access-Control-Allow-Origininto every HTTP response - Sets
Access-Control-Allow-Methods,Access-Control-Allow-Headers, andAccess-Control-Max-Agefor preflight
Security Headers
Every HTTP response includes these security headers by default:
| Header | Value |
|---|---|
x-content-type-options | nosniff |
x-frame-options | DENY |
content-security-policy | default-src 'self' |
Override via AFast::security_headers():
#![allow(unused)]
fn main() {
AFast::new()
.security_headers(vec![
("x-content-type-options", "nosniff"),
("x-frame-options", "SAMEORIGIN"),
("content-security-policy", "default-src 'self'; script-src 'self' 'unsafe-inline'"),
])
.http("0.0.0.0:5000")
.run().await;
}
Ordinary WebSocket
With ordinary-ws, define WebSocket routes using ws inside the service! macro. These routes use text/JSON frames instead of the binary protocol:
#![allow(unused)]
fn main() {
use afast::{ws, WsSender, WsReceiver, WsParam};
#[derive(Deserialize)]
struct ChatParam { room: String }
#[ws(desc("Chat room"))]
async fn chat_ws(
param: WsParam<ChatParam>,
sender: WsSender,
mut receiver: WsReceiver,
) -> afast::Result<()> {
while let Some(msg) = receiver.recv().await {
if let afast::WsMessage::Text(text) = msg {
sender.send_text(format!("[{}] {}", param.0.room, text)).await?;
}
}
Ok(())
}
let svc = service!("chat" => {
ws("/chat/:room", chat_ws),
});
}
Connect: ws://host:port/chat/general?token=abc
TS/JS clients auto-generate platform-aware WebSocket connection methods, compatible with browsers, UniApp, and WeChat Mini Programs.
Server-Sent Events (SSE)
Requires the ordinary-sse feature. Register SSE routes with sse():
#![allow(unused)]
fn main() {
use afast::{SseSender, Query};
#[derive(Deserialize)]
struct SseQuery { room: Option<String> }
#[afast::sse(desc("SSE event stream"))]
async fn notifications(
query: Query<SseQuery>,
sender: SseSender,
) -> afast::Result<()> {
let room = query.0.room.unwrap_or_default();
// Send named event
sender.send_event("connected", &serde_json::json!({"room": room})).await?;
// Send data event (auto-serialized as JSON)
let mut count = 0u64;
loop {
tokio::time::sleep(Duration::from_secs(1)).await;
count += 1;
if sender.send_event("tick", &serde_json::json!({"count": count})).await.is_err() {
break; // Client disconnected
}
}
Ok(())
}
let svc = service!("events" => {
sse("/notifications", notifications),
});
}
Connect: GET http://host:port/notifications?room=general
Response headers:
Content-Type: text/event-stream; charset=utf-8Cache-Control: no-cacheConnection: keep-aliveTransfer-Encoding: chunked
Wire format:
event: connected
data: {"room":"general"}
event: tick
data: {"count":1}
SseSender Methods
| Method | Description |
|---|---|
send<T: Serialize>(data) | Send a data: event with JSON-serialized value |
send_event<T: Serialize>(event, data) | Send a named event with JSON-serialized value |
SseEvent Fields
| Field | Type | Wire Format |
|---|---|---|
event | Option<&str> | event: name\n |
data | String | data: ...\n |
id | Option<&str> | id: ...\n |
retry | Option<u64> | retry: ...\n |
Client Codegen
- TS/JS: Generates
EventSource-based methods - KT (OkHttp): Uses
okhttp3.sse.EventSource - KT (non-OkHttp): Uses
java.net.http.HttpClientwithBodyHandlers.ofLines()
// TS/JS generated client
const es = client.apis.sse_stream({ room: "general" });
es.addEventListener("connected", (e) => console.log("Connected:", e.data));
es.addEventListener("tick", (e) => console.log("Tick:", JSON.parse(e.data)));
es.close();
Long Connections
Handlers using Receiver/Sender are auto-detected as long-connection mode:
#![allow(unused)]
fn main() {
#[handler(desc("Chat"))]
async fn chat(
state: State<AppState>,
auth: Custom<Auth>,
mut receiver: Receiver,
sender: Sender,
) -> Result<()> {
sender.send(b"Welcome!".to_vec()).await?;
while let Some(msg) = receiver.recv().await {
sender.send(msg).await?; // echo
}
Ok(())
}
}
Generated clients return a Socket object for long-connection handlers, with send()/close() and onMessage callback.