HTTP Sunucusu (Wings)
Wings, Tulpar ile birlikte gelen gömülü HTTP framework’üdür.
import "wings" dedikten sonra 20 satırın altında production-şeklinde
bir JSON API’ye varman için tasarlandı: health check, metrics, OpenAPI
ve multi-thread işleme zaten bağlanmış halde.
Hello world
Section titled “Hello world”import "wings";
func index(req) { return {"hello": "world"};}
get("/", index); // handler'ı adıyla bağla — string değilserve(); // port yok → varsayılan 8484; açık için serve(8080)Bu eksiksiz bir HTTP/1.1 sunucusu — keep-alive, CORS uyumlu varsayılan
header’lar, /healthz + /metrics otomatik kayıtlı. Route’lar handler
fonksiyonunu bağlar (index, "index" değil) — yanlış yazım
derleme hatası, sessiz 404 değil.
Port’suz serve() Tulpar’ın varsayılan portunu kullanır: 8484 (ASCII T
= 84 → ikili 01010100, “Tulpar portu”); doluysa yukarı yürür (8485, 8486, …)
ki ikinci bir uygulama da başlayabilsin. Açık port verirsen — serve(8080) —
Wings tam onu bağlar ve doluysa sessizce kaydırmak yerine seni bilgilendirir.
serve() = listen(); ikisi de aynı opsiyonel port’u alır.
Routing
Section titled “Routing”get("/users/:id", show_user); // path param'lar req.params'a gelirpost("/users", create_user);put("/users/:id", update_user);del("/users/:id", delete_user);
func show_user(req) { return orm_find("users", toInt(req.params.id));}Her handler isteği ilk parametre (req) olarak alır. Alanları noktalı
erişimle okuyun — req.params.id, req.query.page, req.json (otomatik
parse edilmiş gövde). Aynı veri global _request’te de var, yani
parametre almayan bir handler da çalışır:
| Alan | Tip | Notlar |
|---|---|---|
method | str | GET, POST, PUT, DELETE, … |
path | str | URL path (query’siz). |
raw_path | str | Path + query, client’in gönderdiği. |
query | json | Parse edilmiş ?k=v&... (URL-decoded). |
headers | json | Header haritası (case-sensitive). |
body | str | Ham body byte’ları, length-bounded. |
Middleware (use)
Section titled “Middleware (use)”use("fn_name") ile global bir middleware kaydet. Kaydedilen her
middleware — kayıt sırasıyla — eşleşen handler’dan önce, tüm serve
modlarında çalışır (tek bir dispatch noktası var). Bir middleware,
func mw(req) biçiminde: ya bir response dict döner (_status set →
zincir kısa devre yapar ve o yanıt gönderilir) ya da {} döner
(devam). req’i yerinde değiştirebilir, böylece sonraki middleware’ler ve
handler değişikliği görür:
func require_auth(req) { str token = req["headers"]["Authorization"]; if (length(token) == 0) { return unauthorized("token required"); // kısa devre → 401 } req["user"] = {"id": 1, "name": "Ada"}; // handler'a görünür return {}; // devam}
use("require_auth");Hiç middleware kaydedilmediğinde zincir sıfır maliyetlidir — Wings hot path’te onu tamamen atlar.
Route grupları (group)
Section titled “Route grupları (group)”group(prefix, register_fn), register_fn’i (bir fonksiyon adı)
kaydettiği her route’a uygulanan bir path prefix’iyle çalıştırır. Prefix
çağrının etrafında saklanır ve geri yüklenir, böylece gruplar iç içe
geçebilir:
func api_v1() { get("/users", "list_users"); // → /api/v1/users post("/users", "create_user"); // → /api/v1/users}group("/api/v1", "api_v1");get / post / put / del / cached_get hepsi aktif prefix’i dikkate
alır, yani iç group(...) çağrıları birleşir (/api/v1 + /admin →
/api/v1/admin).
Dependency injection (depends / dep)
Section titled “Dependency injection (depends / dep)”FastAPI tarzı route-başına bağımlılıklar. depends("fn_name") bağımlılığı
en son kaydedilen route’a iliştirir; handler çalışmadan önce her
bağımlılık req ile çağrılır ve dönüş değeri enjekte edilir. Handler bunu
dep("name") ile okur. Bir bağımlılık, response dict döndürerek (_status
set) kısa devre yapabilir — tıpkı middleware gibi, ama route-başına ve
değer üreten:
func current_user(req) { str t = req["headers"]["Authorization"]; if (length(t) == 0) { return unauthorized("token required"); } return {"id": 1, "name": "Ada"};}
func profile(req) { return ok(dep("current_user")); }
get("/profile", "profile");depends("current_user");Çözülen değerler thread-local bir depoda durur, böylece listen_pool /
listen_async altında eşzamanlı istekler arasında sızmaz.
Tek-thread vs multi-thread
Section titled “Tek-thread vs multi-thread”listen(8080); // tek accept loop, her bağlantıda keep-alivelisten_async(8080); // bağlantı başına thread, paralel recv/sendlisten() test edilmiş tek-thread sunucu: kabul edilen her socket’te
çoklu keep-alive request’i seri serve eder. En düşük request-başı
latency’ye (~0.22 ms p50) sahip ama seri bir accept döngüsü olduğundan
eşzamanlı keep-alive bağlantılarını serileştirir — ham throughput için
listen_pool tercih et (Go net/http’yi geçiyor). Tam karşılaştırma
için Benchmark sayfasına bak.
listen_async() her TCP bağlantısı için detached worker spawn’lar.
Her worker kendi recv / parse / send’ini paralel yapar; handler
dispatch _wings_handler_mu mutex’i altında serileşir (LLVM
thread-local global’leri inene kadar). Kazanç: paralel accept ve
çoklu keep-alive bağlantısı eşzamanlı serve.
Otomatik route’lar
Section titled “Otomatik route’lar”listen() ve listen_async() çağrısında, sen kendin kayıt etmediysen
production’ın beklediği iki route otomatik eklenir:
/healthz
Section titled “/healthz”{"status":"ok","uptime_s":142,"now":"2026-05-02T18:34:21Z"}Kubernetes liveness probe ile uyumlu. listen çağrısından önce kendi
GET /healthz handler’ını kayıt edersen otomatik olan eklenmez.
/metrics
Section titled “/metrics”{"uptime_s":142,"requests_total":5021,"requests_2xx":4998, "requests_4xx":23,"requests_5xx":0,"routes":7}Wings her response sonrası counter’ları artırır. Prometheus için
?format=prom veya kendi route’unla wings_metrics_prom() çağır.
OpenAPI auto-gen
Section titled “OpenAPI auto-gen”func openapi_handler() { return wings_openapi("My API", "1.0.0");}get("/openapi.json", "openapi_handler");Kayıtlı route’lardan OpenAPI 3.0 dokümanı üretir. Swagger UI / Postman direkt tüketir.
Structured logging
Section titled “Structured logging”log_info("kullanıcı kayıt oldu: " + email);log_error("ödeme başarısız: " + toString(code));Çıktı (her satır bir JSON — log aggregator-uyumlu):
{"@timestamp":"2026-05-02T18:34:21Z","level":"info","msg":"kullanıcı kayıt oldu: a@b.c"}Wings’in kendi access log’u da bu formatı kullanır. Benchmark / production
yolda satırı susturmak için TULPAR_HTTP_QUIET=1.
Response helper’ları
Section titled “Response helper’ları”Düz bir obje döndürün, 200 application/json olarak serileşir. Diğer
durum kodları için {"_status": N} envelope’unu elle yazmak yerine
helper’ları kullanın — niyet okunur:
func get_user(req) { json u = find_user(); if (!u) { return not_found("kullanıcı yok"); } return ok(u); // 200}
func create_user(req) { return created(new_user()); // 201}Mevcut: ok(data), created(data) (201), no_content() (204),
bad_request(msg) (400), unauthorized(msg) (401), forbidden(msg)
(403), not_found(msg) (404), conflict(msg) (409), server_error(msg)
(500). Hata helper’ları ortak {"error": msg} gövdesi paylaşır.
text(body) düz metin döner; with_status(data, code) herhangi bir kod.
İstek gövdesini okuma
Section titled “İstek gövdesini okuma”JSON istek gövdesi sizin için parse edilir — doğrudan _request["json"]
okuyun, fromJson() gerekmez:
func create_user(req) { json body = req.json; // zaten parse edildi if (length(body["name"]) == 0) { return bad_request("name gerekli"); } return created({"name": body["name"]});}req (ve global _request) ayrıca şunları taşır: params (:id gibi
path yakalamaları), query, headers, cookies, body (ham), ve
method / path.
İstek gövdesini doğrulama
Section titled “İstek gövdesini doğrulama”Bir route’u kaydettikten hemen sonra body_schema() ile şema iliştir. Gövde
handler çalışmadan önce kontrol edilir — geçersiz gövde koduna hiç
ulaşmaz ve tüm hatalı alanları listeleyen otomatik bir 422 alır:
post("/users", create_user);body_schema({"name": "str", "age?": "int"}); // sonda ? = opsiyonelPOST {"name": 123} → 422 {"error":"validation failed", "fields":{"name":"expected str, got int"}}POST {} → 422 {"fields":{"name":"required"}}POST {"name":"Ada"} → create_user'a ulaşırTipler: str, int, float, num (int veya float), bool, array,
object, json (object veya array), any (var, herhangi bir tip).
Tipli query parametreleri
Section titled “Tipli query parametreleri”req["query"] ham string değerleri tutar. Tipli erişimciler varsayılan bir
fallback ile coerce eder, böylece bir liste endpoint’i sayfalama/filtreleri
tek satırda okur — varlığı elle kontrol edip toInt() çağırmak yerine:
func list_items(req) { int page = query_int(req, "page", 1); // ?page=2 → 2, yoksa → 1 str sort = query(req, "sort", "id"); // ?sort=name → "name" bool desc = query_bool(req, "desc", false); // 1/true/yes → true, 0/false/no → false return ok(fetch_items(page, sort, desc));}query_int eksik veya boş değerde fallback döner; query_bool
1/true/yes ve 0/false/no (büyük/küçük harf duyarsız) kabul eder,
aksi halde fallback’e düşer.
Response model (çıktı şekillendirme)
Section titled “Response model (çıktı şekillendirme)”body_schema()’nın simetrik karşılığı: en son kaydedilen route’a bir şema
iliştirerek başarılı çıktıyı yalnızca bildirilen alanlara filtreler.
Listelenmeyen her şey (bir password_hash, bir _internal bayrağı)
serileştirmeden önce düşürülür, böylece bir handler tüm satırını sır
sızdırmadan dönebilir:
get("/users/:id", "show_user");response_model({"id": "int", "name": "str"}); // password vb. düşürülürTek bir obje veya obje dizisi üzerinde çalışır. Hatalar (status ≥ 400)
filtrelemeyi atlar, böylece bir {"error": ...} gövdesi asla kırpılmaz.
Dosya yükleme (multipart/form-data)
Section titled “Dosya yükleme (multipart/form-data)”multipart/form-data istekleri için metin alanlarını
form(req, name, fallback) ile, yüklenen dosyaları ise uploaded_files(req)
ile oku — {name, filename, content_type, data, size} dizisi. Dosya data’sı
ham byte taşır (binary-safe, length-tracked), böylece PNG’ler ve diğer
binary’ler byte-exact round-trip eder:
func upload(req) { str title = form(req, "title", ""); for (f in uploaded_files(req)) { write_file("./uploads/" + f["filename"], f["data"]); } return created({"title": title, "count": length(uploaded_files(req))});}post("/upload", "upload");Parse lazy’dir (yalnızca form / uploaded_files çağrılınca) ve native
parse_multipart builtin’iyle desteklenir. Her çağrı yeniden parse eder; çok
alanlı durumlarda bir kez form_data(req) çağır ve dönen {fields, files}’i
doğrudan oku.
İnteraktif dokümanlar (Swagger UI)
Section titled “İnteraktif dokümanlar (Swagger UI)”serve() otomatik olarak /docs (Swagger UI) ve onu besleyen
/openapi.json uçlarını route’larından üreterek bağlar — :id path
param’ları ve her body_schema() OpenAPI parametre ve request body’sine
dönüşür. Başlık/sürümü serve() öncesi docs_info("Users API", "1.0.0") ile
ayarla. Kapatmak için TULPAR_WINGS_NODOCS=1 ortam değişkeni veya /docs’u
kendin kaydet.
In-memory durum (otomatik persist)
Section titled “In-memory durum (otomatik persist)”Wings her isteğin belleğini yanıttan sonra geri alır (arena reset); yani
handler’ın ürettiği bir değer istek bitince yok olur. Veriyi uzun-ömürlü
bir global’de tutmak — in-memory “veritabanı” deseni — istediğin tek
şey: global’e yaz, gerisini Tulpar halleder. Global’e yapılan yazımlar
(push(_users, u), _users[i]["name"]=v, _g = v) derleme zamanında
otomatik kalıcılaştırılır — manuel bir çağrı gerekmez:
import "wings";
json _users = [{"id": 1, "name": "Ada"}];int _next_id = 2;
func list_users(req) { return ok(_users); }
func create_user(req) { json body = req.json; json u = {"id": _next_id, "name": body["name"]}; _next_id = _next_id + 1; push(_users, u); // global'e push → otomatik kalıcı return created(u);}
get("/users", list_users);post("/users", create_user);serve(8080); // serve() == listen()Statik dosya sunumu (static)
Section titled “Statik dosya sunumu (static)”static(url_prefix, dir) bir dizini URL prefix’i altında mount eder.
Dosyalar bir 404-fallback olarak sunulur: gerçek route’lar (tam veya
:param) her zaman önce eşleşir, static yalnızca hiçbir şey eşleşmediğinde
kontrol edilen catch-all’dur:
static("/static", "./public"); // GET /static/app.css → ./public/app.cssDizin-tarzı bir istek (mount kökü veya / ile biten yol) index.html sunar,
böylece bir single-page app’i doğrudan kökten servis edebilirsin:
static("/", "./public"); // GET / → ./public/index.htmlPath traversal (..) reddedilir. Metin asset’leri
(html/css/js/json/svg/txt) doğru Content-Type alır; geri kalan
her şey application/octet-stream’e düşer. Binary asset’ler (gömülü NUL’lu
bir PNG) byte-exact round-trip eder, çünkü read_file /
http_create_response strlen yerine length-tracked çalışır.
Özel yanıtlar
Section titled “Özel yanıtlar”Varsayılan 200 application/json + CORS header’ları çoğu durumu karşılar. Düz
string gövdeli özel bir content type için _raw / _content_type
envelope’unu dön — Wings string’i JSON’a çevirmeden olduğu gibi gönderir:
func export_csv(req) { str body = "id,name\n1,Ada\n2,Linus\n"; return {"_raw": body, "_content_type": "text/csv; charset=utf-8"};}Ayrıca özel header’lara ihtiyacın olduğunda — örneğin Content-Disposition
indirme dosya adı — tam yanıtı sokete kendin http_create_response(...) ile yaz,
sonra {"_stream": 1} dön; böylece Wings wire’ı senin hallettiğini bilir ve
kendi framing’ini atlar:
func download_csv(req) { str body = "id,name\n1,Ada\n2,Linus\n"; json headers = {"Content-Disposition": "attachment; filename=\"users.csv\""}; str wire = http_create_response(200, "text/csv; charset=utf-8", body, headers, 0); socket_send(wings_current_fd(), wire); return {"_stream": 1};}http_create_response(status, content_type, body, headers, keep_alive) alttaki
primitif; wings_current_fd() aktif istek soketidir. Bu aynı {"_stream": 1}
deseni SSE veya WebSocket upgrade’i de bu şekilde yazılır.
HTTPS (Wings TLS)
Section titled “HTTPS (Wings TLS)”Wings, plain-HTTP listener’ın yanında HTTPS listener’ı da içeriyor. Aynı handler API’si, aynı routing — sadece listen çağrısı değişir:
import "wings";import "wings_tls";
func home() { return {"message": "TLS üzerinden Merhaba", "secure": true};}
get("/", "home");
wings_tls(8443, "./server.crt", "./server.key");Bu satırlar tam bir HTTPS sunucu. Her kabul edilen connection
SSL_accept → SSL_read → handler dispatch → SSL_write →
SSL_shutdown üzerinden geçer. SSL_CTX startup’ta bir kere
kurulur; cert ve key dosyaları wings_tls(...) çağrısında okunur,
her request’te değil.
Cert + key
Section titled “Cert + key”Local dev için bir yıl geçerli self-signed cert üret:
openssl req -x509 -newkey rsa:2048 \ -keyout server.key -out server.crt \ -days 365 -nodes \ -subj "/CN=localhost" \ -addext "subjectAltName=DNS:localhost,IP:127.0.0.1"İstemci tarafında curl --insecure https://127.0.0.1:8443/
self-signed cert’i kabul eder; tarayıcılar uyarı göstereceği için
tıklayıp geçmek gerek.
Production için gerçek cert chain’ini göster — Let’s Encrypt’in
/etc/letsencrypt/live/<domain>/fullchain.pem + privkey.pem
çifti standart, wings_tls da diğer PEM dosyaları gibi okur:
wings_tls(443, "/etc/letsencrypt/live/tulparlang.dev/fullchain.pem", "/etc/letsencrypt/live/tulparlang.dev/privkey.pem");Build gereksinimi
Section titled “Build gereksinimi”wings_tls OpenSSL-tabanlı. Tulpar build’inde
find_package(OpenSSL) başarılı olmalı — Linux için apt install libssl-dev, MSYS2 Windows için pacman -S mingw-w64-x86_64-openssl, macOS için brew install openssl.
OpenSSL link edilmemişse tls_init 0 döner, wings_tls hata
yazıp socket bind’lamadan çıkar.
v1’de henüz olmayanlar
Section titled “v1’de henüz olmayanlar”- TLS keep-alive. Şu an kabul edilen her TLS connection bir
request servis edip kapanıyor. Plain-HTTP
listen()multi-request keep-alive yapıyor; TLS yolu bunu_wings_serve_one_requesttarzı partial-read state machine işiyle kazanacak. - mTLS (client-cert doğrulama). TLS context şimdilik server-auth
only.
SSL_CTX_set_verify+ CA bundle yükleme küçük bir follow-up. - HTTP/2 + ALPN. Framework TLS üstü HTTP/1.1 konuşuyor; HTTP/2 ALPN müzakeresi sonraki adım.