Middleware
Los middlewares de FoxServer son piezas del host, escritas en X#, que procesan la petición antes y después de que llegue a tu controlador. Vienen con el servidor y se encienden y se configuran desde el manifiesto: JWT, registro de peticiones, SSE, webhooks, estado, OpenAPI, JSON-RPC y MCP, entre otros.
Dos matices que ahorran una tarde. No hay forma de escribir un middleware desde VFP: lo que tu proyecto puede hacer es BeforeEndpoint y AfterEndpoint por controlador, y el ApplicationLifecycle para lo que es de todo el proyecto. Y CORS no es un middleware: se resuelve antes de la cadena, así que no se enciende ni se apaga con los demás.
Autenticación JWT
Protege los endpoints que no llevan public en su HELP. El cliente manda Authorization: Bearer <token> y el middleware lo valida antes de que tu código corra.
En el manifiesto:
"middleware": {
"auth": {
"enabled": true,
"type": "jwt",
"secret": "<cifrado con middleware-set, nunca en claro>",
"tokenexpirationseconds": 86400,
"loginendpoint": "auth/login"
}
}auth.secret se guarda cifrado, y se escribe con foxserver middleware-set. Un secreto en claro en el manifiesto hace que el motor responda 500 («not a valid Base-64 string»). Cifrarlo necesita la coclase FoxServer.SecurityHelper registrada: el instalador lo hace, y en una instalación desde ZIP se registra bin\SecurityHelper.comhost.dll con el regsvr32 de 32 bits. El contrato del endpoint de login
Esto no se adivina, y es donde se atasca todo el mundo la primera vez: el token lo emite el middleware, no tu código. Tu controlador valida las credenciales y le dice al motor si valen; el motor firma el JWT.
| La ruta | Tiene que coincidir con middleware.auth.loginendpoint, y llevar public en el HELP |
| El cuerpo | No puede estar vacío |
X-Auth-Status | Exactamente "true" o "false". Ningún otro valor vale |
Con "true" | El JSON tiene que traer un objeto en data, lo que obliga a "responseformat": "foxserver": con el formato libre no hay envoltura |
Dentro de data | Tres campos obligatorios: id, name y role. Si falta uno o viene vacío, no hay token |
Lo demás de data | Viaja como claim adicional del token, y se lee con req.GetClaim() |
| En ese endpoint | No valen this.Ok() ni this.Fail(): construyen su propia respuesta y se pierde la cabecera, así que el motor contesta 406. Ahí se usa res.status(n).json(...) |
El token que sale lleva sub (el id), name, role, iat, exp y los claims adicionales. iat y exp son NumericDate del RFC 7519, segundos enteros, y su diferencia es exactamente tokenexpirationseconds.
El login, entero:
PROCEDURE pLogin(req, res) HELP "POST: auth/login public"
LOCAL loRes, loData, lcEmail, lcPassword, loR
loRes = THIS.newObject("status,data,message")
TRY
* El cuerpo crudo y ParseJson: mas control que req.json cuando
* el cuerpo puede no venir.
LOCAL loBody
loBody = THIS.ParseJson(req.GetRawBody())
IF ISNULL(loBody)
loRes.status = "error"
loRes.message = "Body JSON requerido"
res.AddHeader("X-Auth-Status", "false")
loR = res.status(400)
loR.json(THIS.ToJson(loRes))
EXIT
ENDIF
lcEmail = LOWER(ALLTRIM(NVL(loBody.email, "")))
lcPassword = ALLTRIM(NVL(loBody.password, ""))
IF !THIS.CredencialesValidas(lcEmail, lcPassword)
loRes.status = "error"
loRes.message = "Invalid credentials"
res.AddHeader("X-Auth-Status", "false")
loR = res.status(401)
loR.json(THIS.ToJson(loRes))
EXIT
ENDIF
* Los tres campos son obligatorios. Cualquier otro que anadas a
* loData viaja como claim del token.
loData = THIS.newObject("id,name,role")
loData.id = ALLTRIM(usuarios.USR_ID)
loData.name = ALLTRIM(usuarios.USR_NOMBRE)
loData.role = ALLTRIM(usuarios.USR_ROL)
loRes.status = "ok"
loRes.data = loData
loRes.message = "Successful login"
* Esta es la linea que emite el token. El JWT lo firma el
* middleware; aqui no se genera nada.
res.AddHeader("X-Auth-Status", "true")
loR = res.status(200)
loR.json(THIS.ToJson(loRes))
CATCH TO loEx
loRes.status = "error"
loRes.message = loEx.Message
THIS.LogFromException(loEx)
res.AddHeader("X-Auth-Status", "false")
loR = res.status(500)
loR.json(THIS.ToJson(loRes))
ENDTRY
ENDPROCUn endpoint protegido: basta con no poner public
PROCEDURE pMiPerfil(req, res) HELP "GET: profile"
LOCAL loRes
* El middleware ya validó el token antes de entrar aquí. El usuario
* viene resuelto, y los claims extra se leen uno a uno.
loRes = THIS.newObject("id,name,role,empresa")
loRes.id = req.AuthenticatedUser.id
loRes.name = req.AuthenticatedUser.name
loRes.role = req.AuthenticatedUser.role
loRes.empresa = req.GetClaim("empresa", "")
RETURN THIS.Ok(loRes)
ENDPROCY si el token falta, está caducado o mal firmado, el middleware contesta solo:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="foxserver", error="invalid_token",
error_description="The access token expired"
Content-Type: application/problem+json; charset=utf-8
{"type":"about:blank","title":"Unauthorized","status":401,
"detail":"Invalid token","instance":"/profile","code":"unauthorized"}La cabecera WWW-Authenticate es la del RFC 6750, y distingue los tres casos: sin credencial no lleva error=, que es lo que manda el §3.1. El cuerpo es Problem Details (RFC 9457), como todos los errores del motor.
CORS
Responde a las peticiones preflight OPTIONS sin que lleguen al controlador, y pone las cabeceras en todas las respuestas. Se configura en el manifiesto:
{
"allowedorigins": "https://miapp.com,https://admin.miapp.com",
"allowedmethods": "GET,POST,PUT,PATCH,DELETE,OPTIONS",
"allowedheaders": "Content-Type,Authorization,X-Requested-With"
}allowedorigins no es autenticación: si expones MCP o JSON-RPC sin auth, el motor te lo avisa al arrancar. Registro de peticiones
Escribe cada petición en ficheros diarios, en la carpeta que diga logging.path. El nombre lo componen el prefijo, el tipo y la fecha:
logging.path\
api_Request_20260915.log peticiones servidas
api_Error_20260915.log erroresEl log del host es otro: logs\FoxServer_<fecha>.log, en la raíz de la instalación, uno por día, partido a 10 MB y purgado a los 30. El nivel se ajusta con FOXSERVER_LOG_LEVEL y la retención con FOXSERVER_LOG_RETENTION_DAYS.
Lo que sí puedes escribir tú: los ganchos del controlador
No hay API para escribir un middleware desde VFP, pero cada controlador tiene dos ganchos que el motor llama alrededor de cada endpoint suyo. Son el sitio de abrir las tablas y de cerrarlas.
DEFINE CLASS ProductosController AS ApiController OLEPUBLIC
* Antes de CADA endpoint de este controlador.
* Devolver .F. corta: el endpoint no llega a ejecutarse.
FUNCTION BeforeEndpoint(req, res)
SET DELETED ON
SET EXCLUSIVE OFF
OPEN DATABASE (THIS.cPath + "data\tienda.dbc") SHARED
RETURN .T.
ENDFUNC
* Despues de CADA endpoint, haya ido bien o mal.
PROCEDURE AfterEndpoint(req, res)
CLOSE DATABASES ALL
ENDPROC
ENDDEFINEApplicationLifecycle, que corre al arrancar y al parar el servidor. Siguiente: Licencias →