HTTP Processing Pipeline
The HTTP processing pipeline in io-oidis-localhost handles all incoming HTTP requests through a chain of components, from raw socket to final response.
Request Flow
Client Request
|
v
HttpServer Creates HTTP/HTTPS server, receives raw request
|
|--- TRACE? ----------> 405 Method Not Allowed
|--- OPTIONS /xorigin? -> CORS preflight response
|--- POST/PUT/PATCH? --> Buffer request body
|
v
resolveHttpRequest Wraps request into connector + protocol
|
|--- /xorigin protocol? -> LiveContentResolver (LCP)
|--- / with index.html? -> Direct file stream
|
v
HttpResolver.ResolveRequest(args)
|
v
@Resolver match URL matched against registered resolver patterns
|
v
BaseHttpResolver Common resolver lifecycle (args, access, resolve)
|
v
Resolver.Process() Specific resolver logic
|
v
Response API Formats and sends HTTP response
|
v
HttpServer.sendHttpResponse Writes status, headers, compressed body
Components
HttpServer
Manages the HTTP/HTTPS server lifecycle. Key responsibilities:
- Server creation on configured ports (HTTP and HTTPS)
- TLS/SSL via
CertsManager— loads certificates from disk, external provider, or ACME challenge - WebSocket upgrade — creates
WebSocketServerinstances for real-time communication - Request buffering — accumulates POST/PUT/PATCH body data before dispatching
- Origin validation —
isRequestAllowed()checks request origin against configured domain, IPs, and allowed origins - CSP headers — Content Security Policy header generation from configuration
- Response compression — gzip/deflate encoding via
sendHttpResponse() - Metrics — tracks inbound/outbound sizes and request timing via
Metrics
HttpManager
Wraps the incoming request into an HttpRequestParser instance. Extends the services-layer HttpManager with localhost-specific request parsing.
HttpRequestParser
Parses raw HTTP requests with back-end awareness:
- URL and scheme — extracted from Node.js
IncomingMessage - Client IP — resolved from
x-forwarded-for,connection.remoteAddress, orsocket.remoteAddress - Server IP — detected from OS network interfaces
- Headers — normalized to lowercase keys via
getRawHeaders() - User agent — synthesized Navigator object for back-end context
- Query parameters — parsed via
getUrlArgs() - ETag support — via
getEtags()
HttpResolver
Orchestrates the resolver chain. Extends the services-layer HttpResolver with:
CreateRequest()— createsHttpRequestParserinstancesgetHttpManagerClass()/getRequestParserClass()— factory methods returning localhost-specific classesFetchHelper()— outbound HTTP client with proxy detection and tunneling supportResolveRequest(args)— dispatches to the matching@Resolver-decorated class
Resolver Chain
Resolvers are registered via the @Resolver decorator and discovered through ForceCompile.ts imports. When a request arrives, its URL is matched against resolver patterns in registration order.
Registered Resolvers
| Resolver | Route Pattern | Purpose |
|---|---|---|
FrontEndLoader | / | Serves the application entry point (front-end bootstrap) |
LoaderResolver | /resource/javascript/loader.min.js | Serves the client-side loader script with injected config |
RESTResolver | /api/{section}/... | REST API endpoints registered via @Route |
ConnectorResponse | /wuihost/CloseClient, /{clientId}/response.jsonp, connector.config.jsonp | LCP connector lifecycle and configuration |
XOriginInterface | /wuihost/, /xorigin/, /xorigin/Request | Cross-origin request interface for LCP |
XOriginServices | /resource/javascript/xservices.js | Cross-origin services script |
LiveContentResolver | /liveContent/ | Server-sent events for real-time content updates |
AcmeResolver | /.well-known/acme-challenge/{token} | ACME certificate challenge responses |
CSPReportResolver | /cspReport | Content Security Policy violation report endpoint |
MapFilesPatchResolver | /**/*.min.js.map, /**/*.min.css.map | Source map file interception |
FileRequestResolver | /favicon.ico, /**/*.js, /**/*.css, /**/*.html, ... | Static file serving for all known file extensions |
Resolver Registration
Resolvers use the @Resolver decorator from @io-oidis-commons:
import { Resolver } from "@io-oidis-commons/Io/Oidis/Commons/Primitives/Decorators.js";
@Resolver("/api/{section}")
export class MyResolver extends BaseHttpResolver {
// ...
}
The decorator accepts a single route string or an array of route patterns. Route parameters use {paramName} syntax and are passed to argsHandler() as GET parameters.
REST Route Priority
REST routes registered via @Route are resolved through RESTResolver.getRoutes(), which:
- Collects all routes from
globalThis.oidisAPIRoutes(populated by@Routedecorators) - Orders by namespace hierarchy — routes from higher-level loaders override lower-level ones with the same path and HTTP method
- Sorts by
priorityfield (ascending) for final ordering - Caches the sorted result for subsequent lookups
Resolver Lifecycle
Each resolver follows this lifecycle in BaseHttpResolver:
Process()— entry point, triggers the resolver chainargsHandler()— extracts GET/POST parameters from the requestaccessValidator()— checks authentication and authorizationbrowserValidator()— validates user agent (rejects search bots for API calls)resolver()— executes the core resolution logicafterLoad()— post-processing hook (default: serves static page content)
Content Security Policy
Default Policy
The default policy lives in io-oidis-localhost/bin/project/configs/commons.jsonp (domain.csp.policies.localhostConfig.default). Web workers, service workers and the PWA manifest from the same origin are allowed by their own directives (worker-src 'self', manifest-src 'self'); without them workers fall back to script-src, which has no 'self', and the manifest to default-src 'none'. A solution that needs workers from blob: URLs sets "worker-src": ["self", "blob:"] in its own configuration — configured values replace the default, they are not merged.
Nonce of Server-Rendered Pages
Each HTTP request without a body gets its own copy of the port configuration from HttpServer; resolveRequest() stores the request's nonce in its Protocol.nonce. getNonce() in the resolver and the CSP header of the response therefore carry the same value, also when other requests are resolved while the resolver awaits data.
A server page built with StaticPageContentManager, whose head, body and nonce are static and shared by the whole process, has to set StaticPageContentManager.Nonce(this.getNonce()) before it appends scripts, start from StaticPageContentManager.Clear(true) and be rendered and sent synchronously, as FrontEndLoader.Process() does.
The localhost error pages (/ServerError/Browser 406, /ServerError/Exception/* 500, /ServerError/Http/Moved 301, /ServerError/Http/Forbidden 403, /ServerError/Http/NotFound 404) call StaticPageContentManager.Clear(true) before rendering, because the GUI error pages clear the page without force and keep the head collected so far. Without it an error page re-sent the head of the page rendered before — typically the SPA shell with the inline Google Analytics loader carrying the shell's nonce — and the browser blocked the loader (Sentry: Blocked 'script' from 'inline:' with the sample window.dataLayer = window.dataLayer || [, e.g. on the 406 page for an unknown browser).
CSP Reporting & Sentry Security Reports
HttpServer.getCSPHeaders() builds the Content-Security-Policy header per request from domain.csp configuration (nonce-based, pattern-matched per URL). Two things are added automatically on top of the configured directives:
- Report routing — responses carry both
Report-To(Reporting API v0, legacy) andReporting-Endpoints(v1, modern Chromium) headers, plus thereport-to/report-uridirectives, all pointing at thecsp-endpointgroup served byCSPReportResolver(/cspReport). Emitting both header versions lets new and older browsers resolve the endpoint. 'report-sample'— appended toscript-src*andstyle-src*directives so violation reports include a snippet of the offending inline script/style. Without it, Sentry issues are just "blocked inline" with no way to identify the source. Directives set to'none'are skipped: per CSP spec'none'must stand alone, and mixing another token in makes browsers ignore it — which would change what the policy enforces.'report-sample'only enriches reports; it never changes what the policy allows.
Report forwarding to Sentry
CSPReportResolver receives violation reports at /cspReport and, when sentry.dsn is set and sentry.cspReportTunnel is enabled, forwards them to Sentry's Security Header endpoint (.../api/{project}/security/) via SentryTunnelResolver.Proxy(data, true). Browsers send one of two formats — both are handled; historically only the Reporting API format was, so Firefox/Safari reports were silently dropped:
| Format | Sent by | Payload shape |
|---|---|---|
Reporting API (report-to) | Chromium | array of { type: "csp-violation", body: { documentURL, blockedURL, effectiveDirective, sample, … } } (camelCase) — normalized to the deprecated csp-report serialization Sentry accepts |
Legacy (report-uri) | Firefox, Safari | single { "csp-report": { "document-uri", "blocked-uri", … } } (hyphenated) — already in Sentry's shape, forwarded as-is |
Do not log unrecognized payloads at Warning. Non-CSP payloads that reach /cspReport (e.g. from version-mismatched clients) are logged at Debug, not Warning. SentryAdapter forwards WARNING (and ERROR) to Sentry as captureMessage, so warning on unknown payloads would flood Sentry with "unrecognized report" noise. See LogProcessor.
Response API
Responses are handled through a type hierarchy. See Response API for detailed documentation.
| Response Type | Purpose |
|---|---|
WebResponse | Base HTTP response with status, headers, body |
RESTResponse | REST API responses with structured error handling |
StreamResponse | Streaming responses (e.g., file downloads, Sentry tunnel) |
LiveContentResponse | Server-sent events for real-time updates |
CallbackResponse | Callback-based async responses |
InBodyStatusResponse | Status code embedded in response body |
See also: Response API | Localhost Overview | Routing