This document describes how HTTP requests are processed through the relay system, from initial reception to backend forwarding. It covers request classification, routing logic, session lookup, reverse proxy creation, and metrics collection. For information about how sessions are established and managed, see Store and Session Management. For details on the transport implementation that carries tunneled traffic, see HTTP Tunneling with RoundTripper.
The relay processes incoming HTTP requests through multiple stages: request classification (external vs internal), routing (path-based or host-based), session lookup (with alias resolution), reverse proxy creation, and traffic forwarding through established tunnel sessions. Each stage involves different components that collaborate to route traffic to the appropriate backend service.
Request Flow Architecture
Sources: misc.go1-79 ingress.go1-143 High-Level Diagram 4
When a request arrives at the relay, it first enters through protocol-specific servers (WSServer or WTServer) which call their IsUpgrade() method to determine whether the request is a protocol upgrade (WebSocket/WebTransport connection) or a regular HTTP request. Non-upgrade requests are routed through the Dispatch() method based on request characteristics.
Sources: High-Level Diagram 4
The relay distinguishes between external requests (normal traffic) and internal requests (administrative endpoints). Internal requests are identified by a .internal TLD suffix.
Sources: misc.go22-24 misc.go38-55 misc.go58-78 misc.go16-20
The IsInternal() function checks if the request host ends with .internal by calling utils.StripPort() to remove port numbers and then checking with strings.HasSuffix() for the suffix misc.go22-24 Internal requests with the special hostname root.internal (defined as ROOT_INTERNAL constant at misc.go13) are routed to RootInternalHandler(), which serves administrative endpoints. Other .internal hosts are handled by handleInternal() which returns a simple status message misc.go16-20
The WSServer.RootInternalHandler() serves three administrative endpoints controlled by environment variables:
| Endpoint Purpose | Environment Variable | Handler Function | Response Type |
|---|---|---|---|
| Debug variables (expvars) | INTERNAL_DEBUG_VARS_PATH | expvar.Handler() | JSON metrics |
| Session records API | INTERNAL_API_SESSIONS_PATH | WSServer.RecordsHandler() | JSON array of Record |
| Alias management API | INTERNAL_ALIASES_PATH | WSServer.AliasHandler() | JSON map or status |
Sources: misc.go38-55
The handler checks each environment variable and matches against r.URL.Path:
os.Getenv("INTERNAL_DEBUG_VARS_PATH") matches, serves expvar.Handler() misc.go39-42os.Getenv("INTERNAL_API_SESSIONS_PATH") matches, serves s.RecordsHandler(w, r) misc.go44-47os.Getenv("INTERNAL_ALIASES_PATH") matches, serves s.AliasHandler(w, r) misc.go49-52http.NotFound(w, r) misc.go54When a request is sent to the root hostname (without a subdomain), the relay performs path-based routing by extracting the leading path component and treating it as a session key.
The leadingComponent() function extracts the first segment of the URL path:
Sources: misc.go34-36
The function implementation at misc.go34-36:
func leadingComponent(s string) string {
return strings.Split(strings.TrimPrefix(s, "/"), "/")[0]
}
For example, a request to http://root/abc/def/page.html extracts "abc" as the session key, and the path /abc/def/page.html is later stripped to /def/page.html before forwarding.
The WSServer.RootHandler() method implements path-based routing:
Sources: misc.go58-78
The handler performs these steps:
leadingComponent(r.URL.Path) misc.go59GetRoundTripper(rpath) to retrieve the session's transport misc.go60LoggedReverseProxy with the RoundTripper misc.go66http.StripPrefix() to remove the leading component misc.go76WebteleportRelayStreamsClosed counter after serving misc.go77For requests with non-root hostnames, the relay uses the hostname as the session key directly.
The IngressHandler.Dispatch() method implements host-based routing:
Sources: ingress.go118-134
The dispatch process:
r.Host ingress.go119GetRoundTripper(r.Host) ingress.go119utils.HostNotFoundHandler() ingress.go121utils.LoggedReverseProxy(rt) with the RoundTripper ingress.go123rp.Rewrite to configure X-Forwarded headers, host, and scheme ingress.go124-128rp.ModifyResponse callback to increment expvars.WebteleportRelayStreamsClosed ingress.go129-132The GetRoundTripper() method is the key abstraction for retrieving a session's HTTP transport.
Sources: ingress.go45-51 store.go140-149 store.go114-125
The IngressHandler.GetRoundTripper() method:
storage.GetRecord(h) with the hostname ingress.go46nil, false if not found ingress.go47-48rec.RoundTripper, true if found ingress.go50The underlying Store.GetRecord() performs store.go140-149:
utils.StripPort(h) to remove port number store.go141idna.ToASCII(k) to convert internationalized domain names store.go142strings.Split(k, ".")[0] to extract first component store.go143LookupRecord(k) with normalized key store.go144The Store.LookupRecord() method store.go114-125:
RecordMap[k] for direct match store.go116AliasMap[k] for alias mapping store.go118RecordMap[aliasKey] store.go120Record if found at either stepEach Record contains the metadata and transport for a session:
| Field | Type | Purpose |
|---|---|---|
Key | string | Unique session identifier |
Session | tunnel.Session | Underlying tunnel connection |
RoundTripper | http.RoundTripper | HTTP transport (with metrics) |
Header | tags.Tags | Request headers |
Tags | tags.Tags | Session tags for filtering |
Since | time.Time | Session start time |
IP | string | Client IP address |
Path | string | Original request path |
Sources: record.go13-22
The RoundTripper field is wrapped with MetricsTransport during record creation, enabling automatic statistics collection for all traffic through this session.
Once a RoundTripper is obtained, the relay creates a reverse proxy to forward traffic.
The Rewrite function modifies outgoing requests:
Sources: misc.go67-75 ingress.go124-128
Both RootHandler and IngressHandler.Dispatch() use the same rewrite pattern:
req.SetXForwarded() to populate X-Forwarded-For, X-Forwarded-Host, and X-Forwarded-Proto headers misc.go68 ingress.go125req.Out.URL.Host to preserve the original host misc.go70 ingress.go126req.Out.URL.Scheme to "http" misc.go74 ingress.go127The scheme is hardcoded to "http" because the tunnel itself handles the transport encryption; the backend service receives plain HTTP traffic over the secure tunnel.
For path-based routing through RootHandler, the leading component is stripped before forwarding:
Original: GET /abc/def/ghi
Extracted: "abc" (session key)
Stripped: GET /def/ghi (forwarded to backend)
This is accomplished by wrapping the reverse proxy with http.StripPrefix("/"+rpath, rp) misc.go76
Sources: misc.go76
The MetricsTransport wrapper intercepts all HTTP traffic to collect statistics.
Sources: metrics.go102-141
The TransportStats structure maintains comprehensive metrics:
| Metric | Type | Description |
|---|---|---|
BytesSent | int64 | Total bytes sent to backend |
BytesReceived | int64 | Total bytes received from backend |
RequestCount | int64 | Number of requests initiated |
ResponseCount | int64 | Number of successful responses |
FailedRequests | int64 | Number of failed requests |
ActiveRequests | int64 | Current in-flight requests |
TotalRequestDuration | time.Duration | Cumulative request time |
LastRequestTime | time.Time | Timestamp of last request |
MaxRequestDuration | time.Duration | Longest request time |
MinRequestDuration | time.Duration | Shortest request time |
Sources: metrics.go11-22
The transport wraps request and response bodies with metric readers/writers:
Sources: metrics.go76-88
The wrapBody() function checks if the body implements io.ReadWriteCloser metrics.go77 and wraps it with either metricsReadWriteCloser metrics.go78-82 or metricsReadCloser metrics.go84-87 to count bytes as they're read or written.
The relay provides multiple fallback mechanisms when sessions are not found.
Sources: misc.go26-32 ingress.go121
When a session is not found:
IngressHandler.Dispatch() returns utils.HostNotFoundHandler() ingress.go121RootHandler() calls DefaultIndex().ServeHTTP() misc.go62DefaultIndex() checks the INDEX environment variable misc.go28WellKnownHealthMiddleware misc.go31After serving each proxied request, the relay increments expvar counters:
RootHandler increments WebteleportRelayStreamsClosed misc.go77IngressHandler.Dispatch increments the same counter in ModifyResponse ingress.go130This tracks the number of HTTP streams (requests) that have been completed through the relay.
Sources: misc.go77 ingress.go129-132
Here is an end-to-end example of a request being processed:
Sources: misc.go58-78 ingress.go45-51 metrics.go102-141
Refresh this wiki