Two guards against abuse. A body cap per route in bytes: the global cap is NYX_HTTP_MAX_BODY (1 MiB) and a route can be stricter or looser — app_max_body(app, "POST", "/contacto", 16384) for a contact form, 8 MiB for photos. Over the cap the answer is 413. The recipe plays the client with raw TCP sockets so it can run on its own.
// std/serve limits: a body cap per route and a request limiter per origin // Límites de std/serve: tope de cuerpo por ruta y limitador de peticiones por origen // // Body cap per route (bytes). The global cap is NYX_HTTP_MAX_BODY (1 MiB); a route // can be stricter or looser: // // app_max_body(app, "POST", "/contacto", 16384) // a contact form: 16 KiB // app_max_body(app, "POST", "/foto", 8388608) // photos: 8 MiB // // Over the cap: 413. The parser reads up to the largest cap declared by any route // before a route can refuse, so raise a cap only where it is needed. // // Request limiter per origin (fixed window), SHARED by all the workers of the // process (they are threads of one process, guarded by a mutex): // // app_rate_limit(app, 120, 60) // whole app: 120 per minute // app_route_rate_limit(app, "POST", "/contacto", 5, 60) // one route: 5 per minute // // Over the limit: 429 with `Retry-After: <seconds until the window closes>`. The // origin is the client's IP; behind a reverse proxy every request comes from the // proxy, so count by another key with app_rate_limit_key(app, fn), for example // rate_key_forwarded_for (first X-Forwarded-For) or a user id. An empty key // exempts the request. Counts are per process: two instances behind a balancer // count separately. // // This recipe plays the client with raw TCP sockets so it can run on its own. import "std/web" import "std/serve" fn recibir(req: Request) -> Response { return response_text(200, "got " + int_to_string(req.body.length()) + " bytes") } fn ping(req: Request) -> Response { return response_text(200, "pong") } // The origin is named by a header so the recipe can play several clients from // one machine. A real app would use rate_key_forwarded_for or the default (the IP). fn origen(req: Request) -> String { return http_find_header(req.headers_flat, "X-Client") } // One request; the status code of the answer (0 = no answer). fn pedir(port: int, method: String, path: String, client: String, body: String) -> int { var fd: int = 0 - 1 var tries: int = 0 while fd < 0 and tries < 50 { fd = tcp_connect("127.0.0.1", port) if fd < 0 { sleep(100) } tries = tries + 1 } if fd < 0 { return 0 } tcp_set_timeout(fd, 5) var req: String = method + " " + path + " HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\n" if client.length() > 0 { req = req + "X-Client: " + client + "\r\n" } if method == "POST" { req = req + "Content-Length: " + int_to_string(body.length()) + "\r\n" } tcp_write(fd, req + "\r\n" + body) let resp: String = tcp_read_partial(fd, 4096) tcp_close(fd) if resp.length() < 12 { return 0 } return string_to_int_or(resp.substring(9, 12), 0) } fn main() -> int { let app: App = app_new() let port: int = 18214 app_get(app, "/ping", ping) app_post(app, "/contacto", recibir) app_max_body(app, "POST", "/contacto", 100) app_rate_limit_key(app, origen) app_rate_limit(app, 4, 60) spawn { serve_app(app, port, 2) } print("100-byte form: " + int_to_string(pedir(port, "POST", "/contacto", "", "0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789"))) print("101-byte form: " + int_to_string(pedir(port, "POST", "/contacto", "", "01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"))) var i: int = 1 while i <= 5 { print("client ana, request " + int_to_string(i) + ": " + int_to_string(pedir(port, "GET", "/ping", "ana", ""))) i = i + 1 } print("client beto, request 1: " + int_to_string(pedir(port, "GET", "/ping", "beto", ""))) return 0 }
nyx-serve starting on 127.0.0.1:18214 with 2 workers 100-byte form: 200 101-byte form: 413 client ana, request 1: 200 client ana, request 2: 200 client ana, request 3: 200 client ana, request 4: 200 client ana, request 5: 429 client beto, request 1: 200
How it works
The parser reads up to the largest cap declared by ANY route before a route can refuse, so raise a cap only where it is needed. The request limiter counts per origin in a fixed window and is SHARED by every worker of the process (they are threads of one process, guarded by a mutex): app_rate_limit(app, 120, 60) for the whole app, app_route_rate_limit(app, "POST", "/contacto", 5, 60) for one route. Over the limit the answer is 429 with Retry-After set to the seconds until the window closes — see request 5 of client ana
, while beto
still gets 200.
The origin is the client's IP, and behind a reverse proxy every request comes from the proxy; count by another key with app_rate_limit_key(app, fn), for example rate_key_forwarded_for (the first X-Forwarded-For) or a user id. An empty key exempts the request. Counts are per process: two instances behind a balancer count separately.