Routing and Request Flow
This page describes how HTTP requests travel from the browser to the server and back, including URL routing, the two client-side communication channels (HTMX-managed and JavaScript fetch), and timeout/error handling.
Overview
Kittox is a single-page application. The browser loads one full HTML page at startup (_Page.html template), and all subsequent interactions happen via partial HTML exchanges — the server returns HTML fragments that replace portions of the DOM.
There are two mechanisms for these exchanges:
| Channel | Used by | Response type | Timeout managed by |
|---|---|---|---|
HTMX (hx-get, hx-post) | Pagination, filters, navigation, view loading | HTML fragment swapped into a target element | htmx.config.timeout |
JavaScript fetch() | Forms, lookups, tools, charts, calendar | HTML overlay appended to <body>, or JSON, or binary download | AbortController via kxFetchWithTimeout wrapper |
Both channels share the same URL namespace (kx/...) and the same server-side handler; the difference is entirely on the client side.
URL Structure
Every Kittox request follows this pattern:
{AppPath}/kx/{resource}/{ViewName}[/{operation}][?params]| Segment | Example | Description |
|---|---|---|
AppPath | /taskitto | Application base path, set in Config.yaml → Server/Port and executable name |
kx | Fixed namespace prefix for all Kittox API calls | |
resource | view, login | Resource type |
ViewName | ActivityInput | Name of the YAML view definition |
operation | data, form, save, delete, tool/ExportExcel | Action to perform on the view |
Route Table
Kittox uses two routing layers, tried in order:
- Attribute-based routing (
TKXRoutingRoute) — handlers decorated with[TKXPath]attributes, discovered via Delphi RTTI at startup. This is where everykx/…endpoint now lives. See Attribute-Based Routing for details. - Legacy routing (
TKWebApplication.DoHandleRequest) — reduced to a single job: serving the application root/(the login/home page). All formerif/elsedispatch has been removed.
The attribute-based layer is checked first. If no match is found, the request falls through to the legacy layer (Home, else a 404). This design, inspired by MARS / WiRL, lets application code add or replace URL handlers without touching the framework.
Both layers run their dispatch through a shared request filter chain: JWT session hydration, the authentication gate and error handling are defined once (as filters) and applied uniformly, instead of being coded into each handler. See Request filters.
Attribute-routed handlers
| URL Pattern | HTTP | Handler class | Module |
|---|---|---|---|
kx/login | POST | TKXAuthHandlerBase | Core |
kx/resetpassword | POST | TKXAuthHandlerBase | Core |
kx/changepassword | POST | TKXAuthHandlerBase | Core |
kx/logout | GET/POST | TKXAuthHandlerBase | Core |
kx/reloadhome | POST | TKXAuthHandlerBase | Core |
kx/setlang/{Lang} | POST | TKXSetLangHandler | Core |
kx/view/{V} | GET/POST | TKXViewHandlerBase | Core |
kx/view/{V}/data | GET/POST | TKXViewHandlerBase | Core |
kx/view/{V}/form | GET | TKXViewHandlerBase | Core |
kx/view/{V}/save | POST | TKXViewHandlerBase | Core |
kx/view/{V}/save-cache | POST | TKXViewHandlerBase | Core |
kx/view/{V}/delete | POST | TKXViewHandlerBase | Core |
kx/view/{V}/enter-edit | POST | TKXViewHandlerBase | Core |
kx/view/{V}/form-close | POST | TKXViewHandlerBase | Core |
kx/view/{V}/lookup | GET | TKXViewHandlerBase | Core |
kx/view/{V}/wizard-finish | POST | TKXViewHandlerBase | Core |
kx/view/{V}/detail/{I}/data | GET/POST | TKXViewHandlerBase | Core |
kx/view/{V}/detail/{I}/save | POST | TKXViewHandlerBase | Core |
kx/view/{V}/detail/{I}/delete | POST | TKXViewHandlerBase | Core |
kx/view/{V}/tool/{T} | POST | TKXViewHandlerBase | Core |
kx/view/{V}/upload/{F} | POST | TKXViewHandlerBase | Core |
kx/view/{V}/notify/{F} | POST | TKXViewHandlerBase | Core |
kx/view/{V}/blob/{F} | GET | TKXViewHandlerBase | Core |
kx/view/{V}/chart-data | GET | TKXChartHandler | Enterprise |
kx/view/{V}/calendar-data | GET | TKXCalendarHandler | Enterprise |
kx/view/{V}/map-data | GET | TKXMapHandler | Enterprise |
Every URL endpoint is now attribute-routed. Two core handlers cover them: the authentication family (TKXAuthHandlerBase, in Kitto.Web.Handler.Auth.pas) and the whole view domain (TKXViewHandlerBase, in Kitto.Web.Handler.View.pas) — view rendering, list/grid data, the form dialog, save / save-cache, delete, the enter-edit / form-close transitions, plus lookup, wizard-finish, master-detail data/save/delete, tool execution, file upload, field-change notification and blob/image serving. Both are self-contained virtual handlers an application can subclass to override a single step (or replace an endpoint) without forking the framework — see RegisterOverride. The auth endpoints are additionally marked [TKXAnonymous]. The three Enterprise data endpoints (chart, calendar, map) use their own handlers. The only URL still served by the legacy DoHandleRequest is the application root / (the login/home page, rendered by Home()).
Route ordering
Attribute-routed handlers are automatically sorted by specificity (more literal URL segments = higher priority), and token count must match exactly — so /kx/view/{V} (bare view) never shadows /kx/view/{V}/data, and /kx/view/{V}/detail/{I}/save (6 tokens) is never caught by /kx/view/{V}/save (4 tokens).
HTMX-Managed Requests
The majority of user interactions — pagination, sorting, filtering, navigating between views — are handled by HTMX attributes declared directly in the HTML generated by the server:
<!-- Pagination: generated by TKXListPanelController -->
<button hx-get="kx/view/Activities/data?start=50&limit=50"
hx-target="#kx-list-body-Activities"
hx-swap="innerHTML"
hx-include="[name^='f_']"
hx-indicator="#kx-loading">
Next
</button>When the user clicks this button, HTMX:
- Sends a GET request to
kx/view/Activities/data - Shows the
#kx-loadingspinner overlay - Receives an HTML fragment from the server
- Swaps it into the
#kx-list-body-Activitiestarget element - Hides the loading spinner
The server responds with HTML fragments only — never a full page. HTMX handles the DOM replacement transparently. Common HTMX attributes used in Kittox:
| Attribute | Purpose |
|---|---|
hx-get / hx-post | URL to call |
hx-target | DOM element to update with the response |
hx-swap | How to insert the response (innerHTML, outerHTML) |
hx-include | Additional form fields to include in the request |
hx-indicator | Element to show/hide as loading indicator |
hx-vals | Extra values to send as JSON |
hx-headers | Extra HTTP headers (Kittox sends X-KittoX: true) |
JavaScript fetch() Requests
Some operations cannot use the HTMX swap model and require explicit JavaScript. These use the fetch() API (wrapped by kxFetchWithTimeout for timeout handling). The reasons fall into five categories:
1. Modal overlays appended to <body>
Affected: Open form (/form), open lookup (/lookup)
The server returns a complete dialog overlay (backdrop + dialog HTML). This must be appended to document.body as a new element, not swapped into an existing target container. HTMX's model requires a pre-existing target element, which doesn't exist for dynamically created modals.
// kxgrid.js — openForm
kxFetchWithTimeout(url, { headers: { 'X-KittoX': 'true' } })
.then(function(r) { return r.text(); })
.then(function(html) {
var div = document.createElement('div');
div.innerHTML = html;
if (div.firstElementChild)
document.body.appendChild(div.firstElementChild); // append, not swap
});2. JSON responses (not HTML)
Affected: Chart data (/chart-data), calendar events (/calendar-data)
These endpoints return JSON, not HTML. The client passes the data to Chart.js or the calendar widget programmatically. HTMX is designed for HTML-in, HTML-out exchanges.
// kxgrid.js — kxChart.refresh
kxFetchWithTimeout('kx/view/' + viewName + '/chart-data', { ... })
.then(function(r) { return r.json(); })
.then(function(data) {
chart.data.labels = data.labels;
chart.data.datasets[0].data = data.data;
chart.update(); // Chart.js animated update
});3. Binary downloads
Affected: Tool execution (/tool/{T}) for download-type tools (Excel export, PDF, etc.)
The client must inspect the Content-Disposition response header to determine if the response is a file download, then create a temporary blob URL and trigger the browser's save dialog. HTMX has no mechanism for binary download handling or response header inspection.
4. Inline script execution
Affected: Save (/save)
The save response may contain HTML with <script> tags (e.g., close-form commands, refresh triggers). HTMX's innerHTML swap does not execute inline scripts. The save handler manually extracts and executes them:
div.querySelectorAll('script').forEach(function(script) {
var s = document.createElement('script');
s.textContent = script.textContent;
document.head.appendChild(s);
document.head.removeChild(s);
});5. Post-response JavaScript logic
Affected: Lookup (/lookup), calendar delete (/delete)
After inserting the response HTML, these operations need additional JavaScript steps: attaching event handlers (e.g., double-click for quick lookup selection), calling htmx.process() to activate HTMX attributes in the new content, updating toolbar button state, or triggering a calendar refresh. These multi-step workflows go beyond what a simple HTMX swap can express.
Summary: Which Channel Handles What
| Operation | Channel | Why |
|---|---|---|
| Load list rows / pagination | HTMX | Standard swap into #kx-list-body-{view} |
| Apply filters | HTMX | Standard swap with hx-include for filter values |
| Sort columns | HTMX | Standard swap with sort params |
| Navigate views / menu | HTMX | Standard swap into content area |
| Open edit/add form | fetch | Modal overlay appended to body |
| Save form | fetch | Response contains executable scripts |
| Delete record | HTMX | Standard swap (refreshes grid) |
| Open lookup dialog | fetch | Modal overlay + post-insert JS logic |
| Execute tool | fetch | May return binary download or needs header inspection |
| Load chart data | fetch | JSON response for Chart.js |
| Load calendar data | fetch | JSON response for calendar widget |
| Calendar delete | fetch | Post-response state update + calendar refresh |
| Login / logout | HTMX | Standard form submission |
| Login submitted before HTMX is ready | native POST | Safety net only: the form carries method/action, and the server answers 303 to the app root instead of a fragment — see How the form is submitted |
Timeout and Error Handling
Both communication channels are protected by a configurable timeout. The timeout value is set in Config.yaml:
# Timeout in milliseconds (default: 100000 = 100 seconds)
AjaxTimeout: 100000HTMX timeout
The _Page.html template configures HTMX at page load:
htmx.config.timeout = {{ajaxTimeout}}; // injected from Config.yamlWhen an HTMX request exceeds the timeout, the htmx:timeout event fires. Kittox listens for this event (and also htmx:sendError for network failures) and shows an error dialog:
document.body.addEventListener("htmx:timeout", function(evt) {
var rc = evt.detail.requestConfig;
kxGrid.showConfirm('Error', 'Server is not responding',
'Retry', // re-issues the same HTMX call
'Reset', // reloads the page
function() { htmx.ajax(rc.verb, rc.path, {target: rc.target, ...}); },
function() { window.location.reload(); }
);
});fetch() timeout
All fetch() calls go through the kxFetchWithTimeout wrapper, which uses the standard AbortController API with the same timeout value:
function kxFetchWithTimeout(url, options) {
var timeout = htmx.config.timeout || 100000;
var controller = new AbortController();
var timeoutId = setTimeout(function() { controller.abort(); }, timeout);
return fetch(url, Object.assign({}, options, { signal: controller.signal }))
.then(function(response) {
clearTimeout(timeoutId);
return response;
})
.catch(function(err) {
clearTimeout(timeoutId);
if (err.name === 'AbortError') {
// Show Retry / Reset dialog
// Retry re-calls kxFetchWithTimeout with the same arguments
// and feeds the result back into the original promise chain
}
throw err;
});
}The Retry button re-issues the exact same request transparently — the original .then() chain continues as if the first attempt had succeeded. The Reset button reloads the page entirely.
Error dialog
Both channels show the same modal error dialog (kxGrid.showConfirm in error mode):
| Button | Action |
|---|---|
| Retry | Re-issues the exact same request (HTMX or fetch). When the caller cannot re-issue it, falls back to the same reload as Reset |
| Reset | Reloads the page through kxReloadHome() to return to the initial screen, keeping the session |
Why the reload goes through kxReloadHome()
Reaching the application root drops the current credential on purpose, so that a fresh page load starts from the login. A plain window.location.reload() on the home is indistinguishable from that and would sign the user out — wrong here, since the session is still valid and only the last request failed.
kxReloadHome() therefore POSTs to kx/reloadhome first, which raises a one-shot session flag telling the root that this reload was issued by the application, and only then reloads. The flag is raised only for an authenticated session, grants nothing by itself (it merely stops an already valid credential from being dropped), and is cleared as soon as the page is served — so a user pressing F5 still signs out, JWT cookie included. If the server is unreachable the POST fails and the reload lands on the login, which is the honest outcome.
The fatal error dialog is deliberately different: there the session is being destroyed, so it reloads straight to the login.
Session Lost (Server Restart)
When the server is restarted, all in-memory sessions are lost. If the browser still has a session cookie from the previous server instance, the server detects this situation and shows a fatal error dialog.
How it works
- The client sends a request with the old session cookie
TKWebEngine.EnsureSessioncallsFindOrCreateSession— an atomic operation that finds or creates a session under a single lock- If no existing session matches the cookie, a new session is created and flagged as
IsSessionLost = True - The authorization filter (
TKXAuthorizationFilter) checks the flag before dispatch:- API requests (
kx/...): raises an exception"Session lost or expired, please restart!"→ the error-handler filter shows a fatal error dialog - Home/page requests (F5, page load) and
[TKXAnonymous]endpoints: pass through normally — the login page is displayed
- API requests (
Atomic session creation
When the user presses F5, the browser sends multiple parallel requests (the page itself plus static resources like CSS and images). Without protection, each request could create a separate session, leading to race conditions and crashes.
TKWebSessions.FindOrCreateSession solves this by performing the find-or-create under a single MonitorEnter lock:
- Lock the session list
- Look up the session by the identifier the request presents (session-id cookie, or the
sidclaim of the JWT cookie) - If found, return it; if not, create and add a new session atomically
- Unlock
This guarantees that all parallel requests carrying the same identifier are routed to the same session, even when they arrive simultaneously after a restart or F5. A request that presents no identifier gets a new, anonymous session — it is never attached to an existing one.
Changed in 4.0.15 — no fallback on the client address
Until 4.0.15, a request without an identifier fell back to the first session whose client address matched. Every browser's first contact carries no cookie, so it was attached to whatever session already existed for that address — often an authenticated one. Behind a reverse proxy (nginx, IIS ARR, HAProxy) the client address is the proxy's, identical for all users, so any new visitor could land in another user's authenticated session.
The fallback has been removed, together with the two lookup helpers that implemented it (FindSessionByClientAddress, FindSession). The client address is still recorded on the session, for the monitor and the log, but it is no longer a lookup key. Application code that called either helper must key on the session id instead.
Page refresh (F5) handling
When the user presses F5 on an active session, EnsureSession detects this via IsPageRefresh (a non-Ajax request to the application root). In this case:
- The existing session's authentication state is preserved (user name, auth data, language)
- The old session is removed
- A new session is created with the same session ID (so that in-flight parallel requests still find it)
- The preserved auth state is transferred to the new session
This allows F5 to act as a session reset without losing the login, and without creating duplicate sessions from the parallel resource requests that accompany the page reload.
Fatal error dialog
When any fatal error occurs (including session lost), the error dialog behaves differently from non-fatal errors:
| Fatal error | Non-fatal error | |
|---|---|---|
| OK button | window.location.reload() — returns to login | Closes the dialog |
| Overlay click | Disabled (must click OK) | Closes the dialog |
This is correct for all fatal errors because SignalFatalError destroys the session after the response — the user must reload to get a new session.
No client-side changes needed
Both communication channels handle the error dialog HTML natively:
- HTMX: the
HX-Retarget: bodyandHX-Reswap: beforeendresponse headers tell HTMX to append the dialog overlay to the body - fetch: callers (
openForm,save, tools, etc.) already read the response as HTML text and append it todocument.body
Request Processing on the Server
Browser request
--> TKWebEngine.HandleRequest()
|-- Session lookup/creation
| `-- GetSessionIdFromRequest reads kx_<appname> session cookie
| (legacy auth) or decodes the 'sid' claim from kx_token
| (Auth: JWT, no signature check yet — only routing)
|-- TKXRoutingRoute (attribute layer, tried first)
| |-- Match URL -> resource/method by [TKXPath] + specificity
| |-- ActivateInstance (sets TKAuthenticator.Current, etc.)
| |-- Request filter chain (shared with the legacy pipeline):
| | |-- TKXJWTAuthFilter -> TKAuthenticator.AuthorizeRequest
| | | (TKJWTAuthenticator validates the JWT signature/claims,
| | | hydrates the session, slides the cookie; no-op otherwise)
| | |-- TKXAuthorizationFilter -> session-lost + unauthenticated 404 gate
| | `-- TKXErrorHandlerFilter wraps dispatch (exception -> modal dialog)
| |-- Handler method executes (e.g. TKXViewHandlerBase.HandleData):
| | 1. Finds the View by name in the metadata catalog
| | 2. Creates/loads data store from the database
| | 3. Creates/invokes controller (TKXListPanelController, …)
| | 4. Controller renders HTML fragment (or JSON, or binary stream)
| `-- TKWebResponse sends the result
`-- TKWebApplication.DoHandleRequest() (fallback: only the '/' home page,
run through the same filter chain; unmatched -> 404)
<-- HTTP response (HTML fragment / JSON / binary)The AuthorizeRequest hook lets each authenticator inject its own per-request credential check without forcing the framework runtime layer to depend on its third-party libraries. Auth: JWT uses it to validate the signed token; future external authenticators (OIDC, SAML) will plug in the same way.
Key parameter encoding
Record keys are encoded as URL query strings: FieldName1=Value1&FieldName2=Value2.
Example: key=ID%3D123%26Code%3DABC decodes to ID=123&Code=ABC.
Only fields marked as IsKey: True in the model are accepted, to prevent injection attacks.
The X-Kittox header
All Kittox requests (both HTMX and fetch) include the X-KittoX: true HTTP header. This allows the server to distinguish Kittox API calls from regular page loads and static resource requests. In the _Page.html template, HTMX is configured globally:
<body hx-headers='{"X-KittoX": "true"}'>JavaScript fetch() calls include it explicitly in the headers option.
This header also feeds the navigation guard: every kx/… endpoint returns an HTML fragment for the SPA, never a standalone page, so a top-level browser navigation to such a URL (address bar, opened link, window.open) is bounced with a 302 redirect to the app root (/), showing the login page (anonymous) or the home page (authenticated) instead of a bare fragment. Endpoints legitimately opened by direct navigation (the blob/{Field} download/preview) opt out with [TKXNavigable]; auth endpoints and the home page are exempt too.
To tell a real top-level navigation from a programmatic request, the guard uses the browser's Sec-Fetch-Mode header (Fetch Metadata, sent by all current browsers): only navigate is bounced, while cors / same-origin / no-cors — a fetch/XHR or a subresource load such as an <img>/<iframe> src — is let through. This matters for data loaded by third-party widgets whose internal fetch cannot add the X-KittoX header (for example the calendar loading its events from calendar-data): such requests reach the endpoint normally and are authenticated by the session cookie, instead of being redirected to the root (which would log the user out). When Sec-Fetch-Mode is absent (very old or non-browser clients) the guard falls back to the X-KittoX heuristic. See Attribute-Based Routing — Navigation guard.
