Skip to main content

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 WebSocketServer instances 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, or socket.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() — creates HttpRequestParser instances
  • getHttpManagerClass() / getRequestParserClass() — factory methods returning localhost-specific classes
  • FetchHelper() — outbound HTTP client with proxy detection and tunneling support
  • ResolveRequest(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​

ResolverRoute PatternPurpose
FrontEndLoader/Serves the application entry point (front-end bootstrap)
LoaderResolver/resource/javascript/loader.min.jsServes 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.jsonpLCP connector lifecycle and configuration
XOriginInterface/wuihost/, /xorigin/, /xorigin/RequestCross-origin request interface for LCP
XOriginServices/resource/javascript/xservices.jsCross-origin services script
LiveContentResolver/liveContent/Server-sent events for real-time content updates
AcmeResolver/.well-known/acme-challenge/{token}ACME certificate challenge responses
CSPReportResolver/cspReportContent Security Policy violation report endpoint
MapFilesPatchResolver/**/*.min.js.map, /**/*.min.css.mapSource 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:

  1. Collects all routes from globalThis.oidisAPIRoutes (populated by @Route decorators)
  2. Orders by namespace hierarchy — routes from higher-level loaders override lower-level ones with the same path and HTTP method
  3. Sorts by priority field (ascending) for final ordering
  4. Caches the sorted result for subsequent lookups

Resolver Lifecycle​

Each resolver follows this lifecycle in BaseHttpResolver:

  1. Process() — entry point, triggers the resolver chain
  2. argsHandler() — extracts GET/POST parameters from the request
  3. accessValidator() — checks authentication and authorization
  4. browserValidator() — validates user agent (rejects search bots for API calls)
  5. resolver() — executes the core resolution logic
  6. afterLoad() — 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) and Reporting-Endpoints (v1, modern Chromium) headers, plus the report-to/report-uri directives, all pointing at the csp-endpoint group served by CSPReportResolver (/cspReport). Emitting both header versions lets new and older browsers resolve the endpoint.
  • 'report-sample' — appended to script-src* and style-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:

FormatSent byPayload shape
Reporting API (report-to)Chromiumarray of { type: "csp-violation", body: { documentURL, blockedURL, effectiveDirective, sample, … } } (camelCase) — normalized to the deprecated csp-report serialization Sentry accepts
Legacy (report-uri)Firefox, Safarisingle { "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 TypePurpose
WebResponseBase HTTP response with status, headers, body
RESTResponseREST API responses with structured error handling
StreamResponseStreaming responses (e.g., file downloads, Sentry tunnel)
LiveContentResponseServer-sent events for real-time updates
CallbackResponseCallback-based async responses
InBodyStatusResponseStatus code embedded in response body

See also: Response API | Localhost Overview | Routing