Middleware
FoxServer's middlewares are parts of the host, written in X#, that process the request before and after it reaches your controller. They ship with the server and are switched on and configured from the manifest: JWT, request logging, SSE, webhooks, status, OpenAPI, JSON-RPC and MCP, among others.
Two details that save you an afternoon. There is no way to write a middleware from VFP: what your project can do is BeforeEndpoint and AfterEndpoint per controller, and the ApplicationLifecycle for what belongs to the whole project. And CORS is not a middleware: it is resolved before the chain, so it is not switched on or off with the rest.
JWT authentication
It protects the endpoints that do not carry public in their HELP. The client sends Authorization: Bearer <token> and the middleware validates it before your code runs.
In the manifest:
"middleware": {
"auth": {
"enabled": true,
"type": "jwt",
"secret": "<cifrado con middleware-set, nunca en claro>",
"tokenexpirationseconds": 86400,
"loginendpoint": "auth/login"
}
}auth.secret is kept encrypted, and written with foxserver middleware-set. A plaintext secret in the manifest makes the engine answer 500 ("not a valid Base-64 string"). Encrypting it needs the FoxServer.SecurityHelper coclass registered: the installer does it, and on a ZIP install you register bin\SecurityHelper.comhost.dll with the 32-bit regsvr32. The login endpoint contract
This cannot be guessed, and it is where everyone gets stuck the first time: the token is issued by the middleware, not by your code. Your controller validates the credentials and tells the engine whether they are good; the engine signs the JWT.
| The route | Must match middleware.auth.loginendpoint, and carry public in the HELP |
| The body | Cannot be empty |
X-Auth-Status | Exactly "true" or "false". No other value works |
With "true" | The JSON must carry an object in data, which forces "responseformat": "foxserver": the free format has no envelope |
Inside data | Three mandatory fields: id, name and role. If one is missing or empty, there is no token |
Everything else in data | Travels as an extra claim of the token, and is read with req.GetClaim() |
| On that endpoint | this.Ok() and this.Fail() do not work: they build their own response and the header is lost, so the engine answers 406. There you use res.status(n).json(...) |
The token that comes out carries sub (the id), name, role, iat, exp and the extra claims. iat and exp are RFC 7519 NumericDate, whole seconds, and their difference is exactly tokenexpirationseconds.
The login, in full:
PROCEDURE pLogin(req, res) HELP "POST: auth/login public"
LOCAL loRes, loData, lcEmail, lcPassword, loR
loRes = THIS.newObject("status,data,message")
TRY
* The raw body plus ParseJson: more control than req.json when
* the body may not arrive at all.
LOCAL loBody
loBody = THIS.ParseJson(req.GetRawBody())
IF ISNULL(loBody)
loRes.status = "error"
loRes.message = "A JSON body is required"
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.CredentialsAreValid(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
* The three fields are mandatory. Anything else you add to
* loData travels as a claim of the token.
loData = THIS.newObject("id,name,role")
loData.id = ALLTRIM(users.USR_ID)
loData.name = ALLTRIM(users.USR_NAME)
loData.role = ALLTRIM(users.USR_ROLE)
loRes.status = "ok"
loRes.data = loData
loRes.message = "Successful login"
* This is the line that issues the token. The JWT is signed by
* the middleware; nothing is generated here.
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
ENDPROCA protected endpoint: just leave out public
PROCEDURE pMyProfile(req, res) HELP "GET: profile"
LOCAL loRes
* The middleware validated the token before getting here. The user
* comes resolved, and extra claims are read one by one.
loRes = THIS.newObject("id,name,role,company")
loRes.id = req.AuthenticatedUser.id
loRes.name = req.AuthenticatedUser.name
loRes.role = req.AuthenticatedUser.role
loRes.company = req.GetClaim("company", "")
RETURN THIS.Ok(loRes)
ENDPROCAnd if the token is missing, expired or badly signed, the middleware answers on its own:
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"}The WWW-Authenticate header is the RFC 6750 one, and it tells the three cases apart: with no credential presented it carries no error=, which is what §3.1 requires. The body is Problem Details (RFC 9457), like every error the engine produces.
CORS
It answers OPTIONS preflight requests before they reach the controller, and adds the headers to every response. Configured in the manifest:
{
"allowedorigins": "https://miapp.com,https://admin.miapp.com",
"allowedmethods": "GET,POST,PUT,PATCH,DELETE,OPTIONS",
"allowedheaders": "Content-Type,Authorization,X-Requested-With"
}allowedorigins is not authentication: if you expose MCP or JSON-RPC without auth, the engine warns you at start-up. Request logging
It writes every request into daily files, in the folder logging.path names. The file name is made of the prefix, the type and the date:
logging.path\
api_Request_20260915.log requests served
api_Error_20260915.log errorsThe host log is a different one: logs\FoxServer_<date>.log, at the root of the installation, one per day, split at 10 MB and purged after 30. The level is set with FOXSERVER_LOG_LEVEL and the retention with FOXSERVER_LOG_RETENTION_DAYS.
What you can write: the controller hooks
There is no API for writing a middleware from VFP, but every controller has two hooks that the engine calls around each of its endpoints. They are the place to open your tables and to close them.
DEFINE CLASS ProductsController AS ApiController OLEPUBLIC
* Before EACH endpoint of this controller.
* Returning .F. cuts it off: the endpoint never runs.
FUNCTION BeforeEndpoint(req, res)
SET DELETED ON
SET EXCLUSIVE OFF
OPEN DATABASE (THIS.cPath + "data\shop.dbc") SHARED
RETURN .T.
ENDFUNC
* After EACH endpoint, whether it went well or not.
PROCEDURE AfterEndpoint(req, res)
CLOSE DATABASES ALL
ENDPROC
ENDDEFINEApplicationLifecycle, which runs when the server starts and stops. Next: Licensing →