Contrato runtime (event/context)¶
Estado verificado al 28 de marzo de 2026. Nota de runtime: FastFN resuelve dependencias y build por función según el runtime: Python usa
requirements.txt, Node usapackage.json, PHP instala desdecomposer.jsoncuando existe, y Rust compila handlers concargo. Enfastfn dev --nativenecesitas runtimes y herramientas del host;fastfn devdepende de un daemon de Docker activo. Este documento define exactamente que envia OpenResty al handler y que debe devolver el runtime.
1) Transporte interno¶
- Protocolo: socket Unix por runtime (
python,node,php,rust). - Framing:
4-byte big-endian length + JSON. - Request interno:
{ fn, version, event }. - Response interno:
{ status, headers, body }o binario base64.
2) Que envia el cliente HTTP y como llega al handler¶
Request publico (cliente -> gateway)¶
curl -sS 'http://127.0.0.1:8080/risk-score?email=user@example.com' \
-H 'x-user-email: user@example.com' \
-H 'x-api-key: my-key' \
-H 'Cookie: session_id=abc123; theme=dark' \
-H 'Content-Type: application/json' \
-d '{"extra":"value"}'
Mapeo en event¶
- query string ->
event.query - headers ->
event.headers - body raw (string) ->
event.body - cookies parseadas ->
event.session - IP/UA cliente ->
event.client - metadata de gateway/politica ->
event.context - env por funcion ->
event.env
3) Payload interno completo (gateway -> runtime)¶
{
"fn": "hello",
"version": "v2",
"event": {
"id": "req-1770795478241-13-311866",
"ts": 1770795478241,
"method": "GET",
"path": "/hello@v2",
"raw_path": "/hello@v2?name=NodeWay",
"query": {"name": "NodeWay"},
"headers": {
"host": "127.0.0.1:8080",
"user-agent": "curl/8.7.1",
"accept": "*/*",
"x-api-key": "my-key",
"cookie": "session_id=abc123"
},
"body": "",
"session": {
"id": "abc123",
"raw": "session_id=abc123",
"cookies": {"session_id": "abc123"}
},
"client": {"ip": "127.0.0.1", "ua": "curl/8.7.1"},
"context": {
"request_id": "req-1770795478241-13-311866",
"runtime": "node",
"function_name": "hello",
"version": "v2",
"timeout_ms": 1500,
"max_concurrency": 15,
"max_body_bytes": 1048576,
"gateway": {"worker_pid": 12345},
"debug": {"enabled": false},
"user": null
},
"env": {
"NODE_GREETING": "v2"
}
}
}
4) Referencia de campos event¶
| Campo | Tipo | Origen | Notas |
|---|---|---|---|
id |
string |
gateway | request id unico |
ts |
number |
gateway | epoch ms |
method |
string |
HTTP request | GET/POST/PUT/PATCH/DELETE |
path |
string |
gateway | ruta normalizada sin query |
raw_path |
string |
gateway | URI original con query |
query |
object |
query string | valores de URL |
headers |
object |
headers request | incluye auth/cookies si cliente envia |
body |
string o null |
request body | cuerpo raw, no parseado por gateway |
client.ip |
string |
gateway | IP remota |
client.ua |
string o null |
header | User-Agent |
context.request_id |
string |
gateway | mismo id |
context.runtime |
string |
discovery | runtime resuelto |
context.function_name |
string |
routing | nombre funcion |
context.version |
string |
routing | version efectiva |
context.timeout_ms |
number |
politica | timeout aplicado |
context.max_concurrency |
number |
politica | limite aplicado |
context.max_body_bytes |
number |
politica | limite body aplicado |
context.gateway.worker_pid |
number |
OpenResty | pid worker |
context.debug.enabled |
boolean |
politica | debug headers habilitados |
session |
object o null |
gateway | cookies parseadas (ver abajo) |
session.id |
string o null |
gateway | session id auto-detectado |
session.raw |
string |
gateway | valor crudo de Cookie |
session.cookies |
object |
gateway | mapa key:value de cookies |
context.user |
object o null |
/_fn/invoke |
contexto custom inyectado |
env |
object |
fn.env.json |
variables por funcion/version |
5) Inyeccion de context.user desde /_fn/invoke¶
curl -sS 'http://127.0.0.1:8080/_fn/invoke' \
-X POST \
-H 'Content-Type: application/json' \
--data '{
"name":"hello",
"method":"GET",
"query":{"name":"Ctx"},
"context":{"trace_id":"abc-123","tenant":"demo"}
}'
El handler lo recibe en:
event.context.user.trace_idevent.context.user.tenant
6) event.session - parseo de cookies¶
El gateway parsea automáticamente el header Cookie y lo expone como event.session. Si el request no trae Cookie, entonces event.session es null.
Forma¶
{
"id": "abc123",
"raw": "session_id=abc123; theme=dark",
"cookies": {
"session_id": "abc123",
"theme": "dark"
}
}
| Campo | Descripción |
|---|---|
id |
Identificador de sesión auto-detectado. Busca session_id, sessionid y sid en ese orden. |
raw |
Valor crudo del header Cookie tal como lo envía el cliente. |
cookies |
Mapa key:value con todas las cookies parseadas. |
Ejemplos de uso¶
Python:
def handler(event):
session = event.get("session") or {}
session_id = session.get("id")
theme = (session.get("cookies") or {}).get("theme", "light")
return {"status": 200, "body": f"session={session_id}, theme={theme}"}
Node:
exports.handler = async (event) => {
const session = event.session || {};
const sessionId = session.id;
const theme = (session.cookies || {}).theme || "light";
return { status: 200, body: `session=${sessionId}, theme=${theme}` };
};
Lua:
return function(event)
local session = event.session or {}
local sid = session.id
local theme = (session.cookies or {}).theme or "light"
return { status = 200, body = "session=" .. tostring(sid) .. ", theme=" .. theme }
end
Go:
package main
import "fmt"
func Handler(event map[string]interface{}) map[string]interface{} {
session, _ := event["session"].(map[string]interface{})
sid, _ := session["id"].(string)
cookies, _ := session["cookies"].(map[string]interface{})
theme, _ := cookies["theme"].(string)
if theme == "" { theme = "light" }
return map[string]interface{}{
"status": 200,
"body": fmt.Sprintf("session=%s, theme=%s", sid, theme),
}
}
7) Response del runtime (obligatorio)¶
Texto/JSON¶
Binario¶
{
"status": 200,
"headers": {"Content-Type": "image/png"},
"is_base64": true,
"body_base64": "iVBORw0KGgo..."
}
Respuesta sencilla (depende del runtime)¶
El contrato canonico sigue siendo { status, headers, body } (o { is_base64, body_base64 } para binario).
Algunos runtimes tambien aceptan atajos y los normalizan automaticamente:
| Runtime | Soporte de atajos | Que normaliza |
|---|---|---|
| Node | si | valores primitivos/objetos sin envelope (por ejemplo objeto -> JSON 200, string -> text/plain o text/html) |
| Python | parcial | dict sin envelope -> JSON 200; tambien soporta tupla (body, status, headers) |
| PHP | si | primitivos/arrays/objetos a envelope HTTP valido |
| Lua | si | valores sin envelope a JSON 200 |
| Go | no | requiere envelope explicito |
| Rust | no | requiere envelope explicito |
Recomendacion de portabilidad:
- Usa envelope explicito en ejemplos compartidos y codigo multi-runtime.
- Usa atajos solo cuando apuntas intencionalmente a un runtime especifico.
Passthrough estilo edge (proxy)¶
Una funcion puede devolver un campo proxy. Esto se parece a Cloudflare Workers return fetch(request):
- tu handler devuelve un request “declarativo”
- fastfn hace el request saliente dentro del gateway
- fastfn devuelve status/headers/body del upstream al cliente
- si
proxyestá presente, manda la respuesta del upstream (losstatus/headers/bodyde arriba quedan como fallback)
Ejemplo (Node):
exports.handler = async (event) => {
return {
status: 200,
headers: { "Content-Type": "application/json" },
proxy: {
path: "/hello?name=edge",
method: event.method || "GET",
headers: { "x-fastfn-edge": "1" },
body: event.body || "",
timeout_ms: (event.context || {}).timeout_ms || 2000
}
};
};
Ejemplo de filtro + rewrite (auth + passthrough)¶
Este patrón es lo más parecido al caso típico en Workers: validar la request entrante y luego reescribir/passthrough.
function header(event, name) {
const h = event.headers || {};
return h[name] || h[name.toLowerCase()] || h[name.toUpperCase()] || null;
}
exports.handler = async (event) => {
const env = event.env || {};
// Filtro: requiere API key
const expected = String(env.EDGE_FILTER_API_KEY || "");
const provided = String(header(event, "x-api-key") || "");
if (!expected || provided !== expected) {
return { status: 401, headers: { "Content-Type": "application/json" }, body: "{\"error\":\"unauthorized\"}" };
}
// Rewrite + passthrough
const userId = String((event.query || {}).user_id || "");
return {
proxy: {
path: "/v1/users/" + encodeURIComponent(userId),
method: "GET",
headers: { "x-edge": "1" },
timeout_ms: (event.context || {}).timeout_ms || 2000
}
};
};
Para permitir esto, habilitá edge en el fn.config.json de esa función (proxy viene deshabilitado por defecto).
Campos soportados (minimo):
url: URL absolutahttp(s)://...(o)path: path que empieza con/(requiereedge.base_urlenfn.config.json)method:GET|POST|PUT|PATCH|DELETEheaders: objeto de headers hacia upstreambody: body stringtimeout_ms: timeout del request (ms)max_response_bytes: limite de bytes del responseis_base64+body_base64: body de request en base64 (opcional)
Seguridad:
- el proxy está deshabilitado por defecto por funcion
- se habilita con
edgeenfn.config.json - proxyear a paths del control-plane (
/_fn/*,/console/*) está bloqueado
{
"edge": {
"base_url": "https://api.example.com",
"allow_hosts": ["api.example.com"],
"allow_private": false,
"max_response_bytes": 1048576
}
}
8) Tipos de respuesta soportados¶
application/jsontext/htmltext/csv- binarios como
image/png
/_fn/invoke envuelve respuestas no-texto en base64 para mantener salida JSON estable.
9) Captura de stdout/stderr¶
Todos los runtimes capturan salida del handler (print(), console.log(), eprintln!(), etc.) y la conservan junto a la respuesta. Esto sirve para debugging en consola, Quick Test y clientes externos que necesiten una señal corta.
Como funciona¶
| Runtime | captura stdout | captura stderr |
|---|---|---|
| Python | print(), sys.stdout.write() |
sys.stderr.write() |
| Node | console.log(), console.info(), console.debug() |
console.error(), console.warn() |
| Lua | print() |
— |
| PHP | echo, print (stdout del subprocess) |
error_log(), fwrite(STDERR, ...) (stderr del subprocess) |
| Go | fmt.Println() (stdout del subprocess) |
fmt.Fprintln(os.Stderr, ...) (stderr del subprocess) |
| Rust | println!() (stdout del subprocess) |
eprintln!() (stderr del subprocess) |
Campos en la respuesta interna¶
Cuando existe salida capturada, el runtime agrega stdout y stderr al JSON interno:
{
"status": 200,
"headers": {"Content-Type": "application/json"},
"body": "{\"ok\":true}",
"stdout": "debug: procesando request\nuser_id=42",
"stderr": "warning: campo deprecated"
}
Si no hay salida, esos campos se omiten.
Headers de debug¶
Cuando context.debug.enabled es true, el gateway expone esa salida como headers:
X-Fn-Stdout— stdout capturado, truncado a 4096 bytesX-Fn-Stderr— stderr capturado, truncado a 4096 bytes
Estos headers sirven para una inspeccion rapida desde un cliente externo, pero no son la salida completa.
Quick Test y consola¶
El endpoint /_fn/invoke devuelve stdout y stderr completos en JSON. El panel Quick Test de la consola los muestra en secciones separadas.
Salida completa en logs¶
Cuando ejecutas FastFN localmente, la salida capturada del handler tambien se escribe en logs del runtime con un prefijo por funcion:
[python] [fn:hello@default stdout] {'query': {'id': '42'}}
[python] [fn:hello@default stderr] warning: falta un campo opcional
Usa este camino cuando necesites la salida completa. Es mas facil de leer que los headers y no trunca payloads grandes.
Dos formas comunes de leerla:
fastfn logs --native --file runtime --lines 100
curl -sS 'http://127.0.0.1:8080/_fn/logs?file=runtime&format=json&runtime=python&fn=hello&version=default&stream=stdout&lines=50' \
-H 'x-fn-admin-token: my-secret-token'
Usa el CLI cuando estas en la misma maquina. Usa /_fn/logs?file=runtime para herramientas admin o internas que necesiten el stream completo por HTTP.
10) Modo estricto de filesystem (por defecto)¶
fastfn ejecuta handlers con modo estricto de filesystem habilitado por defecto:
FN_STRICT_FS=1(default)FN_STRICT_FS_ALLOW=/ruta/a/permitir,/otra/ruta(opcional)
Reglas:
- la funcion puede leer/escribir dentro de su propio directorio
- no puede leer rutas arbitrarias fuera de su sandbox
- no puede leer archivos protegidos de plataforma:
fn.config.jsonfn.env.json- no se permite spawn de subprocess desde handlers en modo estricto
Nota de implementacion:
- Python y Node aplican bloqueo estricto de filesystem a nivel runtime.
- PHP y Rust corren en procesos aislados con validacion de rutas y ejecucion acotada, pero hoy no interceptan por archivo la lista protegida.
Importante:
- es un sandbox a nivel runtime (lenguaje), no un aislamiento kernel (cgroups/seccomp/chroot).
Diagrama del Contrato Runtime¶
flowchart TD
A["Evento del gateway"] --> B["Adaptador runtime"]
B --> C["Handler de usuario"]
C --> D["Respuesta FastFN normalizada"]
Contrato¶
Define la forma esperada de request/response, campos de configuración y garantías de comportamiento.
Ejemplo End-to-End¶
Usa los ejemplos de esta página como plantillas canónicas para implementación y testing.
Casos Límite¶
- Fallbacks ante configuración faltante
- Conflictos de rutas y precedencia
- Matices por runtime
Sandboxing de filesystem¶
Cuando FN_STRICT_FS=1 (habilitado por defecto en produccion), FastFN restringe operaciones de archivo dentro del codigo del handler.
Directorios permitidos:
| Directorio | Acceso |
|---|---|
| Directorio de la funcion | lectura/escritura |
.deps/ / node_modules/ |
lectura |
| Paths de stdlib del runtime | lectura |
/tmp |
lectura/escritura |
/etc/ssl, /etc/pki |
lectura (certificados TLS) |
/usr/share/zoneinfo |
lectura (datos de zona horaria) |
Operaciones bloqueadas (Python): open(), os.open(), Path.open(), os.listdir(), os.scandir(), subprocess.*, os.system(), ctypes.CDLL — bloqueadas para paths fuera de directorios permitidos.
Operaciones bloqueadas (Node): readFileSync, writeFileSync, openSync, readdirSync y equivalentes async — bloqueadas para paths fuera de directorios permitidos.
Archivos de config protegidos: fn.config.json, fn.env.json, fn.test_events.json — los handlers no pueden leer estos archivos ni siquiera dentro del directorio de la funcion.
Toggle: usa FN_STRICT_FS=0 para desactivar sandboxing (solo desarrollo).
Aislamiento de variables de entorno¶
FastFN aisla variables de entorno entre requests concurrentes.
- Python: usa un proxy thread-local de
os.environ(_EnvOverrideProxy). Cada request ve sus propios valores deevent.envmezclados transparentemente. - Node: usa
AsyncLocalStoragepara proxearprocess.env. Cada contexto de request tiene su propio overlay de entorno.
Prefijos bloqueados: los handlers no pueden acceder a variables de entorno con estos prefijos, incluso si estan configuradas en el host:
FN_ADMIN_*FN_CONSOLE_*FN_TRUSTED_*
Estos prefijos estan reservados para uso interno de la plataforma y se eliminan antes de la ejecucion del handler.