diff --git a/.github/workflows/rps-saturation.yml b/.github/workflows/rps-saturation.yml
index 2c897e0f9..e9495638b 100644
--- a/.github/workflows/rps-saturation.yml
+++ b/.github/workflows/rps-saturation.yml
@@ -60,6 +60,11 @@ on:
required: false
type: string
default: ''
+ arm_excludes:
+ description: 'Drop arms whose name matches this substring (same syntax as arm_contains)'
+ required: false
+ type: string
+ default: ''
workflow_dispatch:
inputs:
mode:
@@ -243,6 +248,10 @@ on:
description: 'Optional arm-name substring filter (e.g. nc-tiny). Empty = all arms in mode.'
required: false
default: ''
+ arm_excludes:
+ description: 'Drop arms whose name matches this substring. Empty = keep every arm.'
+ required: false
+ default: ''
permissions:
contents: read
@@ -260,6 +269,7 @@ jobs:
RPS_REPEATS: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.repeats || 1 }}
RPS_ARM_SHARD: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.arm_shard || 'all' }}
RPS_ARM_CONTAINS: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.arm_contains || '' }}
+ RPS_ARM_EXCLUDES: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.arm_excludes || '' }}
strategy:
fail-fast: false
matrix:
@@ -890,6 +900,7 @@ jobs:
RPS_REPEATS: ${{ env.RPS_REPEATS }}
RPS_ARM_SHARD: ${{ env.RPS_ARM_SHARD }}
RPS_ARM_CONTAINS: ${{ env.RPS_ARM_CONTAINS }}
+ RPS_ARM_EXCLUDES: ${{ env.RPS_ARM_EXCLUDES }}
run: |
$hap = Join-Path $env:RUNNER_TEMP 'haproxy-quic/sbin/haproxy'
$hapArgs = @()
@@ -902,6 +913,10 @@ jobs:
Write-Host "Arm name filter: $($env:RPS_ARM_CONTAINS)"
$containsArgs = @('-ArmContains', $env:RPS_ARM_CONTAINS)
}
+ if ($env:RPS_ARM_EXCLUDES) {
+ Write-Host "Arm name exclude: $($env:RPS_ARM_EXCLUDES)"
+ $containsArgs += @('-ArmExcludes', $env:RPS_ARM_EXCLUDES)
+ }
pwsh tools/RpsLoadProbe/run-rps.ps1 `
-Mode $env:RPS_MODE `
-Concurrency $env:RPS_CONCURRENCY `
@@ -949,7 +964,8 @@ jobs:
-ResultsDir tools/RpsLoadProbe/results `
-Mode $env:RPS_MODE `
-ArmShard $env:RPS_ARM_SHARD `
- -Repeats ([int]$env:RPS_REPEATS)
+ -Repeats ([int]$env:RPS_REPEATS) `
+ -ArmExcludes $env:RPS_ARM_EXCLUDES
# Post-ramp gates: hard-fail on every event (including workflow_dispatch).
# fail-fast: false keeps sibling OS jobs running; Upload CSV uses if: always().
@@ -962,7 +978,7 @@ jobs:
$csv = Get-ChildItem tools/RpsLoadProbe/results -Filter 'rps-ramp-*.csv' |
Sort-Object LastWriteTime -Descending | Select-Object -First 1
if (-not $csv) { throw 'No CSV found for edition gate validation' }
- pwsh tools/RpsLoadProbe/validate-edition-gates.ps1 -CsvPath $csv.FullName
+ pwsh tools/RpsLoadProbe/validate-edition-gates.ps1 -CsvPath $csv.FullName -AllowPartial
- name: Validate compare-product gates
id: gate-product
diff --git a/.github/workflows/rps-suite.yml b/.github/workflows/rps-suite.yml
index 6ed53cd2e..f7c30517b 100644
--- a/.github/workflows/rps-suite.yml
+++ b/.github/workflows/rps-suite.yml
@@ -1,8 +1,14 @@
-# One job per wiki row for a single ramp mode, on all three OS.
+# One job per wiki row for a single ramp mode.
+# Every mode runs on Windows, Linux, and macOS except compare-editions, which is Linux only.
+# Skipped Windows and macOS jobs on editions must not fail the suite.
+# compare-product on Windows and macOS drops twp-mitm-* arms. Linux keeps them in the same
+# job as that row's reverse peers so Lite÷Reverse stays same-VM.
+# compare-spot on beta/stable pull requests is not this workflow.
# Dispatch one run per mode (compare-product, compare-bodies, ...) so each table has one
# Source URL and the paste scripts, which look arms up by name, never mix modes.
-# compare-saturation and compare-editions stay one job per OS (key "all"): saturation Block A
-# compares proxy arms against origin-direct from the same job.
+# compare-saturation stays one job per OS (key "all"): saturation Block A compares proxy
+# arms against origin-direct from the same job. compare-editions is one group per wiki row;
+# each group contains the feature arm and the baseline it is divided by.
#
# Free-plan account limits: 20 concurrent jobs, 5 of them macOS. This workflow caps each OS
# matrix itself; runs of different modes still queue together against the account limit.
@@ -41,6 +47,11 @@ on:
description: 'Concurrent Windows or Linux legs'
required: true
default: '15'
+ only_keys:
+ description: 'Comma-separated group keys (empty = every group for the mode)'
+ required: false
+ default: ''
+ type: string
permissions:
contents: read
@@ -66,10 +77,11 @@ jobs:
shell: pwsh
env:
SUITE_MODE: ${{ inputs.mode }}
+ ONLY_KEYS: ${{ inputs.only_keys }}
run: |
$ErrorActionPreference = 'Stop'
- # These modes compare arms that must share a job, so they stay unsharded.
- $unsharded = @('compare-saturation', 'compare-editions')
+ # Saturation compares proxy arms against origin-direct from the same job.
+ $unsharded = @('compare-saturation')
if ($unsharded -contains $env:SUITE_MODE) {
$keys = @('all')
}
@@ -87,6 +99,15 @@ jobs:
# Longest rows first so the slowest work starts in the first wave.
$keys = @($groups | Sort-Object Arms -Descending | Select-Object -ExpandProperty Key)
}
+ if (-not [string]::IsNullOrWhiteSpace($env:ONLY_KEYS)) {
+ $want = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
+ foreach ($part in $env:ONLY_KEYS.Split(',')) {
+ $trimmed = $part.Trim()
+ if ($trimmed) { [void]$want.Add($trimmed) }
+ }
+ $keys = @($keys | Where-Object { $want.Contains($_) })
+ if ($keys.Count -eq 0) { throw "only_keys matched no groups: $($env:ONLY_KEYS)" }
+ }
Write-Host ("{0}: {1} group(s): {2}" -f $env:SUITE_MODE, $keys.Count, ($keys -join ', '))
# Piping a one-element array into ConvertTo-Json unwraps it to a JSON string.
# fromJSON then is not an array, and the OS matrices are skipped (saturation,
@@ -114,6 +135,7 @@ jobs:
windows:
name: ${{ inputs.mode }} / ${{ matrix.key }} / windows
needs: prep
+ if: ${{ inputs.mode != 'compare-editions' }}
strategy:
fail-fast: false
max-parallel: ${{ fromJSON(inputs.max_parallel_other) }}
@@ -125,10 +147,12 @@ jobs:
runner_os: windows-latest
arm_shard: ${{ matrix.key }}
repeats: ${{ inputs.repeats }}
+ arm_excludes: ${{ inputs.mode == 'compare-product' && 'twp-mitm-' || '' }}
macos:
name: ${{ inputs.mode }} / ${{ matrix.key }} / macos
needs: prep
+ if: ${{ inputs.mode != 'compare-editions' }}
strategy:
fail-fast: false
max-parallel: ${{ fromJSON(inputs.max_parallel_mac) }}
@@ -140,6 +164,7 @@ jobs:
runner_os: macos-15
arm_shard: ${{ matrix.key }}
repeats: ${{ inputs.repeats }}
+ arm_excludes: ${{ inputs.mode == 'compare-product' && 'twp-mitm-' || '' }}
aggregate:
name: aggregate ${{ inputs.mode }}
@@ -150,6 +175,7 @@ jobs:
- name: Check every row on every OS
shell: pwsh
env:
+ GH_TOKEN: ${{ github.token }}
SUITE_MODE: ${{ inputs.mode }}
KEYS_JSON: ${{ needs.prep.outputs.keys }}
LINUX_RESULT: ${{ needs.linux.result }}
@@ -159,8 +185,14 @@ jobs:
$ErrorActionPreference = 'Stop'
$keys = @($env:KEYS_JSON | ConvertFrom-Json)
$arts = gh api "repos/$env:GITHUB_REPOSITORY/actions/runs/$env:GITHUB_RUN_ID/artifacts?per_page=100" --paginate --jq '.artifacts[] | select(.name | startswith("rps-status-")) | .name'
+ if ($LASTEXITCODE -ne 0) { throw "Could not list status artifacts (gh exit $LASTEXITCODE)" }
$statusNames = @($arts | Where-Object { $_ })
- $osLabels = [ordered]@{ linux = 'ubuntu-latest'; windows = 'windows-latest'; macos = 'macos-15' }
+ $linuxOnly = $env:SUITE_MODE -eq 'compare-editions'
+ $osLabels = [ordered]@{ linux = 'ubuntu-latest' }
+ if (-not $linuxOnly) {
+ $osLabels.windows = 'windows-latest'
+ $osLabels.macos = 'macos-15'
+ }
$missing = @()
foreach ($key in $keys) {
$label = ($key -replace '/', '-')
@@ -169,6 +201,7 @@ jobs:
if ($statusNames -notcontains $name) { $missing += $name }
}
}
+ $expected = if ($linuxOnly) { "$($keys.Count) x Linux only" } else { "$($keys.Count) x 3 OS" }
$summary = @(
"### RPS suite: $env:SUITE_MODE",
"",
@@ -178,7 +211,7 @@ jobs:
"| windows | $env:WINDOWS_RESULT |",
"| macos | $env:MACOS_RESULT |",
"",
- "Expected rows: $($keys.Count) x 3 OS.",
+ "Expected rows: $expected.",
"Status artifacts found: $($statusNames.Count)."
)
if ($missing) {
@@ -187,8 +220,10 @@ jobs:
$summary += ($missing | ForEach-Object { "- $_" })
}
$summary -join "`n" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8
- $osFailed = @($env:LINUX_RESULT, $env:WINDOWS_RESULT, $env:MACOS_RESULT) | Where-Object { $_ -ne 'success' }
+ $checked = @($env:LINUX_RESULT)
+ if (-not $linuxOnly) { $checked += @($env:WINDOWS_RESULT, $env:MACOS_RESULT) }
+ $osFailed = @($checked | Where-Object { $_ -ne 'success' })
if ($missing -or $osFailed) {
throw "Suite incomplete: $($missing.Count) status artifact(s) missing, OS results: linux=$env:LINUX_RESULT windows=$env:WINDOWS_RESULT macos=$env:MACOS_RESULT"
}
- Write-Host "Suite complete: $($keys.Count) rows x 3 OS"
+ Write-Host "Suite complete: $expected"
diff --git a/docs/api/Titanium.Web.Proxy.Caching.HttpResponseCacheMiddleware.html b/docs/api/Titanium.Web.Proxy.Caching.HttpResponseCacheMiddleware.html
index a7f8933d5..1c60b303a 100644
--- a/docs/api/Titanium.Web.Proxy.Caching.HttpResponseCacheMiddleware.html
+++ b/docs/api/Titanium.Web.Proxy.Caching.HttpResponseCacheMiddleware.html
@@ -128,7 +128,7 @@
Constructors
Edit this page
- View Source
+ View Source
HttpResponseCacheMiddleware(IHttpResponseCache, TimeSpan?)
@@ -167,7 +167,7 @@ Methods
Edit this page
- View Source
+ View Source
InvokeAsync(ProxyMiddlewareContext, ProxyMiddlewareDelegate, CancellationToken)
@@ -224,11 +224,11 @@ Returns
Edit this page
- View Source
+ View Source
TryCacheCurrentResponse(SessionEventArgs)
-
diff --git a/docs/api/Titanium.Web.Proxy.ProxyServer.html b/docs/api/Titanium.Web.Proxy.ProxyServer.html
index 746e5e7a9..ae4b25aba 100644
--- a/docs/api/Titanium.Web.Proxy.ProxyServer.html
+++ b/docs/api/Titanium.Web.Proxy.ProxyServer.html
@@ -1603,6 +1603,39 @@ Property Value
+
+ |
+ Edit this page
+
+
+ View Source
+
+
+ HttpResponseCacheFiller
+
+
+ Declaration
+
+
public Action<SessionEventArgs>? HttpResponseCacheFiller { get; set; }
+
+ Property Value
+
|
Edit this page
@@ -2521,6 +2554,38 @@ Property Value
+
+ |
+ Edit this page
+
+
+ View Source
+
+
+
+ Adds response headers on the way to the client without subscribing BeforeResponse.
+Plus CORS uses this so a listener can stay on H1 terminate-lite.
+
+
+ Declaration
+
+
public Action<Response>? ResponseHeaderContributor { get; set; }
+
+ Property Value
+
|
Edit this page
@@ -2812,7 +2877,7 @@ Property Value
Edit this page
- View Source
+ View Source
ThreadPoolWorkerThread
diff --git a/docs/index.json b/docs/index.json
index 6718f4924..6f8f40e84 100644
--- a/docs/index.json
+++ b/docs/index.json
@@ -7,7 +7,7 @@
"api/Titanium.Web.Proxy.Caching.HttpResponseCacheMiddleware.html": {
"href": "api/Titanium.Web.Proxy.Caching.HttpResponseCacheMiddleware.html",
"title": "Class HttpResponseCacheMiddleware | Titanium Web Proxy",
- "summary": "Class HttpResponseCacheMiddleware GET/HEAD response cache middleware. Only allocated when placed in the middleware list; otherwise the hot path pays nothing. Inheritance object HttpResponseCacheMiddleware Implements IProxyMiddleware Inherited Members object.Equals(object) object.Equals(object, object) object.GetHashCode() object.GetType() object.ReferenceEquals(object, object) object.ToString() Namespace: Titanium.Web.Proxy.Caching Assembly: Titanium.Web.Proxy.dll Syntax public sealed class HttpResponseCacheMiddleware : IProxyMiddleware Constructors | Edit this page View Source HttpResponseCacheMiddleware(IHttpResponseCache, TimeSpan?) Declaration public HttpResponseCacheMiddleware(IHttpResponseCache cache, TimeSpan? defaultTtl = null) Parameters Type Name Description IHttpResponseCache cache TimeSpan? defaultTtl Methods | Edit this page View Source InvokeAsync(ProxyMiddlewareContext, ProxyMiddlewareDelegate, CancellationToken) Declaration public ValueTask InvokeAsync(ProxyMiddlewareContext context, ProxyMiddlewareDelegate next, CancellationToken cancellationToken) Parameters Type Name Description ProxyMiddlewareContext context ProxyMiddlewareDelegate next CancellationToken cancellationToken Returns Type Description ValueTask | Edit this page View Source TryCacheCurrentResponse(SessionEventArgs) Call from AfterResponse when caching upstream 200 bodies. Declaration public void TryCacheCurrentResponse(SessionEventArgs session) Parameters Type Name Description SessionEventArgs session Implements Titanium.Web.Proxy.Abstractions.Middleware.IProxyMiddleware"
+ "summary": "Class HttpResponseCacheMiddleware GET/HEAD response cache middleware. Only allocated when placed in the middleware list; otherwise the hot path pays nothing. Inheritance object HttpResponseCacheMiddleware Implements IProxyMiddleware Inherited Members object.Equals(object) object.Equals(object, object) object.GetHashCode() object.GetType() object.ReferenceEquals(object, object) object.ToString() Namespace: Titanium.Web.Proxy.Caching Assembly: Titanium.Web.Proxy.dll Syntax public sealed class HttpResponseCacheMiddleware : IProxyMiddleware Constructors | Edit this page View Source HttpResponseCacheMiddleware(IHttpResponseCache, TimeSpan?) Declaration public HttpResponseCacheMiddleware(IHttpResponseCache cache, TimeSpan? defaultTtl = null) Parameters Type Name Description IHttpResponseCache cache TimeSpan? defaultTtl Methods | Edit this page View Source InvokeAsync(ProxyMiddlewareContext, ProxyMiddlewareDelegate, CancellationToken) Declaration public ValueTask InvokeAsync(ProxyMiddlewareContext context, ProxyMiddlewareDelegate next, CancellationToken cancellationToken) Parameters Type Name Description ProxyMiddlewareContext context ProxyMiddlewareDelegate next CancellationToken cancellationToken Returns Type Description ValueTask | Edit this page View Source TryCacheCurrentResponse(SessionEventArgs) Call from the session response path when a small 200 body is already buffered. Declaration public void TryCacheCurrentResponse(SessionEventArgs session) Parameters Type Name Description SessionEventArgs session Implements Titanium.Web.Proxy.Abstractions.Middleware.IProxyMiddleware"
},
"api/Titanium.Web.Proxy.Caching.MemoryHttpResponseCache.html": {
"href": "api/Titanium.Web.Proxy.Caching.MemoryHttpResponseCache.html",
@@ -552,7 +552,7 @@
"api/Titanium.Web.Proxy.ProxyServer.html": {
"href": "api/Titanium.Web.Proxy.ProxyServer.html",
"title": "Class ProxyServer | Titanium Web Proxy",
- "summary": "Class ProxyServer Translates an HTTP/1.1 client connection onto an h2-only origin (Http2 with AllowHttpProtocolTranslation enabled - see ResolveHttp2ForClientAsync(SessionEventArgsBase, bool, string, int, string, int?, UpstreamHttpProtocol, bool, bool, CancellationToken, bool)), leasing one h2 stream per HTTP/1.1 request from a shared Titanium.Web.Proxy.Http2.Http2OriginConnection via Titanium.Web.Proxy.ProxyServer.Http2OriginConnectionPool rather than opening a new TCP/TLS connection for every request. Inheritance object ProxyServer Implements IDisposable Inherited Members object.Equals(object) object.Equals(object, object) object.GetHashCode() object.GetType() object.MemberwiseClone() object.ReferenceEquals(object, object) object.ToString() Namespace: Titanium.Web.Proxy Assembly: Titanium.Web.Proxy.dll Syntax public class ProxyServer : IDisposable Remarks This re-implements the HTTP/1.1 client read loop (request line, headers, BeforeRequest, authorization, header preparation, CancelRequest/replaced-response handling) rather than reusing the private HandleHttpSessionRequest/HandleHttpSessionResponse methods, because those methods send/receive over TcpServerConnection.Stream using the raw HTTP/1.1 wire format, which an h2 origin connection cannot speak. This mirrors the precedent set by the h2-to-HTTP/1.1 bridge (Http2ToHttp11BridgeHandler), which similarly bypasses the wire-format-specific machinery for the leg that does not match it. Origin connections are multiplexed across independent HTTP/1.1 clients through Titanium.Web.Proxy.ProxyServer.Http2OriginConnectionPool (fan-in share). Response bodies are delivered via Titanium.Web.Proxy.Http2.Http2OriginConnection streaming writers where available. Constructors | Edit this page View Source ProxyServer(bool, bool, bool) Initializes a new instance of ProxyServer class with provided parameters. Declaration public ProxyServer(bool userTrustRootCertificate = true, bool machineTrustRootCertificate = false, bool trustRootCertificateAsAdmin = false) Parameters Type Name Description bool userTrustRootCertificate When true (the default), EnsureRootCertificate() installs the MITM root into the current-user Personal and Trusted Root stores. Prefer user-only trust for interactive apps; pass false when trust must be fully opt-in. bool machineTrustRootCertificate When true, also trust in the local-machine stores (needs elevation). Defaults to false — machine trust is opt-in for services/admin installs, not for normal desktop use. bool trustRootCertificateAsAdmin When true, attempt elevated trust via UAC (Windows only). Defaults to false. | Edit this page View Source ProxyServer(string?, string?, bool, bool, bool) Initializes a new instance of ProxyServer class with provided parameters. Declaration public ProxyServer(string? rootCertificateName, string? rootCertificateIssuerName, bool userTrustRootCertificate = true, bool machineTrustRootCertificate = false, bool trustRootCertificateAsAdmin = false) Parameters Type Name Description string rootCertificateName Name of the root certificate. string rootCertificateIssuerName Name of the root certificate issuer. bool userTrustRootCertificate When true (the default), EnsureRootCertificate() installs the MITM root into the current-user Personal and Trusted Root stores. Prefer user-only trust for interactive apps; pass false when trust must be fully opt-in. bool machineTrustRootCertificate When true, also trust in the local-machine stores (needs elevation). Defaults to false — machine trust is opt-in for services/admin installs, not for normal desktop use. bool trustRootCertificateAsAdmin When true, attempt elevated trust via UAC (Windows only). Defaults to false. Fields | Edit this page View Source DefaultViaHeaderPseudonym Default Via header pseudonym (RFC 9110 §7.6.3). Used by ViaHeaderPseudonym and by Inspector when Add Via header is enabled. Declaration public const string DefaultViaHeaderPseudonym = \"titanium-web-proxy\" Field Value Type Description string Properties | Edit this page View Source AdmittedClientConnectionCount Number of client connections currently admitted (accepted and past the admission gate, not yet finished being handled), across all TCP-based endpoints. Unlike ClientConnectionCount, this drops to zero as soon as the handler returns, without the trailing TIME_WAIT delay. Declaration public int AdmittedClientConnectionCount { get; } Property Value Type Description int | Edit this page View Source BlockLoopbackDestinations Outbound destination policy hook: when true, every resolved destination IP address is checked for loopback (127.0.0.0/8, ::1, including IPv4-mapped forms such as ::ffff:127.0.0.1) before connecting, and the connection attempt is rejected with an OutboundDestinationBlockedException if it matches. Private (RFC 1918), link-local, and cloud metadata addresses (including 169.254.169.254) remain reachable under this flag alone. Off by default. Use when the proxy must allow intranet destinations (for example 10.*) but must still stop clients from pivoting to the proxy host's own loopback. BlockPrivateNetworkDestinations already covers loopback as part of its broader set; when that flag is enabled, this property is not consulted for the reject. Assigning Profile does not read or clear this property. Enforced on TCP outbound connects only (the same call sites as BlockPrivateNetworkDestinations). Origin HTTP/3 (QUIC) dials a hostname and is not covered. An explicitly configured upstream proxy address is always exempt. Not currently enforced for a SOCKS upstream with ProxyDnsRequests enabled. Declaration public bool BlockLoopbackDestinations { get; set; } Property Value Type Description bool | Edit this page View Source BlockPrivateNetworkDestinations Outbound destination policy hook: when true, every resolved destination IP address is checked against loopback, private (RFC 1918/4193), link-local (which subsumes the 169.254.169.254 cloud metadata endpoint), and other non-globally-routable ranges before connecting, and the connection attempt is rejected with an OutboundDestinationBlockedException if it matches. Off by default: blocking private destinations would break this library's most common configurations, including upstream-proxy chaining to localhost and interception of local development servers. Only enable this when the proxy accepts requests from untrusted clients (an SSRF-relevant deployment), where those same destinations become an attacker-reachable pivot into the host's private network instead of an operator's own intentional configuration. An explicitly configured upstream proxy address (UpStreamHttpProxy, UpStreamHttpsProxy, or a per-session external proxy) is always exempt - that address is operator intent, not attacker-controlled. Checked against the resolved address actually used to connect (no re-resolution afterward, which would make the check a TOCTOU no-op against DNS rebinding). Not currently enforced for a SOCKS upstream with ProxyDnsRequests enabled, since the proxy never resolves the origin itself in that mode and has no address of its own to validate. Declaration public bool BlockPrivateNetworkDestinations { get; set; } Property Value Type Description bool | Edit this page View Source BufferPool The buffer pool used throughout this proxy instance. Set custom implementations by implementing this interface. By default this uses DefaultBufferPool implementation available in StreamExtended library package. Buffer size should be at least 10 bytes. Declaration public IBufferPool BufferPool { get; set; } Property Value Type Description IBufferPool | Edit this page View Source CertificateManager Manages certificates used by this proxy. Declaration public CertificateManager CertificateManager { get; } Property Value Type Description CertificateManager | Edit this page View Source CheckCertificateRevocation Should we check for certificate revocation during SSL authentication to servers Note: If enabled can reduce performance. Defaults to false. Declaration public X509RevocationMode CheckCertificateRevocation { get; set; } Property Value Type Description X509RevocationMode | Edit this page View Source ClientConnectionCount Total number of active TCP client connections. Does not include inbound HTTP/3 (QUIC) clients; see Http3ClientConnectionCount. Declaration public int ClientConnectionCount { get; } Property Value Type Description int | Edit this page View Source ClientHeaderTimeoutSeconds Seconds to wait for a client to finish sending the request line and headers, from the moment this proxy starts reading a new request on the connection. Enforced with a linked CancellationTokenSource around the request-line and header read, not Socket.ReceiveTimeout: that property only bounds a single blocking Receive call, not the asynchronous reads this proxy actually issues, so without this deadline a client that opens a connection and trickles bytes arbitrarily slowly (or stops sending entirely) after the first byte ties up a read loop indefinitely. Default is 0 (disabled), matching every other deadline in this class - no per-session override exists because there is no SessionEventArgs for this request yet at the point this deadline applies. Declaration public int ClientHeaderTimeoutSeconds { get; set; } Property Value Type Description int | Edit this page View Source CompatibilityMode100Continue When true, the proxy immediately responds with a synthetic 100 Continue to any client request carrying Expect: 100-continue, before forwarding the headers to the origin and without waiting for the origin to respond. This breaks the strict handshake (client → proxy 100 → client body → origin body) but prevents the deadlock that occurs with strict clients when Enable100ContinueBehaviour is false (the default). Has no effect when Enable100ContinueBehaviour is true. Default: false. Declaration public bool CompatibilityMode100Continue { get; set; } Property Value Type Description bool | Edit this page View Source ConnectTimeOutSeconds Seconds server connection are to wait for connection to be established. Default value is 20 seconds. Declaration public int ConnectTimeOutSeconds { get; set; } Property Value Type Description int | Edit this page View Source ConnectionTimeOutSeconds Seconds client/server connection are to be kept alive when waiting for read/write to complete. This will also determine the pool eviction time when connection pool is enabled. Default value is 60 seconds. Declaration public int ConnectionTimeOutSeconds { get; set; } Property Value Type Description int | Edit this page View Source CustomUpStreamProxyFailureFunc A callback to provide a chance for an upstream proxy failure to be handled by a new upstream proxy. User should return the ExternalProxy object with valid credentials or null. Declaration public Func>? CustomUpStreamProxyFailureFunc { get; set; } Property Value Type Description Func> | Edit this page View Source DecryptFailureBypassMaxEntries Maximum learned hosts retained (approximate LRU eviction). Default 256. Declaration public int DecryptFailureBypassMaxEntries { get; set; } Property Value Type Description int | Edit this page View Source DecryptFailureBypassThreshold Origin TLS failure strikes required before a host is bypassed on later CONNECTs. Same-CONNECT opaque fallback after an awaited H2 probe failure marks bypass immediately. Default 2. Declaration public int DecryptFailureBypassThreshold { get; set; } Property Value Type Description int | Edit this page View Source DecryptFailureBypassTtl How long a learned decrypt-bypass entry remains valid. Default 30 minutes. Declaration public TimeSpan DecryptFailureBypassTtl { get; set; } Property Value Type Description TimeSpan | Edit this page View Source DnsServerEndPoint DNS server endpoint used by Titanium.Web.Proxy.Http3.Dns.UdpSvcbDnsResolver for HTTPS/SVCB queries. Defaults to the first usable OS-configured plain-UDP DNS server discovered via NetworkInterface. This is a best-effort default and does not honor Windows NRPT, DoH, or VPN split-DNS policy. When no OS-configured DNS server can be discovered, the property reports 0.0.0.0:0 and proactive SVCB discovery is skipped (never falls back to a public third-party resolver). Assign an explicit endpoint to override discovery. Declaration [Experimental(\"TWP001\")] public IPEndPoint DnsServerEndPoint { get; set; } Property Value Type Description IPEndPoint | Edit this page View Source Enable100ContinueBehaviour Does this proxy uses the HTTP protocol 100 continue behaviour strictly? Broken 100 continue implementations on server/client may cause problems if enabled. Defaults to false. Declaration public bool Enable100ContinueBehaviour { get; set; } Property Value Type Description bool | Edit this page View Source EnableConnectionPool Should we enable the server connection pool. Defaults to true. When connection pooling is enabled, instead of creating a new TCP connection to the server for each client TCP connection, we check if an idle server connection is available in our cached pool. If a compatible connection (same destination, scheme, upstream proxy, credentials and negotiated protocol) created from an earlier request is available, we reuse it. Only connections that are safe to reuse under the HTTP protocol are pooled: the response body must be fully received and the connection must be persistent (HTTP/1.1 keep-alive, or an HTTP/1.0 connection that explicitly opted in via \"Connection: keep-alive\"). Connections whose response asked to close, that failed, or that carry connection-oriented authentication state (WinAuth NTLM/Negotiate) or a per-session client certificate are never returned to the shared pool. The ConnectionTimeOutSeconds parameter determines the eviction time for inactive server connections. This reduces TCP (and TLS) connection establishment cost, both in wall clock time and CPU cycles. Set to false to force a fresh server connection for every client connection. Declaration public bool EnableConnectionPool { get; set; } Property Value Type Description bool | Edit this page View Source EnableDecryptFailureBypass When true, the proxy learns hosts whose origin TLS handshake fails under MITM (non-ALPN AuthenticationException, typically bot / TLS-fingerprint rejection) and tunnels subsequent CONNECTs without decrypt. Default false so library and RPS baselines are unchanged. Inspector enables this by default. Success-path cost when on is one dictionary lookup per CONNECT. Declaration public bool EnableDecryptFailureBypass { get; set; } Property Value Type Description bool | Edit this page View Source EnableHttp2 Enable disable HTTP/2 support. Client-facing HTTP/2 is negotiated via TLS ALPN, or as prior-knowledge cleartext h2c on a transparent reverse endpoint (DecryptSsl: false). No Upgrade: h2c. Origin-facing HTTP/2 uses TLS ALPN h2 by default; with ForwardCleartext and Http2, the origin speaks cleartext HTTP/2 prior-knowledge (outbound h2c). A client/server that does not support HTTP/2 transparently falls back to HTTP/1.1 when policy allows. Request/response header and body modification in BeforeRequest/BeforeResponse, chunked trailers, interim (1xx) responses, and the synthetic-response APIs (Ok/Respond/Redirect/GenericResponse/ RespondStreaming) are all supported over HTTP/2, the same as over HTTP/1.x. Not supported: HTTP/2 server push (the wire frames are transcoded but there is no public API to originate a push) and Upgrade: h2c. Explicit-proxy inbound h2c is not implemented. See the protocol support matrix on the wiki for exact, up-to-date HTTP/1.x/HTTP/2 feature coverage. Declaration public bool EnableHttp2 { get; set; } Property Value Type Description bool | Edit this page View Source EnableHttp3 Enable HTTP/3 (QUIC) support. When true: Any TransparentQuicProxyEndPoint is started as a UDP-only QUIC listener for transparent/NAT HTTP/3 interception. Any TransparentProxyEndPoint with EnableHttp3 also listens for HTTP/3 on the same IP:port (TCP H1/H2 + UDP H3) and injects client-facing Alt-Svc. With Auto (default), a cached Alt-Svc / HTTPS/SVCB capability selects outbound HTTP/3 on the next CONNECT or new HTTP/1.1 request. Background QUIC warm-up starts when the cache is filled so that handshake is often already done. An already-open H2↔H2 MITM session is not upgraded mid-connection. Forced Http3 fails closed with no TCP fallback. Requires MsQuic native library and a supported operating-system version (IsSupported). Setting to true with no inbound HTTP/3 endpoint is fine when an explicit/SOCKS/transparent TCP endpoint is present (origin-side QUIC only). A warning is emitted only when EnableHttp3 is set with no client-facing endpoints at all. Default: false (opt-in). Experimental: HTTP/3 support has not yet completed the full interop/soak/fuzz gate process. Suppress TWP001 to opt in; the attribute is removed when the feature graduates to stable. Declaration [Experimental(\"TWP001\")] public bool EnableHttp3 { get; set; } Property Value Type Description bool | Edit this page View Source EnableHttpInterception Forces the full interception path (SessionEventArgs, BeforeRequest, etc.) even when no event handlers are subscribed. Set this when consuming SessionEventArgs for timing or metrics without subscribing to any event. Default: false. Declaration public bool EnableHttpInterception { get; set; } Property Value Type Description bool | Edit this page View Source EnableHttpsSvcbDnsDiscovery When true, the proxy queues a background HTTPS/SVCB RR (DNS type 65) lookup after an Auto-mode capability-cache miss. A positive result (ALPN h3 found) warms Titanium.Web.Proxy.Http3.Http3OriginCapabilityCache for subsequent connections; the CONNECT / request path itself never awaits DNS. Negative results are cached for 1 minute; transient failures use a short backoff. Defaults to true whenever EnableHttp3 is true. Set explicitly to false to disable discovery even when HTTP/3 is enabled — for example, when the configured DNS server is untrusted or unreachable. First-connection HTTP/3 adoption then comes from Alt-Svc. Declaration [Experimental(\"TWP001\")] public bool EnableHttpsSvcbDnsDiscovery { get; set; } Property Value Type Description bool | Edit this page View Source EnableIpv6UnreachableSoftSkip When true (default), after one IPv6 connect failure with NetworkUnreachable (or equivalent), temporarily omit IPv6 addresses from the Happy Eyeballs race for 30 seconds. Reduces first-chance SocketException noise on dual-stack hosts with a broken IPv6 path. Disable if operators require strict IPv6 preference even when the path is unreachable. Declaration public bool EnableIpv6UnreachableSoftSkip { get; set; } Property Value Type Description bool | Edit this page View Source EnableQpackDynamicTable When true, enables RFC 9204 QPACK dynamic table encoding and decoding for inbound HTTP/3 connections. Each connection gets its own Titanium.Web.Proxy.Http3.Qpack.QpackContext with two independent 4096-byte tables (one inbound, one outbound). Defaults to false (static-table-only); existing deployments are unaffected. Declaration [Experimental(\"TWP001\")] public bool EnableQpackDynamicTable { get; set; } Property Value Type Description bool | Edit this page View Source EnableRequestTimingCapture Enables structured request/connection timing capture. When false (the default) no timing objects are allocated and no UtcNow calls are made for timing purposes anywhere in the proxy, so there is zero overhead on the hot path. When enabled, every SessionEventArgsBase exposes a populated Timing (per-request phases: client header read, connection wait, request send, time-to-first-byte, response delivery, total), every upstream connection exposes a populated UpstreamConnectionTiming (reachable from a session via UpstreamConnectionTiming, describing DNS, TCP connect, optional upstream-proxy CONNECT, and TLS handshake durations), and a decrypted TunnelConnectSessionEventArgs exposes the client-facing TLS handshake duration via ClientTlsTiming. Can be toggled at any time; it only affects sessions/connections created after the change, never mutating timing objects already handed out. Defaults to false. Declaration public bool EnableRequestTimingCapture { get; set; } Property Value Type Description bool | Edit this page View Source EnableRfc8441 When true, the proxy enables RFC 8441 WebSocket-over-HTTP/2: Accepts extended CONNECT (:protocol = websocket) from h2 clients and advertises SETTINGS_ENABLE_CONNECT_PROTOCOL=1 to them. Per origin: if the origin is HTTP/2 and advertises RFC 8441 support, DATA frames are relayed directly; if the origin is HTTP/2 and does not, the stream is reset with REFUSED_STREAM; if the origin is HTTP/1.1, the h2→h1 WebSocket upgrade bridge is used. On the HTTP/1.1-client-to-h2-origin translation bridge, translates Upgrade: websocket into extended CONNECT when the origin advertises the setting; otherwise falls back to a dedicated HTTP/1.1 origin connection for that WebSocket. When this property is false, that bridge still returns synthetic 501 Not Implemented for WebSocket upgrades (historical default). Default: false (must opt-in). Declaration public bool EnableRfc8441 { get; set; } Property Value Type Description bool | Edit this page View Source EnableTcpKeepAlive Enable TCP KeepAlive on client and server sockets so NAT/firewall mappings for long-lived CONNECT tunnels are refreshed. Default: true. Declaration public bool EnableTcpKeepAlive { get; set; } Property Value Type Description bool | Edit this page View Source EnableTcpServerConnectionPrefetch Should we enable tcp server connection prefetching? When enabled, as soon as we receive a client connection we concurrently initiate corresponding server connection process using CONNECT hostname or SNI hostname on a separate task so that after parsing client request we will have the server connection immediately ready or in the process of getting ready. If a server connection is available in cache then this prefetch task will immediately return with the available connection from cache. Defaults to true. Declaration public bool EnableTcpServerConnectionPrefetch { get; set; } Property Value Type Description bool | Edit this page View Source EnableWinAuth Enable disable Windows Authentication (NTLM/Kerberos). By default SSPI uses the process identity. To authenticate as another user, set WinAuthCredentialsProvider (issue #461). Defaults to false. Declaration public bool EnableWinAuth { get; set; } Property Value Type Description bool | Edit this page View Source EndpointAdmissionRejectionCount Total number of client connections rejected by any endpoint's MaxConcurrentClients since this instance was created. Declaration public long EndpointAdmissionRejectionCount { get; } Property Value Type Description long | Edit this page View Source ForwardToUpstreamGateway Gets or sets a value indicating whether requests will be chained to upstream gateway. Defaults to false. Declaration public bool ForwardToUpstreamGateway { get; set; } Property Value Type Description bool | Edit this page View Source GetCustomUpStreamProxyFunc A callback to provide authentication credentials for up stream proxy this proxy is using for HTTP(S) requests. User should return the ExternalProxy object with valid credentials. Declaration public Func>? GetCustomUpStreamProxyFunc { get; set; } Property Value Type Description Func> | Edit this page View Source GlobalAdmissionRejectionCount Total number of client connections rejected by MaxConcurrentClientConnections since this instance was created. Declaration public long GlobalAdmissionRejectionCount { get; } Property Value Type Description long | Edit this page View Source Http3ClientConnectionCount Total number of active inbound HTTP/3 (QUIC) client connections. Declaration public int Http3ClientConnectionCount { get; } Property Value Type Description int | Edit this page View Source Http3ServerConnectionCount Total number of active upstream HTTP/3 (QUIC) server connections. These are also included in ServerConnectionCount. Declaration public int Http3ServerConnectionCount { get; } Property Value Type Description int | Edit this page View Source IdleReadTimeoutSeconds Seconds of idle time allowed while reading from the origin (stalled header/body waits). Applied via CancelAfter on the active read operation. Default is 0 (disabled). Per-session override: IdleReadTimeout. Declaration public int IdleReadTimeoutSeconds { get; set; } Property Value Type Description int | Edit this page View Source IdleWriteTimeoutSeconds Seconds of idle time allowed while writing to the origin (stalled header/body waits). Applied via CancelAfter on the active write operation. Default is 0 (disabled). Per-session override: IdleWriteTimeout. Declaration public int IdleWriteTimeoutSeconds { get; set; } Property Value Type Description int | Edit this page View Source IgnoreServerCertificateErrors When true, origin TLS certificates that fail OS chain validation are still accepted (MITM of loopback/self-signed/private CAs). Inspector's \"Ignore server certificate errors\" maps here. Default false. A subscribed ServerCertificateValidationCallback still wins. Declaration public bool IgnoreServerCertificateErrors { get; set; } Property Value Type Description bool | Edit this page View Source ListenerBackLog TCP listener accept backlog. Default: 1024 for burst connection handling. Declaration public int ListenerBackLog { get; set; } Property Value Type Description int | Edit this page View Source Logger The live, shared logger used throughout this proxy instance. Reflects the most recent call to ApplyLoggingConfiguration(). Declaration public ILogger Logger { get; } Property Value Type Description ILogger | Edit this page View Source Logging Configuration for this proxy instance's built-in diagnostic logging - the replacement for the removed ExceptionFunc callback. Every exception the proxy catches (even when handled internally and never surfaced to user code) is reported through this logger at an appropriate severity; see ProxyLoggingOptions for the console/file sinks, enable/disable switch, and minimum level. Mutate the returned instance (or assign a new one) at any point; each assignment/mutation you want to take effect must be followed by ApplyLoggingConfiguration() (which Start(bool) also calls automatically, so the configuration active at the moment the proxy starts running is picked up for the run even if you never call it yourself). Calling it again later - including while the proxy is already running - immediately swaps in the new configuration; this is safe because logging never blocks or otherwise affects proxy traffic. Declaration public ProxyLoggingOptions Logging { get; set; } Property Value Type Description ProxyLoggingOptions | Edit this page View Source MaxBufferedBodyBytes Maximum bytes the proxy will buffer for a single request or response body when body buffering is required (body-read hooks, authentication retry, etc.). Bodies larger than this limit are rejected with 413 (upstream request) or connection teardown (upstream response). Set to 0 to disable the limit (not recommended). Default: 4,194,304 (4 MiB). Declaration public int MaxBufferedBodyBytes { get; set; } Property Value Type Description int | Edit this page View Source MaxCachedConnections Maximum number of concurrent connections per remote host in cache. Only meaningful when EnableConnectionPool is true; to disable pooling, set EnableConnectionPool to false rather than setting this to 0 - the pool eviction loop treats a value below 1 as \"evict without limit while holding the pool-wide lock\", which spins indefinitely once the cache for that host is empty and would stall every other connection acquire/release in the process. Rejected outright at assignment so that state cannot be reached. Default value is 128. Declaration public int MaxCachedConnections { get; set; } Property Value Type Description int Exceptions Type Condition ArgumentOutOfRangeException The assigned value is less than 1. | Edit this page View Source MaxConcurrentClientConnections Maximum number of client connections admitted across all TCP-based endpoints at once. null (the default) disables the global admission gate, preserving today's unbounded behavior. When set, a connection beyond this limit is rejected and disposed immediately after accept, before a handler task is even started. Enforced independently of ClientConnectionCount: see Titanium.Web.Proxy.ProxyServer.admittedClientConnectionCount for why. See also MaxConcurrentClients for a per-endpoint cap layered on top of this global one. Declaration public int? MaxConcurrentClientConnections { get; set; } Property Value Type Description int? | Edit this page View Source MaxConcurrentHttp11HttpsOriginCreates Caps concurrent new HTTPS origin TCP/TLS opens on the H2→H1 bridge only (MITM / re-encrypt). Pool hits (warm keep-alive) are uncapped. Cleartext H1 origins are not gated. Default is Clamp(ProcessorCount, 4, 32). Set before the first H2→H1 HTTPS origin open (typically before Start(bool)); changing the value after the create gate has been used has no effect on the live semaphore. Declaration public int MaxConcurrentHttp11HttpsOriginCreates { get; set; } Property Value Type Description int Exceptions Type Condition ArgumentOutOfRangeException The assigned value is less than 1. | Edit this page View Source MaxDecodedHeaderListBytes Maximum decoded HTTP/2 header list size in bytes, using RFC 7541 accounting (name.Length + value.Length + 32 per field). Requests or responses with a decoded header list exceeding this limit will be refused with RST_STREAM(ENHANCE_YOUR_CALM) (code 0xb). Set to 0 to disable the limit (not recommended). Default: 65,536 (64 KiB). Advertised via SETTINGS_MAX_HEADER_LIST_SIZE. Declaration public int MaxDecodedHeaderListBytes { get; set; } Property Value Type Description int | Edit this page View Source MaxWebSocketFramePayloadBytes Maximum WebSocket frame payload size in bytes that the proxy will accept during frame-level interception (i.e. when BeforeWebSocketFrame has at least one subscriber). Frames whose decoded payload exceeds this limit cause the WebSocket connection to be closed with Close code 1009 (Message Too Big). Raw-relay sessions (no BeforeWebSocketFrame subscriber) bypass this check entirely and pass all frames through unvalidated. Default: 16,777,216 (16 MiB). Declaration public int MaxWebSocketFramePayloadBytes { get; set; } Property Value Type Description int | Edit this page View Source NetworkFailureRetryAttempts Number of times to retry upon network failures when connection pool is enabled. Declaration public int NetworkFailureRetryAttempts { get; set; } Property Value Type Description int | Edit this page View Source NoDelay Gets or sets a Boolean value that specifies whether server and client stream Sockets are using the Nagle algorithm. Defaults to true, no nagle algorithm is used. Declaration public bool NoDelay { get; set; } Property Value Type Description bool | Edit this page View Source OriginHttpVersionPolicy Controls which HTTP version is declared to the origin server on the request line, independently of the version the client declared to the proxy. Defaults to PreserveClientVersion, which matches the proxy's historical pass-through behavior exactly. Set to NormalizeToHttp11 to let HTTP/1.0 clients share pooled, persistent origin connections the same way HTTP/1.1 clients already do. This only changes the wire version written to the origin request line - it never changes the client-facing Http.Request.HttpVersion that event handlers observe, nor the version/persistence used to write the response back to the client. Declaration public OriginHttpVersionPolicy OriginHttpVersionPolicy { get; set; } Property Value Type Description OriginHttpVersionPolicy | Edit this page View Source PolicyModes Which resource-bound PolicyFamily is enforced, observed, or disabled, per the plan's rollout section. Read live by each family's enforcement call site - not baked into a per-request snapshot at connection accept time - so assigning a new value here (a whole-object replacement, never a mutation of the previous instance) takes effect for the next check any in-flight or new request makes, without restarting the proxy. This is the \"runtime switch to drop to Observe without redeploying\" the plan requires; see WithAllObservedExceptDisabled() for the one-call way to do that. Defaults to Balanced's modes (resource families enforced, Http2RelayValidation disabled so compressed H2 relay stays the verbatim-HPACK fast path). AllEnforce additionally enforces relay validation and is what PublicFacing applies. Assigning Profile also replaces this value with that profile's bundle; assign PolicyModes afterward to deviate from the selected profile's modes without changing anything else the profile set. Declaration public ProxyPolicyModes PolicyModes { get; set; } Property Value Type Description ProxyPolicyModes | Edit this page View Source Profile The last profile applied via this property's setter, defaulting to Balanced - the profile every field on this instance already starts at, so a fresh new ProxyServer() reports Balanced without needing its setter to run once at construction time. Assigning this property applies its entire ProxyProfileSettings bundle - ResourceLimits, PolicyModes, SupportedSslProtocols, BlockPrivateNetworkDestinations, MaxConcurrentClientConnections and the deadline-seconds properties - as a single atomic assignment, so a reader can never observe a half-applied profile. Assigning any of those properties individually afterward overrides just that one, without reverting the rest of the profile's bundle. Logged once per Start(bool) call, by name only - never with hosts, URLs or secrets, per the plan's rollout section. Declaration public ProxyProfile Profile { get; set; } Property Value Type Description ProxyProfile | Edit this page View Source ProxyAuthenticationRealm Realm used during Proxy Basic Authentication. Declaration public string ProxyAuthenticationRealm { get; set; } Property Value Type Description string | Edit this page View Source ProxyAuthenticationSchemes A collection of scheme types, e.g. basic, NTLM, Kerberos, Negotiate, to return if scheme authentication is required. Works in relation with ProxySchemeAuthenticateFunc. Declaration public IEnumerable ProxyAuthenticationSchemes { get; set; } Property Value Type Description IEnumerable | Edit this page View Source ProxyBasicAuthenticateFunc A callback to authenticate proxy clients via basic authentication. Parameters are username and password as provided by client. Should return true for successful authentication. Declaration public Func>? ProxyBasicAuthenticateFunc { get; set; } Property Value Type Description Func> | Edit this page View Source ProxyEndPoints A list of IpAddress and port this proxy is listening to. Declaration public List ProxyEndPoints { get; set; } Property Value Type Description List | Edit this page View Source ProxyRunning Is the proxy currently running? Declaration public bool ProxyRunning { get; } Property Value Type Description bool | Edit this page View Source ProxySchemeAuthenticateFunc A pluggable callback to authenticate clients by scheme instead of requiring basic authentication through ProxyBasicAuthenticateFunc. Parameters are current working session, schemeType, and token as provided by a calling client. Should return success for successful authentication, continuation if the package requests, or failure. Declaration public Func>? ProxySchemeAuthenticateFunc { get; set; } Property Value Type Description Func> | Edit this page View Source RequestTimeoutSeconds Total seconds allowed for a single request/response exchange after BeforeRequest returns (connect, send, wait for headers, and body copy). Default is 0 (disabled). Per-session override: RequestTimeout. Declaration public int RequestTimeoutSeconds { get; set; } Property Value Type Description int | Edit this page View Source ResourceLimits The shared, immutable resource-bound snapshot (concurrent-stream cap, CONTINUATION frame-count/wall-clock bounds, peer-initiated incomplete-stream-reset budget, and the other limits described in ProxyResourceLimits) consulted by the HTTP/2 relay so a single proxy-owned value governs both what is enforced and what is advertised to each peer, rather than admitting purely against whatever the origin advertised. Assign a new ProxyResourceLimits (constructed via Create(long, int, long, long?, long?, double?, int?, int, int?, int, TimeSpan, bool, int, int?)) to override the Default snapshot. There is no artificial upper clamp: high-CPU/RAM hosts may pass larger maxCachedConnectionsPerHost, maxConcurrentStreamsPerConnection, etc. as needed. The live TCP pool depth knob MaxCachedConnections remains independently settable and should usually be kept in sync with MaxCachedConnectionsPerHost. Declaration public ProxyResourceLimits ResourceLimits { get; set; } Property Value Type Description ProxyResourceLimits | Edit this page View Source ResponseHeaderTimeoutSeconds Seconds to wait for the origin to send the response status line and headers after the request has been sent. Enforced with a linked CancellationTokenSource (not Socket receive timeout alone). When the deadline elapses a ProxyTimeoutException with ResponseHeader is raised (and may be converted to HTTP 504 before any response bytes have been committed to the client). Default is 0 (disabled). WebSocket upgrades, Server-Sent Events, raw tunnels, and sessions that already wrote a response status to the client are exempt; those waits use IdleReadTimeoutSeconds when configured. Per-session override: ResponseHeaderTimeout. Declaration public int ResponseHeaderTimeoutSeconds { get; set; } Property Value Type Description int | Edit this page View Source ReuseSocket When true (default), SO_REUSEADDR is requested where Titanium.Web.Proxy.Helpers.RunTime.IsSocketReuseAvailable() reports support (always on Windows; on non-Windows, .NET Core 3+ / compatible runtimes). Declaration public bool ReuseSocket { get; set; } Property Value Type Description bool | Edit this page View Source ReverseProxy Optional reverse-proxy route/cluster configuration. When null (default), Core keeps 6.x ForwardHost behavior with zero added cost on the hot path. Declaration public ReverseProxyOptions? ReverseProxy { get; set; } Property Value Type Description ReverseProxyOptions | Edit this page View Source ServerConnectionCount Total number of active server connections (TCP plus upstream QUIC). For HTTP/3-only upstreams see Http3ServerConnectionCount. Declaration public int ServerConnectionCount { get; } Property Value Type Description int | Edit this page View Source ShouldInterceptHttp Optional per-request/stream predicate consulted only when the global interception gate is active. Return true to use the full SessionEventArgs path; return false to use the fast-forward path. null (the default) intercepts every request — preserving today's behavior. Declaration public Func? ShouldInterceptHttp { get; set; } Property Value Type Description Func | Edit this page View Source SupportedServerSslProtocols Ssl versions offered on outbound HTTPS connections to origins (and upstream proxies). Default None means “use SupportedSslProtocols” (typically TLS 1.2 and 1.3). Set an explicit mask to restrict or expand outbound-only independently of inbound client TLS. Older docs described None as “same as the proxy client.” That coupling is incorrect across protocol translations (e.g. inbound QUIC is always TLS 1.3 while outbound TCP SslStream on macOS SecureTransport cannot offer TLS 1.3). Declaration public SslProtocols SupportedServerSslProtocols { get; set; } Property Value Type Description SslProtocols | Edit this page View Source SupportedSslProtocols List of supported Ssl versions. Defaults to TLS 1.2/1.3 only as of 5.0 - a breaking change from 4.x, which also enabled SSL 3.0/TLS 1.0/1.1. Those legacy, broken-by-design protocols require an explicit opt-in by assigning this property directly (e.g. SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12 | SslProtocols.Tls13) if a legacy client/server genuinely requires them. Declaration public SslProtocols SupportedSslProtocols { get; set; } Property Value Type Description SslProtocols | Edit this page View Source TcpTimeWaitSeconds SO_LINGER timeout in seconds applied to client and upstream sockets via LingerOption (enabled with this timeout). This is not the kernel TCP TIME_WAIT duration — TIME_WAIT is controlled by the OS. A positive value means Close may block up to that many seconds flushing send buffers; use 0 for an abortive close (RST). Default is 0 so high-churn proxies avoid TIME_WAIT accumulation; the 1-second connection disposal delay already prefers peer-first close. Declaration public int TcpTimeWaitSeconds { get; set; } Property Value Type Description int | Edit this page View Source ThreadPoolWorkerThread Customize the minimum ThreadPool size (increase it on a server). Defaults to max(ProcessorCount * 2, 16) so short loopback/proxy workloads are not starved while the pool is still ramping workers. Declaration public int ThreadPoolWorkerThread { get; set; } Property Value Type Description int | Edit this page View Source UpStreamEndPoint Local adapter/NIC endpoint where proxy makes request via. Defaults via any IP addresses of this machine. When the resolved destination address family does not match this endpoint, it is ignored so dual-stack destinations can still connect (see UpStreamEndPointIPv4 / UpStreamEndPointIPv6). Declaration public IPEndPoint? UpStreamEndPoint { get; set; } Property Value Type Description IPEndPoint | Edit this page View Source UpStreamEndPointIPv4 Local bind endpoint used when the resolved upstream destination is IPv4. Takes precedence over UpStreamEndPoint for IPv4 destinations. Declaration public IPEndPoint? UpStreamEndPointIPv4 { get; set; } Property Value Type Description IPEndPoint | Edit this page View Source UpStreamEndPointIPv6 Local bind endpoint used when the resolved upstream destination is IPv6. Takes precedence over UpStreamEndPoint for IPv6 destinations. Declaration public IPEndPoint? UpStreamEndPointIPv6 { get; set; } Property Value Type Description IPEndPoint | Edit this page View Source UpStreamHttpProxy External proxy used for Http requests. Declaration public IExternalProxy? UpStreamHttpProxy { get; set; } Property Value Type Description IExternalProxy | Edit this page View Source UpStreamHttpsProxy External proxy used for Https requests. Declaration public IExternalProxy? UpStreamHttpsProxy { get; set; } Property Value Type Description IExternalProxy | Edit this page View Source UpstreamProxyConfigurationScript If set, the upstream proxy will be detected by a script that will be loaded from the provided Uri Declaration public Uri? UpstreamProxyConfigurationScript { get; set; } Property Value Type Description Uri | Edit this page View Source ViaHeaderPseudonym Pseudonym used in Via header fields appended to forwarded requests and responses (RFC 9110 §7.6.3). Defaults to DefaultViaHeaderPseudonym. Set to an empty string to disable Via header injection entirely. Loop detection uses this value: a request arriving with this pseudonym already present in Via is refused with 508 Loop Detected. Declaration public string ViaHeaderPseudonym { get; set; } Property Value Type Description string | Edit this page View Source WinAuthCredentialsProvider Optional per-session credential provider for server 401 WinAuth (NTLM/Negotiate/Kerberos). Return null to use the current process identity (legacy behavior). Do not put plaintext passwords on SessionEventArgs — use this callback instead. Windows SSPI only; ignored on non-Windows platforms. Declaration public Func>? WinAuthCredentialsProvider { get; set; } Property Value Type Description Func> Methods | Edit this page View Source AddEndPoint(ProxyEndPoint) Add a proxy end point. Declaration public void AddEndPoint(ProxyEndPoint endPoint) Parameters Type Name Description ProxyEndPoint endPoint The proxy endpoint. | Edit this page View Source ApplyLoggingConfiguration() Rebuilds the active logger/logger factory from the current Logging configuration, disposing any previously owned built-in providers. Called automatically from the constructor (with the default configuration) and from Start(bool). Call this explicitly any time after changing Logging and you want the change to take effect immediately - whether the proxy is stopped (e.g. before using CertificateManager directly) or already running. Declaration public void ApplyLoggingConfiguration() | Edit this page View Source ClearDecryptFailureBypass() Clears all learned decrypt-bypass entries. Declaration public void ClearDecryptFailureBypass() | Edit this page View Source DisableAllSystemProxies() Clear all proxy settings for current machine. Declaration public void DisableAllSystemProxies() | Edit this page View Source DisableSystemHttpProxy() Clear HTTP proxy settings of current machine. Declaration public void DisableSystemHttpProxy() | Edit this page View Source DisableSystemHttpsProxy() Clear HTTPS proxy settings of current machine. Declaration public void DisableSystemHttpsProxy() | Edit this page View Source DisableSystemProxy(ProxyProtocolType) Clear the specified proxy setting for current machine. Declaration public void DisableSystemProxy(ProxyProtocolType protocolType) Parameters Type Name Description ProxyProtocolType protocolType | Edit this page View Source Dispose() Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. Declaration public void Dispose() | Edit this page View Source Dispose(bool) Declaration [SuppressMessage(\"ApiDesign\", \"RS0016:Add public types and members to the declared API\", Justification = \"Protected Dispose(bool) is required by the standard IDisposable pattern but is not public API.\")] protected virtual void Dispose(bool disposing) Parameters Type Name Description bool disposing | Edit this page View Source ForceDecryptFailureBypass(string) Marks host as actively bypassed (same as a forced learn after origin TLS failure). Raises DecryptFailureBypassChanged when bypass newly becomes active. Declaration public bool ForceDecryptFailureBypass(string host) Parameters Type Name Description string host Returns Type Description bool | Edit this page View Source GetDecryptFailureBypassEntries() Snapshot of current learned decrypt-bypass entries (may include non-active strikes). Declaration public IReadOnlyList GetDecryptFailureBypassEntries() Returns Type Description IReadOnlyList | Edit this page View Source RemoveDecryptFailureBypass(string) Removes one host from the learned decrypt-bypass cache. Declaration public bool RemoveDecryptFailureBypass(string host) Parameters Type Name Description string host Returns Type Description bool | Edit this page View Source RemoveEndPoint(ProxyEndPoint) Remove a proxy end point. Will throw error if the end point doesn't exist. Declaration public void RemoveEndPoint(ProxyEndPoint endPoint) Parameters Type Name Description ProxyEndPoint endPoint The existing endpoint to remove. | Edit this page View Source RestoreOriginalProxySettings() Restores the original proxy settings. Declaration public void RestoreOriginalProxySettings() | Edit this page View Source SetAsSystemHttpProxy(ExplicitProxyEndPoint) Set the given explicit end point as the default proxy server for current machine. Declaration public void SetAsSystemHttpProxy(ExplicitProxyEndPoint endPoint) Parameters Type Name Description ExplicitProxyEndPoint endPoint The explicit endpoint. | Edit this page View Source SetAsSystemHttpProxy(ExplicitProxyEndPoint, SystemProxySettings) Set the given explicit end point as the default HTTP proxy server for current machine. Declaration public void SetAsSystemHttpProxy(ExplicitProxyEndPoint endPoint, SystemProxySettings settings) Parameters Type Name Description ExplicitProxyEndPoint endPoint The explicit endpoint. SystemProxySettings settings The Windows system proxy settings. | Edit this page View Source SetAsSystemHttpsProxy(ExplicitProxyEndPoint) Set the given explicit end point as the default proxy server for current machine. Declaration public void SetAsSystemHttpsProxy(ExplicitProxyEndPoint endPoint) Parameters Type Name Description ExplicitProxyEndPoint endPoint The explicit endpoint. | Edit this page View Source SetAsSystemHttpsProxy(ExplicitProxyEndPoint, SystemProxySettings) Set the given explicit end point as the default HTTPS proxy server for current machine. Declaration public void SetAsSystemHttpsProxy(ExplicitProxyEndPoint endPoint, SystemProxySettings settings) Parameters Type Name Description ExplicitProxyEndPoint endPoint The explicit endpoint. SystemProxySettings settings The Windows system proxy settings. | Edit this page View Source SetAsSystemProxy(ExplicitProxyEndPoint, ProxyProtocolType) Set the given explicit end point as the default proxy server for current machine. Declaration public void SetAsSystemProxy(ExplicitProxyEndPoint endPoint, ProxyProtocolType protocolType) Parameters Type Name Description ExplicitProxyEndPoint endPoint The explicit endpoint. ProxyProtocolType protocolType The proxy protocol type. | Edit this page View Source SetAsSystemProxy(ExplicitProxyEndPoint, ProxyProtocolType, SystemProxySettings?) Set the given explicit end point as the default proxy server for current machine. Declaration public void SetAsSystemProxy(ExplicitProxyEndPoint endPoint, ProxyProtocolType protocolType, SystemProxySettings? settings) Parameters Type Name Description ExplicitProxyEndPoint endPoint The explicit endpoint. ProxyProtocolType protocolType The proxy protocol type. SystemProxySettings settings The Windows system proxy settings, or null to preserve the current bypass list. | Edit this page View Source SetHttp3Enabled(bool) Enables or disables EnableHttp3. Enabling still requires MsQuic (IsSupported); disabling is always applied. Safe to call while the proxy is running — new origin connections pick up the change. Existing sessions keep the protocol they already negotiated. Declaration public bool SetHttp3Enabled(bool enabled) Parameters Type Name Description bool enabled Returns Type Description bool true when HTTP/3 is enabled after the call. | Edit this page View Source ShouldBypassDecryptForLearnedHost(string?) When EnableDecryptFailureBypass is on and host is actively bypassed, returns true (decrypt should be skipped). Declaration public bool ShouldBypassDecryptForLearnedHost(string? host) Parameters Type Name Description string host Returns Type Description bool | Edit this page View Source Start(bool) Start this proxy server instance. Transactional: if any endpoint fails to start, every listener this call already started is stopped, the system-upstream-proxy resolver (if this call created one) is disposed, and ProxyRunning is left false before the exception propagates. A caller that catches the exception is left with an instance in exactly the same state as before calling Start(bool), not a partially-bound proxy with some endpoints silently listening. Declaration public void Start(bool changeSystemProxySettings = true) Parameters Type Name Description bool changeSystemProxySettings Whether or not clear any system proxy settings which is pointing to our own endpoint (causing a cycle). E.g due to ungracious proxy shutdown before. | Edit this page View Source Stop() Stop this proxy server instance. Endpoints remain registered so Start(bool) can re-listen on the same ports. In-flight sessions are cancelled; pooled upstream connections are cleared. The connection factory itself stays usable for a subsequent Start (it is only disposed with the proxy). Declaration public void Stop() | Edit this page View Source StopAsync(TimeSpan?) Asynchronously stop this proxy server, cancel in-flight sessions, and wait briefly for client connection count to drain before clearing the upstream pool. Declaration public Task StopAsync(TimeSpan? drainTimeout = null) Parameters Type Name Description TimeSpan? drainTimeout Maximum time to wait for active client handlers to exit after cancellation. Defaults to 5 seconds. Returns Type Description Task | Edit this page View Source TryDisableAllSystemProxies() Clear all OS proxy settings without throwing. Declaration public SystemProxyChangeResult TryDisableAllSystemProxies() Returns Type Description SystemProxyChangeResult | Edit this page View Source TryDisableSystemProxy(ProxyProtocolType) Clear OS proxy for the given protocols without throwing. Declaration public SystemProxyChangeResult TryDisableSystemProxy(ProxyProtocolType protocolType) Parameters Type Name Description ProxyProtocolType protocolType Returns Type Description SystemProxyChangeResult | Edit this page View Source TryEnableHttp3IfSupported() Turns on EnableHttp3 when MsQuic is available (IsSupported). Hosts (CLI, Inspector, examples) should call this instead of setting EnableHttp3 blindly. Returns true when HTTP/3 was enabled. Declaration public bool TryEnableHttp3IfSupported() Returns Type Description bool | Edit this page View Source TryRestoreOriginalProxySettings() Restore OS proxy without throwing. Declaration public SystemProxyChangeResult TryRestoreOriginalProxySettings() Returns Type Description SystemProxyChangeResult | Edit this page View Source TrySetAsSystemProxy(ExplicitProxyEndPoint, ProxyProtocolType, SystemProxySettings?) Enable OS system proxy without throwing. Failures are logged and returned so Inspector/CLI can show a status message instead of crashing. Declaration public SystemProxyChangeResult TrySetAsSystemProxy(ExplicitProxyEndPoint endPoint, ProxyProtocolType protocolType, SystemProxySettings? settings = null) Parameters Type Name Description ExplicitProxyEndPoint endPoint ProxyProtocolType protocolType SystemProxySettings settings Returns Type Description SystemProxyChangeResult Events | Edit this page View Source AfterResponse Intercept after response event from server. Declaration public event AsyncEventHandler? AfterResponse Event Type Type Description AsyncEventHandler | Edit this page View Source BeforeRequest Intercept request event to server. Declaration public event AsyncEventHandler? BeforeRequest Event Type Type Description AsyncEventHandler | Edit this page View Source BeforeResponse Intercept response event from server. Declaration public event AsyncEventHandler? BeforeResponse Event Type Type Description AsyncEventHandler | Edit this page View Source BeforeUpStreamConnectRequest Intercept connect request sent to upstream proxy. Declaration public event AsyncEventHandler? BeforeUpStreamConnectRequest Event Type Type Description AsyncEventHandler | Edit this page View Source ClientCertificateSelectionCallback Event to override client certificate selection during mutual SSL authentication. Declaration public event AsyncEventHandler? ClientCertificateSelectionCallback Event Type Type Description AsyncEventHandler | Edit this page View Source ClientConnectionCountChanged Event occurs when client connection count changed. Declaration public event EventHandler? ClientConnectionCountChanged Event Type Type Description EventHandler | Edit this page View Source DecryptFailureBypassChanged Raised when a host becomes actively bypassed (threshold reached or same-CONNECT mark). Handlers must not block; Inspector marshals to the UI thread. Declaration public event EventHandler? DecryptFailureBypassChanged Event Type Type Description EventHandler | Edit this page View Source Http3ClientConnectionCountChanged Event occurs when inbound HTTP/3 client connection count changed. Declaration public event EventHandler? Http3ClientConnectionCountChanged Event Type Type Description EventHandler | Edit this page View Source Http3ServerConnectionCountChanged Event occurs when upstream HTTP/3 server connection count changed. Declaration public event EventHandler? Http3ServerConnectionCountChanged Event Type Type Description EventHandler | Edit this page View Source OnClientConnectionCreate Customize TcpClient used for client connection upon create. Declaration public event AsyncEventHandler? OnClientConnectionCreate Event Type Type Description AsyncEventHandler | Edit this page View Source OnRequestBodyWrite Intercept request body send event to server. Subscribe to inspect or modify the request body chunk-by-chunk as it streams to the server, without buffering the whole body. Do not combine with SessionEventArgs.GetRequestBody (which buffers). Declaration public event AsyncEventHandler? OnRequestBodyWrite Event Type Type Description AsyncEventHandler | Edit this page View Source OnResponseBodyWrite Intercept response body send event to client. Subscribe to inspect or modify the response body chunk-by-chunk as it streams to the client, without buffering the whole body. Do not combine with SessionEventArgs.GetResponseBody (which buffers). Declaration public event AsyncEventHandler? OnResponseBodyWrite Event Type Type Description AsyncEventHandler | Edit this page View Source OnServerConnectionCreate Customize TcpClient used for server connection upon create. Declaration public event AsyncEventHandler? OnServerConnectionCreate Event Type Type Description AsyncEventHandler | Edit this page View Source ServerCertificateValidationCallback Event to override the default verification logic of remote SSL certificate received during authentication. Declaration public event AsyncEventHandler? ServerCertificateValidationCallback Event Type Type Description AsyncEventHandler | Edit this page View Source ServerConnectionCountChanged Event occurs when server connection count changed. Declaration public event EventHandler? ServerConnectionCountChanged Event Type Type Description EventHandler Implements IDisposable"
+ "summary": "Class ProxyServer Translates an HTTP/1.1 client connection onto an h2-only origin (Http2 with AllowHttpProtocolTranslation enabled - see ResolveHttp2ForClientAsync(SessionEventArgsBase, bool, string, int, string, int?, UpstreamHttpProtocol, bool, bool, CancellationToken, bool)), leasing one h2 stream per HTTP/1.1 request from a shared Titanium.Web.Proxy.Http2.Http2OriginConnection via Titanium.Web.Proxy.ProxyServer.Http2OriginConnectionPool rather than opening a new TCP/TLS connection for every request. Inheritance object ProxyServer Implements IDisposable Inherited Members object.Equals(object) object.Equals(object, object) object.GetHashCode() object.GetType() object.MemberwiseClone() object.ReferenceEquals(object, object) object.ToString() Namespace: Titanium.Web.Proxy Assembly: Titanium.Web.Proxy.dll Syntax public class ProxyServer : IDisposable Remarks This re-implements the HTTP/1.1 client read loop (request line, headers, BeforeRequest, authorization, header preparation, CancelRequest/replaced-response handling) rather than reusing the private HandleHttpSessionRequest/HandleHttpSessionResponse methods, because those methods send/receive over TcpServerConnection.Stream using the raw HTTP/1.1 wire format, which an h2 origin connection cannot speak. This mirrors the precedent set by the h2-to-HTTP/1.1 bridge (Http2ToHttp11BridgeHandler), which similarly bypasses the wire-format-specific machinery for the leg that does not match it. Origin connections are multiplexed across independent HTTP/1.1 clients through Titanium.Web.Proxy.ProxyServer.Http2OriginConnectionPool (fan-in share). Response bodies are delivered via Titanium.Web.Proxy.Http2.Http2OriginConnection streaming writers where available. Constructors | Edit this page View Source ProxyServer(bool, bool, bool) Initializes a new instance of ProxyServer class with provided parameters. Declaration public ProxyServer(bool userTrustRootCertificate = true, bool machineTrustRootCertificate = false, bool trustRootCertificateAsAdmin = false) Parameters Type Name Description bool userTrustRootCertificate When true (the default), EnsureRootCertificate() installs the MITM root into the current-user Personal and Trusted Root stores. Prefer user-only trust for interactive apps; pass false when trust must be fully opt-in. bool machineTrustRootCertificate When true, also trust in the local-machine stores (needs elevation). Defaults to false — machine trust is opt-in for services/admin installs, not for normal desktop use. bool trustRootCertificateAsAdmin When true, attempt elevated trust via UAC (Windows only). Defaults to false. | Edit this page View Source ProxyServer(string?, string?, bool, bool, bool) Initializes a new instance of ProxyServer class with provided parameters. Declaration public ProxyServer(string? rootCertificateName, string? rootCertificateIssuerName, bool userTrustRootCertificate = true, bool machineTrustRootCertificate = false, bool trustRootCertificateAsAdmin = false) Parameters Type Name Description string rootCertificateName Name of the root certificate. string rootCertificateIssuerName Name of the root certificate issuer. bool userTrustRootCertificate When true (the default), EnsureRootCertificate() installs the MITM root into the current-user Personal and Trusted Root stores. Prefer user-only trust for interactive apps; pass false when trust must be fully opt-in. bool machineTrustRootCertificate When true, also trust in the local-machine stores (needs elevation). Defaults to false — machine trust is opt-in for services/admin installs, not for normal desktop use. bool trustRootCertificateAsAdmin When true, attempt elevated trust via UAC (Windows only). Defaults to false. Fields | Edit this page View Source DefaultViaHeaderPseudonym Default Via header pseudonym (RFC 9110 §7.6.3). Used by ViaHeaderPseudonym and by Inspector when Add Via header is enabled. Declaration public const string DefaultViaHeaderPseudonym = \"titanium-web-proxy\" Field Value Type Description string Properties | Edit this page View Source AdmittedClientConnectionCount Number of client connections currently admitted (accepted and past the admission gate, not yet finished being handled), across all TCP-based endpoints. Unlike ClientConnectionCount, this drops to zero as soon as the handler returns, without the trailing TIME_WAIT delay. Declaration public int AdmittedClientConnectionCount { get; } Property Value Type Description int | Edit this page View Source BlockLoopbackDestinations Outbound destination policy hook: when true, every resolved destination IP address is checked for loopback (127.0.0.0/8, ::1, including IPv4-mapped forms such as ::ffff:127.0.0.1) before connecting, and the connection attempt is rejected with an OutboundDestinationBlockedException if it matches. Private (RFC 1918), link-local, and cloud metadata addresses (including 169.254.169.254) remain reachable under this flag alone. Off by default. Use when the proxy must allow intranet destinations (for example 10.*) but must still stop clients from pivoting to the proxy host's own loopback. BlockPrivateNetworkDestinations already covers loopback as part of its broader set; when that flag is enabled, this property is not consulted for the reject. Assigning Profile does not read or clear this property. Enforced on TCP outbound connects only (the same call sites as BlockPrivateNetworkDestinations). Origin HTTP/3 (QUIC) dials a hostname and is not covered. An explicitly configured upstream proxy address is always exempt. Not currently enforced for a SOCKS upstream with ProxyDnsRequests enabled. Declaration public bool BlockLoopbackDestinations { get; set; } Property Value Type Description bool | Edit this page View Source BlockPrivateNetworkDestinations Outbound destination policy hook: when true, every resolved destination IP address is checked against loopback, private (RFC 1918/4193), link-local (which subsumes the 169.254.169.254 cloud metadata endpoint), and other non-globally-routable ranges before connecting, and the connection attempt is rejected with an OutboundDestinationBlockedException if it matches. Off by default: blocking private destinations would break this library's most common configurations, including upstream-proxy chaining to localhost and interception of local development servers. Only enable this when the proxy accepts requests from untrusted clients (an SSRF-relevant deployment), where those same destinations become an attacker-reachable pivot into the host's private network instead of an operator's own intentional configuration. An explicitly configured upstream proxy address (UpStreamHttpProxy, UpStreamHttpsProxy, or a per-session external proxy) is always exempt - that address is operator intent, not attacker-controlled. Checked against the resolved address actually used to connect (no re-resolution afterward, which would make the check a TOCTOU no-op against DNS rebinding). Not currently enforced for a SOCKS upstream with ProxyDnsRequests enabled, since the proxy never resolves the origin itself in that mode and has no address of its own to validate. Declaration public bool BlockPrivateNetworkDestinations { get; set; } Property Value Type Description bool | Edit this page View Source BufferPool The buffer pool used throughout this proxy instance. Set custom implementations by implementing this interface. By default this uses DefaultBufferPool implementation available in StreamExtended library package. Buffer size should be at least 10 bytes. Declaration public IBufferPool BufferPool { get; set; } Property Value Type Description IBufferPool | Edit this page View Source CertificateManager Manages certificates used by this proxy. Declaration public CertificateManager CertificateManager { get; } Property Value Type Description CertificateManager | Edit this page View Source CheckCertificateRevocation Should we check for certificate revocation during SSL authentication to servers Note: If enabled can reduce performance. Defaults to false. Declaration public X509RevocationMode CheckCertificateRevocation { get; set; } Property Value Type Description X509RevocationMode | Edit this page View Source ClientConnectionCount Total number of active TCP client connections. Does not include inbound HTTP/3 (QUIC) clients; see Http3ClientConnectionCount. Declaration public int ClientConnectionCount { get; } Property Value Type Description int | Edit this page View Source ClientHeaderTimeoutSeconds Seconds to wait for a client to finish sending the request line and headers, from the moment this proxy starts reading a new request on the connection. Enforced with a linked CancellationTokenSource around the request-line and header read, not Socket.ReceiveTimeout: that property only bounds a single blocking Receive call, not the asynchronous reads this proxy actually issues, so without this deadline a client that opens a connection and trickles bytes arbitrarily slowly (or stops sending entirely) after the first byte ties up a read loop indefinitely. Default is 0 (disabled), matching every other deadline in this class - no per-session override exists because there is no SessionEventArgs for this request yet at the point this deadline applies. Declaration public int ClientHeaderTimeoutSeconds { get; set; } Property Value Type Description int | Edit this page View Source CompatibilityMode100Continue When true, the proxy immediately responds with a synthetic 100 Continue to any client request carrying Expect: 100-continue, before forwarding the headers to the origin and without waiting for the origin to respond. This breaks the strict handshake (client → proxy 100 → client body → origin body) but prevents the deadlock that occurs with strict clients when Enable100ContinueBehaviour is false (the default). Has no effect when Enable100ContinueBehaviour is true. Default: false. Declaration public bool CompatibilityMode100Continue { get; set; } Property Value Type Description bool | Edit this page View Source ConnectTimeOutSeconds Seconds server connection are to wait for connection to be established. Default value is 20 seconds. Declaration public int ConnectTimeOutSeconds { get; set; } Property Value Type Description int | Edit this page View Source ConnectionTimeOutSeconds Seconds client/server connection are to be kept alive when waiting for read/write to complete. This will also determine the pool eviction time when connection pool is enabled. Default value is 60 seconds. Declaration public int ConnectionTimeOutSeconds { get; set; } Property Value Type Description int | Edit this page View Source CustomUpStreamProxyFailureFunc A callback to provide a chance for an upstream proxy failure to be handled by a new upstream proxy. User should return the ExternalProxy object with valid credentials or null. Declaration public Func>? CustomUpStreamProxyFailureFunc { get; set; } Property Value Type Description Func> | Edit this page View Source DecryptFailureBypassMaxEntries Maximum learned hosts retained (approximate LRU eviction). Default 256. Declaration public int DecryptFailureBypassMaxEntries { get; set; } Property Value Type Description int | Edit this page View Source DecryptFailureBypassThreshold Origin TLS failure strikes required before a host is bypassed on later CONNECTs. Same-CONNECT opaque fallback after an awaited H2 probe failure marks bypass immediately. Default 2. Declaration public int DecryptFailureBypassThreshold { get; set; } Property Value Type Description int | Edit this page View Source DecryptFailureBypassTtl How long a learned decrypt-bypass entry remains valid. Default 30 minutes. Declaration public TimeSpan DecryptFailureBypassTtl { get; set; } Property Value Type Description TimeSpan | Edit this page View Source DnsServerEndPoint DNS server endpoint used by Titanium.Web.Proxy.Http3.Dns.UdpSvcbDnsResolver for HTTPS/SVCB queries. Defaults to the first usable OS-configured plain-UDP DNS server discovered via NetworkInterface. This is a best-effort default and does not honor Windows NRPT, DoH, or VPN split-DNS policy. When no OS-configured DNS server can be discovered, the property reports 0.0.0.0:0 and proactive SVCB discovery is skipped (never falls back to a public third-party resolver). Assign an explicit endpoint to override discovery. Declaration [Experimental(\"TWP001\")] public IPEndPoint DnsServerEndPoint { get; set; } Property Value Type Description IPEndPoint | Edit this page View Source Enable100ContinueBehaviour Does this proxy uses the HTTP protocol 100 continue behaviour strictly? Broken 100 continue implementations on server/client may cause problems if enabled. Defaults to false. Declaration public bool Enable100ContinueBehaviour { get; set; } Property Value Type Description bool | Edit this page View Source EnableConnectionPool Should we enable the server connection pool. Defaults to true. When connection pooling is enabled, instead of creating a new TCP connection to the server for each client TCP connection, we check if an idle server connection is available in our cached pool. If a compatible connection (same destination, scheme, upstream proxy, credentials and negotiated protocol) created from an earlier request is available, we reuse it. Only connections that are safe to reuse under the HTTP protocol are pooled: the response body must be fully received and the connection must be persistent (HTTP/1.1 keep-alive, or an HTTP/1.0 connection that explicitly opted in via \"Connection: keep-alive\"). Connections whose response asked to close, that failed, or that carry connection-oriented authentication state (WinAuth NTLM/Negotiate) or a per-session client certificate are never returned to the shared pool. The ConnectionTimeOutSeconds parameter determines the eviction time for inactive server connections. This reduces TCP (and TLS) connection establishment cost, both in wall clock time and CPU cycles. Set to false to force a fresh server connection for every client connection. Declaration public bool EnableConnectionPool { get; set; } Property Value Type Description bool | Edit this page View Source EnableDecryptFailureBypass When true, the proxy learns hosts whose origin TLS handshake fails under MITM (non-ALPN AuthenticationException, typically bot / TLS-fingerprint rejection) and tunnels subsequent CONNECTs without decrypt. Default false so library and RPS baselines are unchanged. Inspector enables this by default. Success-path cost when on is one dictionary lookup per CONNECT. Declaration public bool EnableDecryptFailureBypass { get; set; } Property Value Type Description bool | Edit this page View Source EnableHttp2 Enable disable HTTP/2 support. Client-facing HTTP/2 is negotiated via TLS ALPN, or as prior-knowledge cleartext h2c on a transparent reverse endpoint (DecryptSsl: false). No Upgrade: h2c. Origin-facing HTTP/2 uses TLS ALPN h2 by default; with ForwardCleartext and Http2, the origin speaks cleartext HTTP/2 prior-knowledge (outbound h2c). A client/server that does not support HTTP/2 transparently falls back to HTTP/1.1 when policy allows. Request/response header and body modification in BeforeRequest/BeforeResponse, chunked trailers, interim (1xx) responses, and the synthetic-response APIs (Ok/Respond/Redirect/GenericResponse/ RespondStreaming) are all supported over HTTP/2, the same as over HTTP/1.x. Not supported: HTTP/2 server push (the wire frames are transcoded but there is no public API to originate a push) and Upgrade: h2c. Explicit-proxy inbound h2c is not implemented. See the protocol support matrix on the wiki for exact, up-to-date HTTP/1.x/HTTP/2 feature coverage. Declaration public bool EnableHttp2 { get; set; } Property Value Type Description bool | Edit this page View Source EnableHttp3 Enable HTTP/3 (QUIC) support. When true: Any TransparentQuicProxyEndPoint is started as a UDP-only QUIC listener for transparent/NAT HTTP/3 interception. Any TransparentProxyEndPoint with EnableHttp3 also listens for HTTP/3 on the same IP:port (TCP H1/H2 + UDP H3) and injects client-facing Alt-Svc. With Auto (default), a cached Alt-Svc / HTTPS/SVCB capability selects outbound HTTP/3 on the next CONNECT or new HTTP/1.1 request. Background QUIC warm-up starts when the cache is filled so that handshake is often already done. An already-open H2↔H2 MITM session is not upgraded mid-connection. Forced Http3 fails closed with no TCP fallback. Requires MsQuic native library and a supported operating-system version (IsSupported). Setting to true with no inbound HTTP/3 endpoint is fine when an explicit/SOCKS/transparent TCP endpoint is present (origin-side QUIC only). A warning is emitted only when EnableHttp3 is set with no client-facing endpoints at all. Default: false (opt-in). Experimental: HTTP/3 support has not yet completed the full interop/soak/fuzz gate process. Suppress TWP001 to opt in; the attribute is removed when the feature graduates to stable. Declaration [Experimental(\"TWP001\")] public bool EnableHttp3 { get; set; } Property Value Type Description bool | Edit this page View Source EnableHttpInterception Forces the full interception path (SessionEventArgs, BeforeRequest, etc.) even when no event handlers are subscribed. Set this when consuming SessionEventArgs for timing or metrics without subscribing to any event. Default: false. Declaration public bool EnableHttpInterception { get; set; } Property Value Type Description bool | Edit this page View Source EnableHttpsSvcbDnsDiscovery When true, the proxy queues a background HTTPS/SVCB RR (DNS type 65) lookup after an Auto-mode capability-cache miss. A positive result (ALPN h3 found) warms Titanium.Web.Proxy.Http3.Http3OriginCapabilityCache for subsequent connections; the CONNECT / request path itself never awaits DNS. Negative results are cached for 1 minute; transient failures use a short backoff. Defaults to true whenever EnableHttp3 is true. Set explicitly to false to disable discovery even when HTTP/3 is enabled — for example, when the configured DNS server is untrusted or unreachable. First-connection HTTP/3 adoption then comes from Alt-Svc. Declaration [Experimental(\"TWP001\")] public bool EnableHttpsSvcbDnsDiscovery { get; set; } Property Value Type Description bool | Edit this page View Source EnableIpv6UnreachableSoftSkip When true (default), after one IPv6 connect failure with NetworkUnreachable (or equivalent), temporarily omit IPv6 addresses from the Happy Eyeballs race for 30 seconds. Reduces first-chance SocketException noise on dual-stack hosts with a broken IPv6 path. Disable if operators require strict IPv6 preference even when the path is unreachable. Declaration public bool EnableIpv6UnreachableSoftSkip { get; set; } Property Value Type Description bool | Edit this page View Source EnableQpackDynamicTable When true, enables RFC 9204 QPACK dynamic table encoding and decoding for inbound HTTP/3 connections. Each connection gets its own Titanium.Web.Proxy.Http3.Qpack.QpackContext with two independent 4096-byte tables (one inbound, one outbound). Defaults to false (static-table-only); existing deployments are unaffected. Declaration [Experimental(\"TWP001\")] public bool EnableQpackDynamicTable { get; set; } Property Value Type Description bool | Edit this page View Source EnableRequestTimingCapture Enables structured request/connection timing capture. When false (the default) no timing objects are allocated and no UtcNow calls are made for timing purposes anywhere in the proxy, so there is zero overhead on the hot path. When enabled, every SessionEventArgsBase exposes a populated Timing (per-request phases: client header read, connection wait, request send, time-to-first-byte, response delivery, total), every upstream connection exposes a populated UpstreamConnectionTiming (reachable from a session via UpstreamConnectionTiming, describing DNS, TCP connect, optional upstream-proxy CONNECT, and TLS handshake durations), and a decrypted TunnelConnectSessionEventArgs exposes the client-facing TLS handshake duration via ClientTlsTiming. Can be toggled at any time; it only affects sessions/connections created after the change, never mutating timing objects already handed out. Defaults to false. Declaration public bool EnableRequestTimingCapture { get; set; } Property Value Type Description bool | Edit this page View Source EnableRfc8441 When true, the proxy enables RFC 8441 WebSocket-over-HTTP/2: Accepts extended CONNECT (:protocol = websocket) from h2 clients and advertises SETTINGS_ENABLE_CONNECT_PROTOCOL=1 to them. Per origin: if the origin is HTTP/2 and advertises RFC 8441 support, DATA frames are relayed directly; if the origin is HTTP/2 and does not, the stream is reset with REFUSED_STREAM; if the origin is HTTP/1.1, the h2→h1 WebSocket upgrade bridge is used. On the HTTP/1.1-client-to-h2-origin translation bridge, translates Upgrade: websocket into extended CONNECT when the origin advertises the setting; otherwise falls back to a dedicated HTTP/1.1 origin connection for that WebSocket. When this property is false, that bridge still returns synthetic 501 Not Implemented for WebSocket upgrades (historical default). Default: false (must opt-in). Declaration public bool EnableRfc8441 { get; set; } Property Value Type Description bool | Edit this page View Source EnableTcpKeepAlive Enable TCP KeepAlive on client and server sockets so NAT/firewall mappings for long-lived CONNECT tunnels are refreshed. Default: true. Declaration public bool EnableTcpKeepAlive { get; set; } Property Value Type Description bool | Edit this page View Source EnableTcpServerConnectionPrefetch Should we enable tcp server connection prefetching? When enabled, as soon as we receive a client connection we concurrently initiate corresponding server connection process using CONNECT hostname or SNI hostname on a separate task so that after parsing client request we will have the server connection immediately ready or in the process of getting ready. If a server connection is available in cache then this prefetch task will immediately return with the available connection from cache. Defaults to true. Declaration public bool EnableTcpServerConnectionPrefetch { get; set; } Property Value Type Description bool | Edit this page View Source EnableWinAuth Enable disable Windows Authentication (NTLM/Kerberos). By default SSPI uses the process identity. To authenticate as another user, set WinAuthCredentialsProvider (issue #461). Defaults to false. Declaration public bool EnableWinAuth { get; set; } Property Value Type Description bool | Edit this page View Source EndpointAdmissionRejectionCount Total number of client connections rejected by any endpoint's MaxConcurrentClients since this instance was created. Declaration public long EndpointAdmissionRejectionCount { get; } Property Value Type Description long | Edit this page View Source ForwardToUpstreamGateway Gets or sets a value indicating whether requests will be chained to upstream gateway. Defaults to false. Declaration public bool ForwardToUpstreamGateway { get; set; } Property Value Type Description bool | Edit this page View Source GetCustomUpStreamProxyFunc A callback to provide authentication credentials for up stream proxy this proxy is using for HTTP(S) requests. User should return the ExternalProxy object with valid credentials. Declaration public Func>? GetCustomUpStreamProxyFunc { get; set; } Property Value Type Description Func> | Edit this page View Source GlobalAdmissionRejectionCount Total number of client connections rejected by MaxConcurrentClientConnections since this instance was created. Declaration public long GlobalAdmissionRejectionCount { get; } Property Value Type Description long | Edit this page View Source Http3ClientConnectionCount Total number of active inbound HTTP/3 (QUIC) client connections. Declaration public int Http3ClientConnectionCount { get; } Property Value Type Description int | Edit this page View Source Http3ServerConnectionCount Total number of active upstream HTTP/3 (QUIC) server connections. These are also included in ServerConnectionCount. Declaration public int Http3ServerConnectionCount { get; } Property Value Type Description int | Edit this page View Source HttpResponseCacheFiller Fills the GET/HEAD response cache from a session that is already on the interception path. Does not subscribe BeforeResponse or AfterResponse, so it does not turn NeedsHttpInterception(ProxyEndPoint) on. Declaration public Action? HttpResponseCacheFiller { get; set; } Property Value Type Description Action | Edit this page View Source IdleReadTimeoutSeconds Seconds of idle time allowed while reading from the origin (stalled header/body waits). Applied via CancelAfter on the active read operation. Default is 0 (disabled). Per-session override: IdleReadTimeout. Declaration public int IdleReadTimeoutSeconds { get; set; } Property Value Type Description int | Edit this page View Source IdleWriteTimeoutSeconds Seconds of idle time allowed while writing to the origin (stalled header/body waits). Applied via CancelAfter on the active write operation. Default is 0 (disabled). Per-session override: IdleWriteTimeout. Declaration public int IdleWriteTimeoutSeconds { get; set; } Property Value Type Description int | Edit this page View Source IgnoreServerCertificateErrors When true, origin TLS certificates that fail OS chain validation are still accepted (MITM of loopback/self-signed/private CAs). Inspector's \"Ignore server certificate errors\" maps here. Default false. A subscribed ServerCertificateValidationCallback still wins. Declaration public bool IgnoreServerCertificateErrors { get; set; } Property Value Type Description bool | Edit this page View Source ListenerBackLog TCP listener accept backlog. Default: 1024 for burst connection handling. Declaration public int ListenerBackLog { get; set; } Property Value Type Description int | Edit this page View Source Logger The live, shared logger used throughout this proxy instance. Reflects the most recent call to ApplyLoggingConfiguration(). Declaration public ILogger Logger { get; } Property Value Type Description ILogger | Edit this page View Source Logging Configuration for this proxy instance's built-in diagnostic logging - the replacement for the removed ExceptionFunc callback. Every exception the proxy catches (even when handled internally and never surfaced to user code) is reported through this logger at an appropriate severity; see ProxyLoggingOptions for the console/file sinks, enable/disable switch, and minimum level. Mutate the returned instance (or assign a new one) at any point; each assignment/mutation you want to take effect must be followed by ApplyLoggingConfiguration() (which Start(bool) also calls automatically, so the configuration active at the moment the proxy starts running is picked up for the run even if you never call it yourself). Calling it again later - including while the proxy is already running - immediately swaps in the new configuration; this is safe because logging never blocks or otherwise affects proxy traffic. Declaration public ProxyLoggingOptions Logging { get; set; } Property Value Type Description ProxyLoggingOptions | Edit this page View Source MaxBufferedBodyBytes Maximum bytes the proxy will buffer for a single request or response body when body buffering is required (body-read hooks, authentication retry, etc.). Bodies larger than this limit are rejected with 413 (upstream request) or connection teardown (upstream response). Set to 0 to disable the limit (not recommended). Default: 4,194,304 (4 MiB). Declaration public int MaxBufferedBodyBytes { get; set; } Property Value Type Description int | Edit this page View Source MaxCachedConnections Maximum number of concurrent connections per remote host in cache. Only meaningful when EnableConnectionPool is true; to disable pooling, set EnableConnectionPool to false rather than setting this to 0 - the pool eviction loop treats a value below 1 as \"evict without limit while holding the pool-wide lock\", which spins indefinitely once the cache for that host is empty and would stall every other connection acquire/release in the process. Rejected outright at assignment so that state cannot be reached. Default value is 128. Declaration public int MaxCachedConnections { get; set; } Property Value Type Description int Exceptions Type Condition ArgumentOutOfRangeException The assigned value is less than 1. | Edit this page View Source MaxConcurrentClientConnections Maximum number of client connections admitted across all TCP-based endpoints at once. null (the default) disables the global admission gate, preserving today's unbounded behavior. When set, a connection beyond this limit is rejected and disposed immediately after accept, before a handler task is even started. Enforced independently of ClientConnectionCount: see Titanium.Web.Proxy.ProxyServer.admittedClientConnectionCount for why. See also MaxConcurrentClients for a per-endpoint cap layered on top of this global one. Declaration public int? MaxConcurrentClientConnections { get; set; } Property Value Type Description int? | Edit this page View Source MaxConcurrentHttp11HttpsOriginCreates Caps concurrent new HTTPS origin TCP/TLS opens on the H2→H1 bridge only (MITM / re-encrypt). Pool hits (warm keep-alive) are uncapped. Cleartext H1 origins are not gated. Default is Clamp(ProcessorCount, 4, 32). Set before the first H2→H1 HTTPS origin open (typically before Start(bool)); changing the value after the create gate has been used has no effect on the live semaphore. Declaration public int MaxConcurrentHttp11HttpsOriginCreates { get; set; } Property Value Type Description int Exceptions Type Condition ArgumentOutOfRangeException The assigned value is less than 1. | Edit this page View Source MaxDecodedHeaderListBytes Maximum decoded HTTP/2 header list size in bytes, using RFC 7541 accounting (name.Length + value.Length + 32 per field). Requests or responses with a decoded header list exceeding this limit will be refused with RST_STREAM(ENHANCE_YOUR_CALM) (code 0xb). Set to 0 to disable the limit (not recommended). Default: 65,536 (64 KiB). Advertised via SETTINGS_MAX_HEADER_LIST_SIZE. Declaration public int MaxDecodedHeaderListBytes { get; set; } Property Value Type Description int | Edit this page View Source MaxWebSocketFramePayloadBytes Maximum WebSocket frame payload size in bytes that the proxy will accept during frame-level interception (i.e. when BeforeWebSocketFrame has at least one subscriber). Frames whose decoded payload exceeds this limit cause the WebSocket connection to be closed with Close code 1009 (Message Too Big). Raw-relay sessions (no BeforeWebSocketFrame subscriber) bypass this check entirely and pass all frames through unvalidated. Default: 16,777,216 (16 MiB). Declaration public int MaxWebSocketFramePayloadBytes { get; set; } Property Value Type Description int | Edit this page View Source NetworkFailureRetryAttempts Number of times to retry upon network failures when connection pool is enabled. Declaration public int NetworkFailureRetryAttempts { get; set; } Property Value Type Description int | Edit this page View Source NoDelay Gets or sets a Boolean value that specifies whether server and client stream Sockets are using the Nagle algorithm. Defaults to true, no nagle algorithm is used. Declaration public bool NoDelay { get; set; } Property Value Type Description bool | Edit this page View Source OriginHttpVersionPolicy Controls which HTTP version is declared to the origin server on the request line, independently of the version the client declared to the proxy. Defaults to PreserveClientVersion, which matches the proxy's historical pass-through behavior exactly. Set to NormalizeToHttp11 to let HTTP/1.0 clients share pooled, persistent origin connections the same way HTTP/1.1 clients already do. This only changes the wire version written to the origin request line - it never changes the client-facing Http.Request.HttpVersion that event handlers observe, nor the version/persistence used to write the response back to the client. Declaration public OriginHttpVersionPolicy OriginHttpVersionPolicy { get; set; } Property Value Type Description OriginHttpVersionPolicy | Edit this page View Source PolicyModes Which resource-bound PolicyFamily is enforced, observed, or disabled, per the plan's rollout section. Read live by each family's enforcement call site - not baked into a per-request snapshot at connection accept time - so assigning a new value here (a whole-object replacement, never a mutation of the previous instance) takes effect for the next check any in-flight or new request makes, without restarting the proxy. This is the \"runtime switch to drop to Observe without redeploying\" the plan requires; see WithAllObservedExceptDisabled() for the one-call way to do that. Defaults to Balanced's modes (resource families enforced, Http2RelayValidation disabled so compressed H2 relay stays the verbatim-HPACK fast path). AllEnforce additionally enforces relay validation and is what PublicFacing applies. Assigning Profile also replaces this value with that profile's bundle; assign PolicyModes afterward to deviate from the selected profile's modes without changing anything else the profile set. Declaration public ProxyPolicyModes PolicyModes { get; set; } Property Value Type Description ProxyPolicyModes | Edit this page View Source Profile The last profile applied via this property's setter, defaulting to Balanced - the profile every field on this instance already starts at, so a fresh new ProxyServer() reports Balanced without needing its setter to run once at construction time. Assigning this property applies its entire ProxyProfileSettings bundle - ResourceLimits, PolicyModes, SupportedSslProtocols, BlockPrivateNetworkDestinations, MaxConcurrentClientConnections and the deadline-seconds properties - as a single atomic assignment, so a reader can never observe a half-applied profile. Assigning any of those properties individually afterward overrides just that one, without reverting the rest of the profile's bundle. Logged once per Start(bool) call, by name only - never with hosts, URLs or secrets, per the plan's rollout section. Declaration public ProxyProfile Profile { get; set; } Property Value Type Description ProxyProfile | Edit this page View Source ProxyAuthenticationRealm Realm used during Proxy Basic Authentication. Declaration public string ProxyAuthenticationRealm { get; set; } Property Value Type Description string | Edit this page View Source ProxyAuthenticationSchemes A collection of scheme types, e.g. basic, NTLM, Kerberos, Negotiate, to return if scheme authentication is required. Works in relation with ProxySchemeAuthenticateFunc. Declaration public IEnumerable ProxyAuthenticationSchemes { get; set; } Property Value Type Description IEnumerable | Edit this page View Source ProxyBasicAuthenticateFunc A callback to authenticate proxy clients via basic authentication. Parameters are username and password as provided by client. Should return true for successful authentication. Declaration public Func>? ProxyBasicAuthenticateFunc { get; set; } Property Value Type Description Func> | Edit this page View Source ProxyEndPoints A list of IpAddress and port this proxy is listening to. Declaration public List ProxyEndPoints { get; set; } Property Value Type Description List | Edit this page View Source ProxyRunning Is the proxy currently running? Declaration public bool ProxyRunning { get; } Property Value Type Description bool | Edit this page View Source ProxySchemeAuthenticateFunc A pluggable callback to authenticate clients by scheme instead of requiring basic authentication through ProxyBasicAuthenticateFunc. Parameters are current working session, schemeType, and token as provided by a calling client. Should return success for successful authentication, continuation if the package requests, or failure. Declaration public Func>? ProxySchemeAuthenticateFunc { get; set; } Property Value Type Description Func> | Edit this page View Source RequestTimeoutSeconds Total seconds allowed for a single request/response exchange after BeforeRequest returns (connect, send, wait for headers, and body copy). Default is 0 (disabled). Per-session override: RequestTimeout. Declaration public int RequestTimeoutSeconds { get; set; } Property Value Type Description int | Edit this page View Source ResourceLimits The shared, immutable resource-bound snapshot (concurrent-stream cap, CONTINUATION frame-count/wall-clock bounds, peer-initiated incomplete-stream-reset budget, and the other limits described in ProxyResourceLimits) consulted by the HTTP/2 relay so a single proxy-owned value governs both what is enforced and what is advertised to each peer, rather than admitting purely against whatever the origin advertised. Assign a new ProxyResourceLimits (constructed via Create(long, int, long, long?, long?, double?, int?, int, int?, int, TimeSpan, bool, int, int?)) to override the Default snapshot. There is no artificial upper clamp: high-CPU/RAM hosts may pass larger maxCachedConnectionsPerHost, maxConcurrentStreamsPerConnection, etc. as needed. The live TCP pool depth knob MaxCachedConnections remains independently settable and should usually be kept in sync with MaxCachedConnectionsPerHost. Declaration public ProxyResourceLimits ResourceLimits { get; set; } Property Value Type Description ProxyResourceLimits | Edit this page View Source ResponseHeaderContributor Adds response headers on the way to the client without subscribing BeforeResponse. Plus CORS uses this so a listener can stay on H1 terminate-lite. Declaration public Action? ResponseHeaderContributor { get; set; } Property Value Type Description Action | Edit this page View Source ResponseHeaderTimeoutSeconds Seconds to wait for the origin to send the response status line and headers after the request has been sent. Enforced with a linked CancellationTokenSource (not Socket receive timeout alone). When the deadline elapses a ProxyTimeoutException with ResponseHeader is raised (and may be converted to HTTP 504 before any response bytes have been committed to the client). Default is 0 (disabled). WebSocket upgrades, Server-Sent Events, raw tunnels, and sessions that already wrote a response status to the client are exempt; those waits use IdleReadTimeoutSeconds when configured. Per-session override: ResponseHeaderTimeout. Declaration public int ResponseHeaderTimeoutSeconds { get; set; } Property Value Type Description int | Edit this page View Source ReuseSocket When true (default), SO_REUSEADDR is requested where Titanium.Web.Proxy.Helpers.RunTime.IsSocketReuseAvailable() reports support (always on Windows; on non-Windows, .NET Core 3+ / compatible runtimes). Declaration public bool ReuseSocket { get; set; } Property Value Type Description bool | Edit this page View Source ReverseProxy Optional reverse-proxy route/cluster configuration. When null (default), Core keeps 6.x ForwardHost behavior with zero added cost on the hot path. Declaration public ReverseProxyOptions? ReverseProxy { get; set; } Property Value Type Description ReverseProxyOptions | Edit this page View Source ServerConnectionCount Total number of active server connections (TCP plus upstream QUIC). For HTTP/3-only upstreams see Http3ServerConnectionCount. Declaration public int ServerConnectionCount { get; } Property Value Type Description int | Edit this page View Source ShouldInterceptHttp Optional per-request/stream predicate consulted only when the global interception gate is active. Return true to use the full SessionEventArgs path; return false to use the fast-forward path. null (the default) intercepts every request — preserving today's behavior. Declaration public Func? ShouldInterceptHttp { get; set; } Property Value Type Description Func | Edit this page View Source SupportedServerSslProtocols Ssl versions offered on outbound HTTPS connections to origins (and upstream proxies). Default None means “use SupportedSslProtocols” (typically TLS 1.2 and 1.3). Set an explicit mask to restrict or expand outbound-only independently of inbound client TLS. Older docs described None as “same as the proxy client.” That coupling is incorrect across protocol translations (e.g. inbound QUIC is always TLS 1.3 while outbound TCP SslStream on macOS SecureTransport cannot offer TLS 1.3). Declaration public SslProtocols SupportedServerSslProtocols { get; set; } Property Value Type Description SslProtocols | Edit this page View Source SupportedSslProtocols List of supported Ssl versions. Defaults to TLS 1.2/1.3 only as of 5.0 - a breaking change from 4.x, which also enabled SSL 3.0/TLS 1.0/1.1. Those legacy, broken-by-design protocols require an explicit opt-in by assigning this property directly (e.g. SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12 | SslProtocols.Tls13) if a legacy client/server genuinely requires them. Declaration public SslProtocols SupportedSslProtocols { get; set; } Property Value Type Description SslProtocols | Edit this page View Source TcpTimeWaitSeconds SO_LINGER timeout in seconds applied to client and upstream sockets via LingerOption (enabled with this timeout). This is not the kernel TCP TIME_WAIT duration — TIME_WAIT is controlled by the OS. A positive value means Close may block up to that many seconds flushing send buffers; use 0 for an abortive close (RST). Default is 0 so high-churn proxies avoid TIME_WAIT accumulation; the 1-second connection disposal delay already prefers peer-first close. Declaration public int TcpTimeWaitSeconds { get; set; } Property Value Type Description int | Edit this page View Source ThreadPoolWorkerThread Customize the minimum ThreadPool size (increase it on a server). Defaults to max(ProcessorCount * 2, 16) so short loopback/proxy workloads are not starved while the pool is still ramping workers. Declaration public int ThreadPoolWorkerThread { get; set; } Property Value Type Description int | Edit this page View Source UpStreamEndPoint Local adapter/NIC endpoint where proxy makes request via. Defaults via any IP addresses of this machine. When the resolved destination address family does not match this endpoint, it is ignored so dual-stack destinations can still connect (see UpStreamEndPointIPv4 / UpStreamEndPointIPv6). Declaration public IPEndPoint? UpStreamEndPoint { get; set; } Property Value Type Description IPEndPoint | Edit this page View Source UpStreamEndPointIPv4 Local bind endpoint used when the resolved upstream destination is IPv4. Takes precedence over UpStreamEndPoint for IPv4 destinations. Declaration public IPEndPoint? UpStreamEndPointIPv4 { get; set; } Property Value Type Description IPEndPoint | Edit this page View Source UpStreamEndPointIPv6 Local bind endpoint used when the resolved upstream destination is IPv6. Takes precedence over UpStreamEndPoint for IPv6 destinations. Declaration public IPEndPoint? UpStreamEndPointIPv6 { get; set; } Property Value Type Description IPEndPoint | Edit this page View Source UpStreamHttpProxy External proxy used for Http requests. Declaration public IExternalProxy? UpStreamHttpProxy { get; set; } Property Value Type Description IExternalProxy | Edit this page View Source UpStreamHttpsProxy External proxy used for Https requests. Declaration public IExternalProxy? UpStreamHttpsProxy { get; set; } Property Value Type Description IExternalProxy | Edit this page View Source UpstreamProxyConfigurationScript If set, the upstream proxy will be detected by a script that will be loaded from the provided Uri Declaration public Uri? UpstreamProxyConfigurationScript { get; set; } Property Value Type Description Uri | Edit this page View Source ViaHeaderPseudonym Pseudonym used in Via header fields appended to forwarded requests and responses (RFC 9110 §7.6.3). Defaults to DefaultViaHeaderPseudonym. Set to an empty string to disable Via header injection entirely. Loop detection uses this value: a request arriving with this pseudonym already present in Via is refused with 508 Loop Detected. Declaration public string ViaHeaderPseudonym { get; set; } Property Value Type Description string | Edit this page View Source WinAuthCredentialsProvider Optional per-session credential provider for server 401 WinAuth (NTLM/Negotiate/Kerberos). Return null to use the current process identity (legacy behavior). Do not put plaintext passwords on SessionEventArgs — use this callback instead. Windows SSPI only; ignored on non-Windows platforms. Declaration public Func>? WinAuthCredentialsProvider { get; set; } Property Value Type Description Func> Methods | Edit this page View Source AddEndPoint(ProxyEndPoint) Add a proxy end point. Declaration public void AddEndPoint(ProxyEndPoint endPoint) Parameters Type Name Description ProxyEndPoint endPoint The proxy endpoint. | Edit this page View Source ApplyLoggingConfiguration() Rebuilds the active logger/logger factory from the current Logging configuration, disposing any previously owned built-in providers. Called automatically from the constructor (with the default configuration) and from Start(bool). Call this explicitly any time after changing Logging and you want the change to take effect immediately - whether the proxy is stopped (e.g. before using CertificateManager directly) or already running. Declaration public void ApplyLoggingConfiguration() | Edit this page View Source ClearDecryptFailureBypass() Clears all learned decrypt-bypass entries. Declaration public void ClearDecryptFailureBypass() | Edit this page View Source DisableAllSystemProxies() Clear all proxy settings for current machine. Declaration public void DisableAllSystemProxies() | Edit this page View Source DisableSystemHttpProxy() Clear HTTP proxy settings of current machine. Declaration public void DisableSystemHttpProxy() | Edit this page View Source DisableSystemHttpsProxy() Clear HTTPS proxy settings of current machine. Declaration public void DisableSystemHttpsProxy() | Edit this page View Source DisableSystemProxy(ProxyProtocolType) Clear the specified proxy setting for current machine. Declaration public void DisableSystemProxy(ProxyProtocolType protocolType) Parameters Type Name Description ProxyProtocolType protocolType | Edit this page View Source Dispose() Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. Declaration public void Dispose() | Edit this page View Source Dispose(bool) Declaration [SuppressMessage(\"ApiDesign\", \"RS0016:Add public types and members to the declared API\", Justification = \"Protected Dispose(bool) is required by the standard IDisposable pattern but is not public API.\")] protected virtual void Dispose(bool disposing) Parameters Type Name Description bool disposing | Edit this page View Source ForceDecryptFailureBypass(string) Marks host as actively bypassed (same as a forced learn after origin TLS failure). Raises DecryptFailureBypassChanged when bypass newly becomes active. Declaration public bool ForceDecryptFailureBypass(string host) Parameters Type Name Description string host Returns Type Description bool | Edit this page View Source GetDecryptFailureBypassEntries() Snapshot of current learned decrypt-bypass entries (may include non-active strikes). Declaration public IReadOnlyList GetDecryptFailureBypassEntries() Returns Type Description IReadOnlyList | Edit this page View Source RemoveDecryptFailureBypass(string) Removes one host from the learned decrypt-bypass cache. Declaration public bool RemoveDecryptFailureBypass(string host) Parameters Type Name Description string host Returns Type Description bool | Edit this page View Source RemoveEndPoint(ProxyEndPoint) Remove a proxy end point. Will throw error if the end point doesn't exist. Declaration public void RemoveEndPoint(ProxyEndPoint endPoint) Parameters Type Name Description ProxyEndPoint endPoint The existing endpoint to remove. | Edit this page View Source RestoreOriginalProxySettings() Restores the original proxy settings. Declaration public void RestoreOriginalProxySettings() | Edit this page View Source SetAsSystemHttpProxy(ExplicitProxyEndPoint) Set the given explicit end point as the default proxy server for current machine. Declaration public void SetAsSystemHttpProxy(ExplicitProxyEndPoint endPoint) Parameters Type Name Description ExplicitProxyEndPoint endPoint The explicit endpoint. | Edit this page View Source SetAsSystemHttpProxy(ExplicitProxyEndPoint, SystemProxySettings) Set the given explicit end point as the default HTTP proxy server for current machine. Declaration public void SetAsSystemHttpProxy(ExplicitProxyEndPoint endPoint, SystemProxySettings settings) Parameters Type Name Description ExplicitProxyEndPoint endPoint The explicit endpoint. SystemProxySettings settings The Windows system proxy settings. | Edit this page View Source SetAsSystemHttpsProxy(ExplicitProxyEndPoint) Set the given explicit end point as the default proxy server for current machine. Declaration public void SetAsSystemHttpsProxy(ExplicitProxyEndPoint endPoint) Parameters Type Name Description ExplicitProxyEndPoint endPoint The explicit endpoint. | Edit this page View Source SetAsSystemHttpsProxy(ExplicitProxyEndPoint, SystemProxySettings) Set the given explicit end point as the default HTTPS proxy server for current machine. Declaration public void SetAsSystemHttpsProxy(ExplicitProxyEndPoint endPoint, SystemProxySettings settings) Parameters Type Name Description ExplicitProxyEndPoint endPoint The explicit endpoint. SystemProxySettings settings The Windows system proxy settings. | Edit this page View Source SetAsSystemProxy(ExplicitProxyEndPoint, ProxyProtocolType) Set the given explicit end point as the default proxy server for current machine. Declaration public void SetAsSystemProxy(ExplicitProxyEndPoint endPoint, ProxyProtocolType protocolType) Parameters Type Name Description ExplicitProxyEndPoint endPoint The explicit endpoint. ProxyProtocolType protocolType The proxy protocol type. | Edit this page View Source SetAsSystemProxy(ExplicitProxyEndPoint, ProxyProtocolType, SystemProxySettings?) Set the given explicit end point as the default proxy server for current machine. Declaration public void SetAsSystemProxy(ExplicitProxyEndPoint endPoint, ProxyProtocolType protocolType, SystemProxySettings? settings) Parameters Type Name Description ExplicitProxyEndPoint endPoint The explicit endpoint. ProxyProtocolType protocolType The proxy protocol type. SystemProxySettings settings The Windows system proxy settings, or null to preserve the current bypass list. | Edit this page View Source SetHttp3Enabled(bool) Enables or disables EnableHttp3. Enabling still requires MsQuic (IsSupported); disabling is always applied. Safe to call while the proxy is running — new origin connections pick up the change. Existing sessions keep the protocol they already negotiated. Declaration public bool SetHttp3Enabled(bool enabled) Parameters Type Name Description bool enabled Returns Type Description bool true when HTTP/3 is enabled after the call. | Edit this page View Source ShouldBypassDecryptForLearnedHost(string?) When EnableDecryptFailureBypass is on and host is actively bypassed, returns true (decrypt should be skipped). Declaration public bool ShouldBypassDecryptForLearnedHost(string? host) Parameters Type Name Description string host Returns Type Description bool | Edit this page View Source Start(bool) Start this proxy server instance. Transactional: if any endpoint fails to start, every listener this call already started is stopped, the system-upstream-proxy resolver (if this call created one) is disposed, and ProxyRunning is left false before the exception propagates. A caller that catches the exception is left with an instance in exactly the same state as before calling Start(bool), not a partially-bound proxy with some endpoints silently listening. Declaration public void Start(bool changeSystemProxySettings = true) Parameters Type Name Description bool changeSystemProxySettings Whether or not clear any system proxy settings which is pointing to our own endpoint (causing a cycle). E.g due to ungracious proxy shutdown before. | Edit this page View Source Stop() Stop this proxy server instance. Endpoints remain registered so Start(bool) can re-listen on the same ports. In-flight sessions are cancelled; pooled upstream connections are cleared. The connection factory itself stays usable for a subsequent Start (it is only disposed with the proxy). Declaration public void Stop() | Edit this page View Source StopAsync(TimeSpan?) Asynchronously stop this proxy server, cancel in-flight sessions, and wait briefly for client connection count to drain before clearing the upstream pool. Declaration public Task StopAsync(TimeSpan? drainTimeout = null) Parameters Type Name Description TimeSpan? drainTimeout Maximum time to wait for active client handlers to exit after cancellation. Defaults to 5 seconds. Returns Type Description Task | Edit this page View Source TryDisableAllSystemProxies() Clear all OS proxy settings without throwing. Declaration public SystemProxyChangeResult TryDisableAllSystemProxies() Returns Type Description SystemProxyChangeResult | Edit this page View Source TryDisableSystemProxy(ProxyProtocolType) Clear OS proxy for the given protocols without throwing. Declaration public SystemProxyChangeResult TryDisableSystemProxy(ProxyProtocolType protocolType) Parameters Type Name Description ProxyProtocolType protocolType Returns Type Description SystemProxyChangeResult | Edit this page View Source TryEnableHttp3IfSupported() Turns on EnableHttp3 when MsQuic is available (IsSupported). Hosts (CLI, Inspector, examples) should call this instead of setting EnableHttp3 blindly. Returns true when HTTP/3 was enabled. Declaration public bool TryEnableHttp3IfSupported() Returns Type Description bool | Edit this page View Source TryRestoreOriginalProxySettings() Restore OS proxy without throwing. Declaration public SystemProxyChangeResult TryRestoreOriginalProxySettings() Returns Type Description SystemProxyChangeResult | Edit this page View Source TrySetAsSystemProxy(ExplicitProxyEndPoint, ProxyProtocolType, SystemProxySettings?) Enable OS system proxy without throwing. Failures are logged and returned so Inspector/CLI can show a status message instead of crashing. Declaration public SystemProxyChangeResult TrySetAsSystemProxy(ExplicitProxyEndPoint endPoint, ProxyProtocolType protocolType, SystemProxySettings? settings = null) Parameters Type Name Description ExplicitProxyEndPoint endPoint ProxyProtocolType protocolType SystemProxySettings settings Returns Type Description SystemProxyChangeResult Events | Edit this page View Source AfterResponse Intercept after response event from server. Declaration public event AsyncEventHandler? AfterResponse Event Type Type Description AsyncEventHandler | Edit this page View Source BeforeRequest Intercept request event to server. Declaration public event AsyncEventHandler? BeforeRequest Event Type Type Description AsyncEventHandler | Edit this page View Source BeforeResponse Intercept response event from server. Declaration public event AsyncEventHandler? BeforeResponse Event Type Type Description AsyncEventHandler | Edit this page View Source BeforeUpStreamConnectRequest Intercept connect request sent to upstream proxy. Declaration public event AsyncEventHandler? BeforeUpStreamConnectRequest Event Type Type Description AsyncEventHandler | Edit this page View Source ClientCertificateSelectionCallback Event to override client certificate selection during mutual SSL authentication. Declaration public event AsyncEventHandler? ClientCertificateSelectionCallback Event Type Type Description AsyncEventHandler | Edit this page View Source ClientConnectionCountChanged Event occurs when client connection count changed. Declaration public event EventHandler? ClientConnectionCountChanged Event Type Type Description EventHandler | Edit this page View Source DecryptFailureBypassChanged Raised when a host becomes actively bypassed (threshold reached or same-CONNECT mark). Handlers must not block; Inspector marshals to the UI thread. Declaration public event EventHandler? DecryptFailureBypassChanged Event Type Type Description EventHandler | Edit this page View Source Http3ClientConnectionCountChanged Event occurs when inbound HTTP/3 client connection count changed. Declaration public event EventHandler? Http3ClientConnectionCountChanged Event Type Type Description EventHandler | Edit this page View Source Http3ServerConnectionCountChanged Event occurs when upstream HTTP/3 server connection count changed. Declaration public event EventHandler? Http3ServerConnectionCountChanged Event Type Type Description EventHandler | Edit this page View Source OnClientConnectionCreate Customize TcpClient used for client connection upon create. Declaration public event AsyncEventHandler? OnClientConnectionCreate Event Type Type Description AsyncEventHandler | Edit this page View Source OnRequestBodyWrite Intercept request body send event to server. Subscribe to inspect or modify the request body chunk-by-chunk as it streams to the server, without buffering the whole body. Do not combine with SessionEventArgs.GetRequestBody (which buffers). Declaration public event AsyncEventHandler? OnRequestBodyWrite Event Type Type Description AsyncEventHandler | Edit this page View Source OnResponseBodyWrite Intercept response body send event to client. Subscribe to inspect or modify the response body chunk-by-chunk as it streams to the client, without buffering the whole body. Do not combine with SessionEventArgs.GetResponseBody (which buffers). Declaration public event AsyncEventHandler? OnResponseBodyWrite Event Type Type Description AsyncEventHandler | Edit this page View Source OnServerConnectionCreate Customize TcpClient used for server connection upon create. Declaration public event AsyncEventHandler? OnServerConnectionCreate Event Type Type Description AsyncEventHandler | Edit this page View Source ServerCertificateValidationCallback Event to override the default verification logic of remote SSL certificate received during authentication. Declaration public event AsyncEventHandler? ServerCertificateValidationCallback Event Type Type Description AsyncEventHandler | Edit this page View Source ServerConnectionCountChanged Event occurs when server connection count changed. Declaration public event EventHandler? ServerConnectionCountChanged Event Type Type Description EventHandler Implements IDisposable"
},
"api/Titanium.Web.Proxy.Routing.ReverseProxyFastPath.html": {
"href": "api/Titanium.Web.Proxy.Routing.ReverseProxyFastPath.html",
diff --git a/docs/xrefmap.yml b/docs/xrefmap.yml
index 6d77c5e65..07c017c2e 100644
--- a/docs/xrefmap.yml
+++ b/docs/xrefmap.yml
@@ -9614,6 +9614,19 @@ references:
commentId: E:Titanium.Web.Proxy.ProxyServer.Http3ServerConnectionCountChanged
fullName: Titanium.Web.Proxy.ProxyServer.Http3ServerConnectionCountChanged
nameWithType: ProxyServer.Http3ServerConnectionCountChanged
+- uid: Titanium.Web.Proxy.ProxyServer.HttpResponseCacheFiller
+ name: HttpResponseCacheFiller
+ href: api/Titanium.Web.Proxy.ProxyServer.html#Titanium_Web_Proxy_ProxyServer_HttpResponseCacheFiller
+ commentId: P:Titanium.Web.Proxy.ProxyServer.HttpResponseCacheFiller
+ fullName: Titanium.Web.Proxy.ProxyServer.HttpResponseCacheFiller
+ nameWithType: ProxyServer.HttpResponseCacheFiller
+- uid: Titanium.Web.Proxy.ProxyServer.HttpResponseCacheFiller*
+ name: HttpResponseCacheFiller
+ href: api/Titanium.Web.Proxy.ProxyServer.html#Titanium_Web_Proxy_ProxyServer_HttpResponseCacheFiller_
+ commentId: Overload:Titanium.Web.Proxy.ProxyServer.HttpResponseCacheFiller
+ isSpec: "True"
+ fullName: Titanium.Web.Proxy.ProxyServer.HttpResponseCacheFiller
+ nameWithType: ProxyServer.HttpResponseCacheFiller
- uid: Titanium.Web.Proxy.ProxyServer.IdleReadTimeoutSeconds
name: IdleReadTimeoutSeconds
href: api/Titanium.Web.Proxy.ProxyServer.html#Titanium_Web_Proxy_ProxyServer_IdleReadTimeoutSeconds
@@ -9992,6 +10005,19 @@ references:
isSpec: "True"
fullName: Titanium.Web.Proxy.ProxyServer.ResourceLimits
nameWithType: ProxyServer.ResourceLimits
+- uid: Titanium.Web.Proxy.ProxyServer.ResponseHeaderContributor
+ name: ResponseHeaderContributor
+ href: api/Titanium.Web.Proxy.ProxyServer.html#Titanium_Web_Proxy_ProxyServer_ResponseHeaderContributor
+ commentId: P:Titanium.Web.Proxy.ProxyServer.ResponseHeaderContributor
+ fullName: Titanium.Web.Proxy.ProxyServer.ResponseHeaderContributor
+ nameWithType: ProxyServer.ResponseHeaderContributor
+- uid: Titanium.Web.Proxy.ProxyServer.ResponseHeaderContributor*
+ name: ResponseHeaderContributor
+ href: api/Titanium.Web.Proxy.ProxyServer.html#Titanium_Web_Proxy_ProxyServer_ResponseHeaderContributor_
+ commentId: Overload:Titanium.Web.Proxy.ProxyServer.ResponseHeaderContributor
+ isSpec: "True"
+ fullName: Titanium.Web.Proxy.ProxyServer.ResponseHeaderContributor
+ nameWithType: ProxyServer.ResponseHeaderContributor
- uid: Titanium.Web.Proxy.ProxyServer.ResponseHeaderTimeoutSeconds
name: ResponseHeaderTimeoutSeconds
href: api/Titanium.Web.Proxy.ProxyServer.html#Titanium_Web_Proxy_ProxyServer_ResponseHeaderTimeoutSeconds
diff --git a/src/Titanium.Cli/Config/RunCommand.cs b/src/Titanium.Cli/Config/RunCommand.cs
index 5563ea326..ad1ab5e20 100644
--- a/src/Titanium.Cli/Config/RunCommand.cs
+++ b/src/Titanium.Cli/Config/RunCommand.cs
@@ -691,41 +691,10 @@ private static void ConfigureResponseCache(
var cacheMiddleware = new HttpResponseCacheMiddleware(responseCache);
middleware.Add(cacheMiddleware);
- // Buffer in BeforeResponse so fill does not depend on MITM session-lite coalescing.
- // After the body is streamed, IsBodyReceived is set without IsBodyRead and
- // AfterResponse GetResponseBody throws — perpetual misses (~0.64× CLI vs ~0.99× hits).
- proxy.BeforeResponse += async (_, e) =>
- {
- try
- {
- var response = e.HttpClient.Response;
- if (response.StatusCode == 200 &&
- response.HasBody &&
- !response.IsBodyRead)
- {
- response.KeepBody = true;
- await e.GetResponseBody().ConfigureAwait(false);
- }
- }
- catch
- {
- // Cache best-effort only.
- }
- };
-
- proxy.AfterResponse += (_, e) =>
- {
- try
- {
- cacheMiddleware.TryCacheCurrentResponse(e);
- }
- catch
- {
- // Cache best-effort only.
- }
-
- return Task.CompletedTask;
- };
+ // Session-path fill (a transform plus cache) buffers a small 200 inside the response
+ // handler. Do not subscribe BeforeResponse/AfterResponse: those force every request
+ // off H1 terminate-lite, so a hit never skips the origin.
+ proxy.HttpResponseCacheFiller = cacheMiddleware.TryCacheCurrentResponse;
}
private static async Task TryActivatePlusAsync(TwpConfig config, PlusActivationContext context)
diff --git a/src/Titanium.Cli/Titanium.Cli.csproj b/src/Titanium.Cli/Titanium.Cli.csproj
index 77df06a18..a5b7fce27 100644
--- a/src/Titanium.Cli/Titanium.Cli.csproj
+++ b/src/Titanium.Cli/Titanium.Cli.csproj
@@ -7,7 +7,7 @@
latest
enable
false
- 7.0.16
+ 7.0.17
Jehonathan Thomas
Titanium Web Proxy CLI (titanium / twp).
MIT
diff --git a/src/Titanium.Inspector/Services/IInspectorPathPicker.cs b/src/Titanium.Inspector/Services/IInspectorPathPicker.cs
index c71b99534..2389752b6 100644
--- a/src/Titanium.Inspector/Services/IInspectorPathPicker.cs
+++ b/src/Titanium.Inspector/Services/IInspectorPathPicker.cs
@@ -111,7 +111,7 @@ public Task> PickOpenPathsAsync(string title, string filte
internal readonly record struct StoragePickAttempt(bool DialogShown, string? Path)
{
public IReadOnlyList Paths { get; init; } =
- string.IsNullOrEmpty(Path) ? Array.Empty() : [Path!];
+ string.IsNullOrEmpty(Path) ? Array.Empty() : [Path];
}
internal static class InspectorPathPickerHelpers
diff --git a/src/Titanium.Inspector/Services/SessionArchive.cs b/src/Titanium.Inspector/Services/SessionArchive.cs
index f90bff937..cf005ea70 100644
--- a/src/Titanium.Inspector/Services/SessionArchive.cs
+++ b/src/Titanium.Inspector/Services/SessionArchive.cs
@@ -235,7 +235,7 @@ public static JsonObject ToHarEntry(SessionSnapshot s)
{
try
{
- if (!TryReadHarRequest(entry, out var method, out var url, out var host, out var reqHeaders, out var reqBody, out var contentType, out var reqBytes))
+ if (!TryReadHarRequest(entry, out var request))
{
return null;
}
@@ -246,18 +246,18 @@ public static JsonObject ToHarEntry(SessionSnapshot s)
var snap = new SessionSnapshot
{
Id = id,
- Method = method,
- Url = url,
- Host = host,
+ Method = request.Method,
+ Url = request.Url,
+ Host = request.Host,
StartedUtc = started,
StatusCode = status,
- RequestHeadersText = reqHeaders,
+ RequestHeadersText = request.Headers,
ResponseHeadersText = respHeaders,
- RequestBodyText = reqBody,
+ RequestBodyText = request.Body,
ResponseBodyText = respBody,
- RequestBodyBytes = reqBytes,
+ RequestBodyBytes = request.BodyBytes,
ResponseBodyBytes = respBytes,
- ContentType = contentType ?? respMime,
+ ContentType = request.ContentType ?? respMime,
DurationMs = durationMs,
TtfbMs = ttfbMs,
BodySize = respBytes?.LongLength ?? respBody?.Length,
@@ -386,6 +386,14 @@ private static void ApplyInspectorExtension(JsonElement entry, SessionSnapshot s
return;
}
+ ApplyInspectorIdentity(ext, snap);
+ ApplyInspectorCapture(ext, snap);
+ ApplyInspectorFlags(ext, snap);
+ ApplyInspectorBodies(ext, snap);
+ }
+
+ private static void ApplyInspectorIdentity(JsonElement ext, SessionSnapshot snap)
+ {
if (ext.TryGetProperty("id", out var idEl) && idEl.TryGetInt64(out var savedId) && savedId > 0)
{
snap.Id = savedId;
@@ -415,7 +423,10 @@ private static void ApplyInspectorExtension(JsonElement entry, SessionSnapshot s
{
snap.ProcessName = pn.GetString();
}
+ }
+ private static void ApplyInspectorCapture(JsonElement ext, SessionSnapshot snap)
+ {
if (ext.TryGetProperty("sentBytes", out var sent) && sent.TryGetInt64(out var sentBytes))
{
snap.SentBytes = sentBytes;
@@ -447,7 +458,10 @@ private static void ApplyInspectorExtension(JsonElement entry, SessionSnapshot s
{
snap.ResponseBodyOriginalSize = respOrig;
}
+ }
+ private static void ApplyInspectorFlags(JsonElement ext, SessionSnapshot snap)
+ {
snap.IsWebSocket = ReadBool(ext, "isWebSocket") ?? snap.IsWebSocket;
snap.IsGrpc = ReadBool(ext, "isGrpc") ?? snap.IsGrpc;
snap.IsTranscoded = ReadBool(ext, "isTranscoded") ?? snap.IsTranscoded;
@@ -468,7 +482,10 @@ private static void ApplyInspectorExtension(JsonElement entry, SessionSnapshot s
snap.UpstreamPath = ReadString(ext, "upstreamPath") ?? snap.UpstreamPath;
snap.UpstreamContentType = ReadString(ext, "upstreamContentType") ?? snap.UpstreamContentType;
snap.ProtobufDecodedText = ReadString(ext, "protobufDecodedText") ?? snap.ProtobufDecodedText;
+ }
+ private static void ApplyInspectorBodies(JsonElement ext, SessionSnapshot snap)
+ {
// Prefer _inspector body payloads when present (lossless for Inspect).
if (TryDecodeBase64(ext, "requestBodyBase64", out var reqBytes))
{
@@ -539,7 +556,7 @@ private static bool TryDecodeBase64(JsonElement ext, string name, out byte[] byt
}
}
- private static IReadOnlyList? DeserializeList(JsonElement ext, string name)
+ private static List? DeserializeList(JsonElement ext, string name)
{
if (!ext.TryGetProperty(name, out var el) || el.ValueKind != JsonValueKind.Array)
{
@@ -556,37 +573,33 @@ private static bool TryDecodeBase64(JsonElement ext, string name, out byte[] byt
}
}
- private static bool TryReadHarRequest(
- JsonElement entry,
- out string method,
- out string url,
- out string? host,
- out string reqHeaders,
- out string? reqBody,
- out string? contentType,
- out byte[]? reqBytes)
- {
- method = "GET";
- url = "";
- host = null;
- reqHeaders = "";
- reqBody = null;
- contentType = null;
- reqBytes = null;
+ private readonly record struct HarRequestFields(
+ string Method,
+ string Url,
+ string? Host,
+ string Headers,
+ string? Body,
+ string? ContentType,
+ byte[]? BodyBytes);
+ private static bool TryReadHarRequest(JsonElement entry, out HarRequestFields request)
+ {
+ request = new HarRequestFields("GET", "", null, "", null, null, null);
if (!entry.TryGetProperty("request", out var req))
{
return false;
}
- method = req.TryGetProperty("method", out var m) ? m.GetString() ?? "GET" : "GET";
- url = req.TryGetProperty("url", out var u) ? u.GetString() ?? "" : "";
+ var method = req.TryGetProperty("method", out var m) ? m.GetString() ?? "GET" : "GET";
+ var url = req.TryGetProperty("url", out var u) ? u.GetString() ?? "" : "";
+ string? host = null;
if (Uri.TryCreate(url, UriKind.Absolute, out var uri))
{
host = uri.Host;
}
- reqHeaders = FormatHarHeaders(req);
+ string? reqBody = null;
+ string? contentType = null;
if (req.TryGetProperty("postData", out var post) && post.ValueKind == JsonValueKind.Object)
{
if (post.TryGetProperty("text", out var pt))
@@ -600,6 +613,7 @@ private static bool TryReadHarRequest(
}
}
+ request = new HarRequestFields(method, url, host, FormatHarHeaders(req), reqBody, contentType, null);
return true;
}
diff --git a/src/Titanium.Inspector/Services/SessionBodyDiskCache.cs b/src/Titanium.Inspector/Services/SessionBodyDiskCache.cs
index 0b0ddeaab..fc7179bc4 100644
--- a/src/Titanium.Inspector/Services/SessionBodyDiskCache.cs
+++ b/src/Titanium.Inspector/Services/SessionBodyDiskCache.cs
@@ -695,7 +695,7 @@ private void RemoveIndexEntryLocked(string path)
}
}
- private void QueueCleanup(IReadOnlyList paths)
+ private void QueueCleanup(List paths)
{
if (paths.Count == 0)
{
@@ -704,12 +704,9 @@ private void QueueCleanup(IReadOnlyList paths)
lock (_cleanupGate)
{
- foreach (var path in paths)
+ foreach (var path in paths.Where(static path => !string.IsNullOrEmpty(path)))
{
- if (!string.IsNullOrEmpty(path))
- {
- _cleanupPaths.Enqueue(path);
- }
+ _cleanupPaths.Enqueue(path);
}
_cleanupTask = _cleanupTask.ContinueWith(
diff --git a/src/Titanium.Inspector/Services/SessionStore.cs b/src/Titanium.Inspector/Services/SessionStore.cs
index a398905bf..ffa5f7f6c 100644
--- a/src/Titanium.Inspector/Services/SessionStore.cs
+++ b/src/Titanium.Inspector/Services/SessionStore.cs
@@ -147,14 +147,12 @@ public long? PinnedSessionId
{
var prevId = _pinnedSessionId;
_pinnedSessionId = value;
- if (prevId is long oldId && oldId != value && _byId.TryGetValue(oldId, out previous))
+ // Drop RAM bodies for the previous selection when the file already exists.
+ if (prevId is long oldId && oldId != value && _byId.TryGetValue(oldId, out previous)
+ && previous.BodiesOnDisk)
{
- // Drop RAM bodies for the previous selection when the file already exists.
- if (previous.BodiesOnDisk)
- {
- ClearBodyFields(previous);
- RecalcInMemoryBodyBytesLocked();
- }
+ ClearBodyFields(previous);
+ RecalcInMemoryBodyBytesLocked();
}
if (value is long newId && _byId.TryGetValue(newId, out next))
@@ -338,7 +336,8 @@ public async Task FlushDiskCleanupAsync(TimeSpan? timeout = null)
public async Task EnsureBodiesLoadedAsync(SessionSnapshot snapshot, CancellationToken ct = default)
{
ObjectDisposedException.ThrowIf(_disposed, this);
- if (_disk is null)
+ var disk = _disk;
+ if (disk is null)
{
return;
}
@@ -355,15 +354,43 @@ public async Task EnsureBodiesLoadedAsync(SessionSnapshot snapshot, Cancellation
}
// Fast path: file already gone and nothing pending — do not wait ~1s.
- if (!_disk.FileExists(snapshot.Id) && Volatile.Read(ref _pendingSpills) == 0)
+ if (!disk.FileExists(snapshot.Id) && Volatile.Read(ref _pendingSpills) == 0)
{
snapshot.BodiesMissingFromDisk = true;
return;
}
- // Under heavy capture, the spill writer may still be draining thousands of HARs.
- // Keep waiting while work is queued; only mark missing once the channel is idle
- // and the file is still absent (bounded by ct / ~2 minutes).
+ if (await WaitForSpilledBodyAsync(snapshot, disk, ct).ConfigureAwait(false))
+ {
+ return;
+ }
+
+ if (!HasInMemoryBodies(snapshot) &&
+ Volatile.Read(ref _pendingSpills) == 0 &&
+ !disk.FileExists(snapshot.Id))
+ {
+ snapshot.BodiesMissingFromDisk = true;
+ }
+ }
+
+ public async Task EnsureBodiesLoadedAsync(IEnumerable snapshots, CancellationToken ct = default)
+ {
+ foreach (var snap in snapshots)
+ {
+ ct.ThrowIfCancellationRequested();
+ await EnsureBodiesLoadedAsync(snap, ct).ConfigureAwait(false);
+ }
+ }
+
+ ///
+ /// Under heavy capture, the spill writer may still be draining thousands of HARs.
+ /// Keep waiting while work is queued; only mark missing once the channel is idle
+ /// and the file is still absent (bounded by ct / ~2 minutes).
+ /// Returns true when the caller should stop (body loaded or marked missing).
+ ///
+ private async Task WaitForSpilledBodyAsync(
+ SessionSnapshot snapshot, SessionBodyDiskCache disk, CancellationToken ct)
+ {
var deadline = DateTime.UtcNow + TimeSpan.FromMinutes(2);
while (DateTime.UtcNow < deadline)
{
@@ -373,42 +400,28 @@ public async Task EnsureBodiesLoadedAsync(SessionSnapshot snapshot, Cancellation
if (HasInMemoryBodies(snapshot))
{
snapshot.BodiesMissingFromDisk = false;
- return;
+ return true;
}
- if (_disk.TryLoad(snapshot))
+ if (disk.TryLoad(snapshot))
{
// Keep BodiesOnDisk=true so deselect can unload without rewriting the file.
snapshot.BodiesMissingFromDisk = false;
RecalcInMemoryBodyBytesLocked();
- return;
+ return true;
}
}
- if (Volatile.Read(ref _pendingSpills) == 0 && !_disk.FileExists(snapshot.Id))
+ if (Volatile.Read(ref _pendingSpills) == 0 && !disk.FileExists(snapshot.Id))
{
snapshot.BodiesMissingFromDisk = true;
- return;
+ return true;
}
await Task.Delay(25, ct).ConfigureAwait(false);
}
- if (!HasInMemoryBodies(snapshot) &&
- Volatile.Read(ref _pendingSpills) == 0 &&
- !_disk.FileExists(snapshot.Id))
- {
- snapshot.BodiesMissingFromDisk = true;
- }
- }
-
- public async Task EnsureBodiesLoadedAsync(IEnumerable snapshots, CancellationToken ct = default)
- {
- foreach (var snap in snapshots)
- {
- ct.ThrowIfCancellationRequested();
- await EnsureBodiesLoadedAsync(snap, ct).ConfigureAwait(false);
- }
+ return false;
}
///
diff --git a/src/Titanium.Inspector/Services/SessionStreamBuffer.cs b/src/Titanium.Inspector/Services/SessionStreamBuffer.cs
index 865943dc8..acb162421 100644
--- a/src/Titanium.Inspector/Services/SessionStreamBuffer.cs
+++ b/src/Titanium.Inspector/Services/SessionStreamBuffer.cs
@@ -55,48 +55,61 @@ private async Task ReadLoopAsync()
await foreach (var snapshot in _channel.Reader.ReadAllAsync())
{
batch.Add(snapshot);
- var deadline = Environment.TickCount64 + _batchWindowMs;
- while (batch.Count < _batchMax && _channel.Reader.TryRead(out var more))
+ await CoalesceBatchAsync(batch).ConfigureAwait(false);
+ SessionsBatchAdded?.Invoke(batch.ToList());
+ RaiseSessionAdded(batch);
+ batch.Clear();
+ }
+ }
+
+ private async Task CoalesceBatchAsync(List batch)
+ {
+ var deadline = Environment.TickCount64 + _batchWindowMs;
+ while (batch.Count < _batchMax && _channel.Reader.TryRead(out var more))
+ {
+ batch.Add(more);
+ }
+
+ // Wait briefly for more arrivals when the channel is momentarily empty.
+ while (batch.Count < _batchMax && Environment.TickCount64 < deadline)
+ {
+ var remaining = (int)(deadline - Environment.TickCount64);
+ if (remaining <= 0)
{
- batch.Add(more);
+ break;
}
- // Wait briefly for more arrivals when the channel is momentarily empty.
- while (batch.Count < _batchMax && Environment.TickCount64 < deadline)
+ using var delayCts = new CancellationTokenSource(remaining);
+ try
{
- var remaining = (int)(deadline - Environment.TickCount64);
- if (remaining <= 0)
- {
- break;
- }
-
- using var delayCts = new CancellationTokenSource(remaining);
- try
- {
- if (await _channel.Reader.WaitToReadAsync(delayCts.Token).ConfigureAwait(false))
- {
- while (batch.Count < _batchMax && _channel.Reader.TryRead(out var more))
- {
- batch.Add(more);
- }
- }
- }
- catch (OperationCanceledException)
+ if (await _channel.Reader.WaitToReadAsync(delayCts.Token).ConfigureAwait(false))
{
- break;
+ DrainAvailable(batch);
}
}
-
- SessionsBatchAdded?.Invoke(batch.ToList());
- if (SessionAdded is { } singleHandler)
+ catch (OperationCanceledException)
{
- foreach (var s in batch)
- {
- singleHandler(s);
- }
+ break;
}
+ }
+ }
- batch.Clear();
+ private void DrainAvailable(List batch)
+ {
+ while (batch.Count < _batchMax && _channel.Reader.TryRead(out var more))
+ {
+ batch.Add(more);
+ }
+ }
+
+ private void RaiseSessionAdded(List batch)
+ {
+ // One subscriber list for the whole batch. Re-reading the event per row would drop
+ // the tail for a handler that unsubscribes on the first snapshot.
+ if (SessionAdded is { } handler) // NOSONAR S3264 -- Invoke the captured delegate so the batch shares one list.
+ {
+ foreach (var snapshot in batch)
+ handler(snapshot);
}
}
}
diff --git a/src/Titanium.Inspector/Titanium.Inspector.csproj b/src/Titanium.Inspector/Titanium.Inspector.csproj
index 49ae8476a..f66d463cd 100644
--- a/src/Titanium.Inspector/Titanium.Inspector.csproj
+++ b/src/Titanium.Inspector/Titanium.Inspector.csproj
@@ -8,7 +8,7 @@
enable
true
false
- 7.0.16
+ 7.0.17
Jehonathan Thomas
Titanium Inspector desktop traffic debugger (PolyForm Noncommercial).
LICENSE
diff --git a/src/Titanium.Inspector/ViewModels/AutoResponderViewModel.cs b/src/Titanium.Inspector/ViewModels/AutoResponderViewModel.cs
index 2b3ce523d..5d7690bee 100644
--- a/src/Titanium.Inspector/ViewModels/AutoResponderViewModel.cs
+++ b/src/Titanium.Inspector/ViewModels/AutoResponderViewModel.cs
@@ -158,6 +158,9 @@ public bool TryMatch(string url, string? requestBody, out AutoResponderRule? mat
return false;
}
+ public bool TryMatch(string url, out AutoResponderRule? matched)
+ => TryMatch(url, requestBody: null, out matched);
+
private AutoResponderRule[] SnapshotRules()
{
lock (_rulesGate)
@@ -168,9 +171,6 @@ private AutoResponderRule[] SnapshotRules()
}
}
- public bool TryMatch(string url, out AutoResponderRule? matched)
- => TryMatch(url, requestBody: null, out matched);
-
public bool TryRespond(SessionSnapshot session, out AutoResponderRule? matched)
=> TryMatch(session.Url, session.RequestBodyText, out matched);
diff --git a/src/Titanium.Inspector/ViewModels/MainWindowViewModel.Sessions.cs b/src/Titanium.Inspector/ViewModels/MainWindowViewModel.Sessions.cs
index 84117b783..1495290dd 100644
--- a/src/Titanium.Inspector/ViewModels/MainWindowViewModel.Sessions.cs
+++ b/src/Titanium.Inspector/ViewModels/MainWindowViewModel.Sessions.cs
@@ -603,15 +603,13 @@ private void OnSessionsBatchAdded(IReadOnlyList batch)
return;
}
- foreach (var snapshot in batch)
+ // SessionUpdated may have raced ahead of this batched capture add and already
+ // inserted the row; never append the same snapshot twice.
+ foreach (var snapshot in batch.Where(snapshot =>
+ SessionSearch.Matches(snapshot, SearchQuery, bodyMatcher: null) &&
+ Sessions.IndexOf(snapshot) < 0))
{
- // SessionUpdated may have raced ahead of this batched capture add and already
- // inserted the row; never append the same snapshot twice.
- if (SessionSearch.Matches(snapshot, SearchQuery, bodyMatcher: null) &&
- Sessions.IndexOf(snapshot) < 0)
- {
- Sessions.Add(snapshot);
- }
+ Sessions.Add(snapshot);
}
RefreshSessionCountText();
@@ -820,15 +818,7 @@ private void RemoveVisibleByIds(HashSet ids)
return;
}
- var removeCount = 0;
- foreach (var session in Sessions)
- {
- if (ids.Contains(session.Id))
- {
- removeCount++;
- }
- }
-
+ var removeCount = Sessions.Count(session => ids.Contains(session.Id));
if (removeCount == 0)
{
return;
@@ -842,16 +832,7 @@ private void RemoveVisibleByIds(HashSet ids)
if (removeCount >= BulkGridEditThreshold && removeCount * 2 >= Sessions.Count)
{
- var keep = new List(Sessions.Count - removeCount);
- foreach (var session in Sessions)
- {
- if (!ids.Contains(session.Id))
- {
- keep.Add(session);
- }
- }
-
- ReplaceVisibleSessions(keep);
+ ReplaceVisibleSessions(Sessions.Where(session => !ids.Contains(session.Id)).ToList());
return;
}
diff --git a/src/Titanium.Inspector/ViewModels/MapRemoteViewModel.cs b/src/Titanium.Inspector/ViewModels/MapRemoteViewModel.cs
index 2a621d96d..9ca09ea34 100644
--- a/src/Titanium.Inspector/ViewModels/MapRemoteViewModel.cs
+++ b/src/Titanium.Inspector/ViewModels/MapRemoteViewModel.cs
@@ -157,6 +157,9 @@ public bool TryRewrite(string url, string? requestBody, out string? rewritten, o
return false;
}
+ public bool TryRewrite(string url, out string? rewritten, out MapRemoteRule? matched)
+ => TryRewrite(url, requestBody: null, out rewritten, out matched);
+
private MapRemoteRule[] SnapshotRules()
{
lock (_rulesGate)
@@ -167,9 +170,6 @@ private MapRemoteRule[] SnapshotRules()
}
}
- public bool TryRewrite(string url, out string? rewritten, out MapRemoteRule? matched)
- => TryRewrite(url, requestBody: null, out rewritten, out matched);
-
///
/// Rewrites using wildcard capture from
/// into . A single trailing * in both match and target
diff --git a/src/Titanium.Inspector/Views/ExcludedHostsWindow.axaml.cs b/src/Titanium.Inspector/Views/ExcludedHostsWindow.axaml.cs
index 23b1ccbd3..f5eb82acb 100644
--- a/src/Titanium.Inspector/Views/ExcludedHostsWindow.axaml.cs
+++ b/src/Titanium.Inspector/Views/ExcludedHostsWindow.axaml.cs
@@ -10,7 +10,6 @@ namespace Titanium.Inspector.Views;
public partial class ExcludedHostsWindow : Window
{
private readonly SettingsService _settings;
- private readonly bool _readOnly;
private readonly Action? _onSaved;
private readonly InterceptionService? _interception;
private bool _saved;
@@ -27,7 +26,6 @@ public ExcludedHostsWindow(
InterceptionService? interception = null)
{
_settings = settings;
- _readOnly = readOnly;
_onSaved = onSaved;
_interception = interception;
InitializeComponent();
diff --git a/src/Titanium.Plus/Security/AccessSecurity.cs b/src/Titanium.Plus/Security/AccessSecurity.cs
index 09bfbf587..835852203 100644
--- a/src/Titanium.Plus/Security/AccessSecurity.cs
+++ b/src/Titanium.Plus/Security/AccessSecurity.cs
@@ -95,18 +95,16 @@ private static void RegisterCors(PlusActivationContext context, IReadOnlyDiction
var cors = new CorsMiddleware(origin, methods, headers, creds, maxAge);
context.Middleware!.Add(cors);
- // Inject ACAO on successful proxied responses when session path is used.
+ // Inject ACAO on the way out. A BeforeResponse subscription would disable terminate-lite.
if (context.ProxyServer is Titanium.Web.Proxy.ProxyServer proxy)
{
- proxy.BeforeResponse += (_, e) =>
+ proxy.ResponseHeaderContributor = response =>
{
foreach (var h in cors.BuildResponseHeaders())
{
- e.HttpClient.Response.Headers.RemoveHeader(h.Name);
- e.HttpClient.Response.Headers.AddHeader(h);
+ response.Headers.RemoveHeader(h.Name);
+ response.Headers.AddHeader(h);
}
-
- return Task.CompletedTask;
};
}
diff --git a/src/Titanium.Plus/Titanium.Plus.csproj b/src/Titanium.Plus/Titanium.Plus.csproj
index c854d11ae..95cfc2ffa 100644
--- a/src/Titanium.Plus/Titanium.Plus.csproj
+++ b/src/Titanium.Plus/Titanium.Plus.csproj
@@ -7,7 +7,7 @@
enable
True
StrongNameKey.snk
- 7.0.16
+ 7.0.17
Jehonathan Thomas
Titanium Web Proxy Plus advanced features plugin (PolyForm Noncommercial).
LICENSE
diff --git a/src/Titanium.Web.Proxy.Abstractions/Middleware/IProxyMiddleware.cs b/src/Titanium.Web.Proxy.Abstractions/Middleware/IProxyMiddleware.cs
index c5b99bc92..66f7341eb 100644
--- a/src/Titanium.Web.Proxy.Abstractions/Middleware/IProxyMiddleware.cs
+++ b/src/Titanium.Web.Proxy.Abstractions/Middleware/IProxyMiddleware.cs
@@ -48,6 +48,12 @@ public sealed class ProxyMiddlewareContext
public string? HandledBody { get; set; }
+ ///
+ /// Raw response body for a handled terminate-lite reply. When set, the lite writer uses these
+ /// bytes instead of encoding .
+ ///
+ public byte[]? HandledBodyBytes { get; set; }
+
public List>? HandledHeaders { get; set; }
}
diff --git a/src/Titanium.Web.Proxy.Abstractions/PublicAPI.Unshipped.txt b/src/Titanium.Web.Proxy.Abstractions/PublicAPI.Unshipped.txt
index f5b378ec0..91d8e0cd5 100644
--- a/src/Titanium.Web.Proxy.Abstractions/PublicAPI.Unshipped.txt
+++ b/src/Titanium.Web.Proxy.Abstractions/PublicAPI.Unshipped.txt
@@ -74,6 +74,8 @@ Titanium.Web.Proxy.Abstractions.Middleware.ProxyMiddlewareContext.ClientRemoteEn
Titanium.Web.Proxy.Abstractions.Middleware.ProxyMiddlewareContext.ClientRemoteEndPoint.set -> void
Titanium.Web.Proxy.Abstractions.Middleware.ProxyMiddlewareContext.HandledBody.get -> string?
Titanium.Web.Proxy.Abstractions.Middleware.ProxyMiddlewareContext.HandledBody.set -> void
+Titanium.Web.Proxy.Abstractions.Middleware.ProxyMiddlewareContext.HandledBodyBytes.get -> byte[]?
+Titanium.Web.Proxy.Abstractions.Middleware.ProxyMiddlewareContext.HandledBodyBytes.set -> void
Titanium.Web.Proxy.Abstractions.Middleware.ProxyMiddlewareContext.HandledHeaders.get -> System.Collections.Generic.List>?
Titanium.Web.Proxy.Abstractions.Middleware.ProxyMiddlewareContext.HandledHeaders.set -> void
Titanium.Web.Proxy.Abstractions.Middleware.ProxyMiddlewareContext.HandledStatusCode.get -> int?
diff --git a/src/Titanium.Web.Proxy.Abstractions/Titanium.Web.Proxy.Abstractions.csproj b/src/Titanium.Web.Proxy.Abstractions/Titanium.Web.Proxy.Abstractions.csproj
index baa60f1e0..40ada07a3 100644
--- a/src/Titanium.Web.Proxy.Abstractions/Titanium.Web.Proxy.Abstractions.csproj
+++ b/src/Titanium.Web.Proxy.Abstractions/Titanium.Web.Proxy.Abstractions.csproj
@@ -7,7 +7,7 @@
enable
True
StrongNameKey.snk
- 7.0.16
+ 7.0.17
Jehonathan Thomas
Shared contracts for Titanium Web Proxy routing, clusters, middleware, and plugins.
MIT
diff --git a/src/Titanium.Web.Proxy.Configuration/Titanium.Web.Proxy.Configuration.csproj b/src/Titanium.Web.Proxy.Configuration/Titanium.Web.Proxy.Configuration.csproj
index 1718c2646..e58157f06 100644
--- a/src/Titanium.Web.Proxy.Configuration/Titanium.Web.Proxy.Configuration.csproj
+++ b/src/Titanium.Web.Proxy.Configuration/Titanium.Web.Proxy.Configuration.csproj
@@ -7,7 +7,7 @@
enable
True
StrongNameKey.snk
- 7.0.16
+ 7.0.17
Jehonathan Thomas
YAML/JSON configuration binding for Titanium Web Proxy CLI and reverse-proxy documents.
MIT
diff --git a/src/Titanium.Web.Proxy/Caching/HttpResponseCacheMiddleware.cs b/src/Titanium.Web.Proxy/Caching/HttpResponseCacheMiddleware.cs
index 84f9692fe..3238d2250 100644
--- a/src/Titanium.Web.Proxy/Caching/HttpResponseCacheMiddleware.cs
+++ b/src/Titanium.Web.Proxy/Caching/HttpResponseCacheMiddleware.cs
@@ -1,11 +1,13 @@
using System;
using System.Collections.Generic;
+using System.Net;
using System.Threading;
using System.Threading.Tasks;
using Titanium.Web.Proxy.Abstractions.Middleware;
using Titanium.Web.Proxy.Abstractions.Plugins;
using Titanium.Web.Proxy.EventArguments;
using Titanium.Web.Proxy.Extensions;
+using Titanium.Web.Proxy.Http;
using Titanium.Web.Proxy.Models;
namespace Titanium.Web.Proxy.Caching;
@@ -16,6 +18,9 @@ namespace Titanium.Web.Proxy.Caching;
///
public sealed class HttpResponseCacheMiddleware : IProxyMiddleware
{
+ /// Largest body stored from the H1 terminate-lite coalesce buffer.
+ internal const int MaxCachedBodyBytes = 64 * 1024;
+
private readonly IHttpResponseCache _cache;
private readonly TimeSpan _defaultTtl;
@@ -30,58 +35,72 @@ public async ValueTask InvokeAsync(
ProxyMiddlewareDelegate next,
CancellationToken cancellationToken)
{
- if (context.Session is not SessionEventArgs session)
+ if (!TryResolveCacheable(context, out var method, out var host, out var path))
{
await next(context, cancellationToken).ConfigureAwait(false);
return;
}
- var request = session.HttpClient.Request;
- var method = request.Method ?? "GET";
- var isCacheableMethod =
- method.Equals("GET", StringComparison.OrdinalIgnoreCase) ||
- method.Equals("HEAD", StringComparison.OrdinalIgnoreCase);
-
- if (isCacheableMethod)
+ var key = BuildCacheKey(method, host, path);
+ if (_cache.TryGet(key, out var cached) && cached is not null)
{
- var key = BuildCacheKey(session);
- if (_cache.TryGet(key, out var cached) && cached is not null)
- {
- var headers = new List(cached.Headers.Count + 1);
- foreach (var h in cached.Headers)
- {
- headers.Add(new HttpHeader(h.Key, h.Value));
- }
-
- headers.Add(new HttpHeader("X-Cache", "HIT"));
- session.GenericResponse(cached.Body, (System.Net.HttpStatusCode)cached.StatusCode, headers);
- context.IsHandled = true;
- return;
- }
+ ServeHit(context, cached);
+ return;
}
await next(context, cancellationToken).ConfigureAwait(false);
- if (!isCacheableMethod || context.IsHandled)
+ if (context.Session is SessionEventArgs session && !context.IsHandled)
+ {
+ TryCacheCurrentResponse(session);
+ }
+ }
+
+ ///
+ /// Store a 200 whose body is already in memory (lite coalesce or a buffered session response).
+ /// Hop-by-hop headers and bodies larger than are skipped.
+ ///
+ internal void TryStore(
+ string method,
+ string host,
+ string path,
+ int statusCode,
+ IEnumerable headers,
+ ReadOnlySpan body)
+ {
+ if (statusCode != 200 || body.Length > MaxCachedBodyBytes)
+ {
+ return;
+ }
+
+ if (!method.Equals("GET", StringComparison.OrdinalIgnoreCase) &&
+ !method.Equals("HEAD", StringComparison.OrdinalIgnoreCase))
{
return;
}
- // Cache synthetic / already-buffered 200 responses produced during BeforeRequest.
- // Upstream responses are cached when body is available (e.g. after GetResponseBody).
- TryCacheCurrentResponse(session);
+ var copy = new byte[body.Length];
+ body.CopyTo(copy);
+ _cache.Set(
+ BuildCacheKey(method, host, path),
+ new CachedHttpResponse
+ {
+ StatusCode = 200,
+ Body = copy,
+ Headers = CopyCacheableHeaders(headers),
+ ExpiresUtc = DateTimeOffset.UtcNow + _defaultTtl,
+ },
+ _defaultTtl);
}
///
- /// Call from when caching upstream 200 bodies.
+ /// Call from the session response path when a small 200 body is already buffered.
///
public void TryCacheCurrentResponse(SessionEventArgs session)
{
ArgumentNullException.ThrowIfNull(session);
- var method = session.HttpClient.Request.Method ?? "";
- if (!method.Equals("GET", StringComparison.OrdinalIgnoreCase) &&
- !method.Equals("HEAD", StringComparison.OrdinalIgnoreCase))
+ if (!TryRequestParts(session, out var method, out var host, out var path))
{
return;
}
@@ -102,29 +121,83 @@ public void TryCacheCurrentResponse(SessionEventArgs session)
return;
}
- var headers = new List>();
- foreach (var header in response.Headers)
+ TryStore(method, host, path, 200, response.Headers, body);
+ }
+
+ internal static string BuildCacheKey(string method, string host, string path)
+ {
+ if (string.IsNullOrEmpty(path))
{
- headers.Add(new KeyValuePair(header.Name, header.Value));
+ path = "/";
}
- _cache.Set(
- BuildCacheKey(session),
- new CachedHttpResponse
- {
- StatusCode = 200,
- Body = body,
- Headers = headers,
- ExpiresUtc = DateTimeOffset.UtcNow + _defaultTtl,
- },
- _defaultTtl);
+ return $"{method}:{host}{path}";
+ }
+
+ private static void ServeHit(ProxyMiddlewareContext context, CachedHttpResponse cached)
+ {
+ var headers = new List(cached.Headers.Count + 1);
+ foreach (var h in cached.Headers)
+ {
+ headers.Add(new HttpHeader(h.Key, h.Value));
+ }
+
+ headers.Add(new HttpHeader("X-Cache", "HIT"));
+
+ if (context.Session is SessionEventArgs session)
+ {
+ session.GenericResponse(cached.Body, (HttpStatusCode)cached.StatusCode, headers);
+ }
+
+ context.HandledStatusCode = cached.StatusCode;
+ context.HandledBodyBytes = cached.Body;
+ context.HandledHeaders = new List>(headers.Count);
+ foreach (var h in headers)
+ {
+ context.HandledHeaders.Add(new KeyValuePair(h.Name, h.Value));
+ }
+
+ context.IsHandled = true;
}
- private static string BuildCacheKey(SessionEventArgs session)
+ private static bool TryResolveCacheable(
+ ProxyMiddlewareContext context,
+ out string method,
+ out string host,
+ out string path)
+ {
+ if (context.Session is SessionEventArgs session)
+ {
+ return TryRequestParts(session, out method, out host, out path);
+ }
+
+ if (context.Request is { } view)
+ {
+ method = view.Method ?? "GET";
+ host = view.Host ?? "";
+ path = string.IsNullOrEmpty(view.Path) ? "/" : view.Path;
+ return IsCacheableMethod(method);
+ }
+
+ method = "";
+ host = "";
+ path = "";
+ return false;
+ }
+
+ private static bool TryRequestParts(SessionEventArgs session, out string method, out string host, out string path)
{
var request = session.HttpClient.Request;
- var host = request.Host ?? request.RequestUri?.Host ?? "";
- var path = request.RequestUri?.PathAndQuery;
+ method = request.Method ?? "";
+ if (!IsCacheableMethod(method))
+ {
+ host = "";
+ path = "";
+ return false;
+ }
+
+ host = request.Host ?? request.RequestUri?.Host ?? "";
+ path = request.RequestUri?.PathAndQuery ?? "";
if (string.IsNullOrEmpty(path))
{
path = request.RequestUriString8.GetString();
@@ -134,6 +207,55 @@ private static string BuildCacheKey(SessionEventArgs session)
}
}
- return $"{request.Method}:{host}{path}";
+ return true;
+ }
+
+ private static bool IsCacheableMethod(string method) =>
+ method.Equals("GET", StringComparison.OrdinalIgnoreCase) ||
+ method.Equals("HEAD", StringComparison.OrdinalIgnoreCase);
+
+ private static List> CopyCacheableHeaders(IEnumerable headers)
+ {
+ var hopByHop = new HashSet(StringComparer.OrdinalIgnoreCase)
+ {
+ "Connection",
+ "Keep-Alive",
+ "Proxy-Connection",
+ "Transfer-Encoding",
+ "TE",
+ "Trailer",
+ "Upgrade",
+ };
+
+ var staged = new List>();
+ foreach (var header in headers)
+ {
+ if (header.Name.Equals("Connection", StringComparison.OrdinalIgnoreCase))
+ {
+ foreach (var part in header.Value.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)) // NOSONAR S3267 -- Where would allocate an enumerator on every cache store.
+ {
+ if (!part.Equals("close", StringComparison.OrdinalIgnoreCase) &&
+ !part.Equals("keep-alive", StringComparison.OrdinalIgnoreCase))
+ {
+ hopByHop.Add(part);
+ }
+ }
+
+ continue;
+ }
+
+ staged.Add(new KeyValuePair(header.Name, header.Value));
+ }
+
+ var kept = new List>(staged.Count);
+ foreach (var header in staged) // NOSONAR S3267 -- A capturing Where allocates a closure on every cache store.
+ {
+ if (!hopByHop.Contains(header.Key))
+ {
+ kept.Add(header);
+ }
+ }
+
+ return kept;
}
}
diff --git a/src/Titanium.Web.Proxy/Handlers/H1TerminateFastForward.cs b/src/Titanium.Web.Proxy/Handlers/H1TerminateFastForward.cs
index 1e76408fb..0ba7da5e7 100644
--- a/src/Titanium.Web.Proxy/Handlers/H1TerminateFastForward.cs
+++ b/src/Titanium.Web.Proxy/Handlers/H1TerminateFastForward.cs
@@ -26,7 +26,7 @@ namespace Titanium.Web.Proxy;
/// Signals that H1 terminate-lite cannot finish this exchange (typically a 1xx interim response)
/// and the caller should fall through to the full session path without treating it as a failure.
///
-internal sealed class H1TerminateLiteFallbackException : Exception
+internal sealed class H1TerminateLiteFallbackException : Exception // NOSONAR S3871 -- Internal control-flow signal for the terminate-lite fallback, not a public error contract.
{
public H1TerminateLiteFallbackException(string message) : base(message)
{
@@ -83,9 +83,10 @@ internal static void ReleaseH1TerminateLiteClient(HttpWebClient client)
/// GC under c=32 (cool: c=1 already leads YARP, c=32 was ~0.88–0.90×).
/// Callers must also refuse /
/// at the connection level — this path only speaks HTTP/1.1 TCP to the origin.
- /// Pre-origin middleware (CIDR/WAF/JWT/rate-limit) runs on this path via
- /// without a session bag; AfterResponse subscribers
- /// still force the full session path via .
+ /// Pre-origin middleware (CIDR/WAF/JWT/rate-limit/cache/CORS) runs on this path via
+ /// without a session bag. A
+ /// or subscriber still forces the full session path via
+ /// .
///
internal bool CanUseH1TerminateLite(ProxyEndPoint endPoint, Request request, bool enable100Continue,
bool enableWinAuth, bool hasCustomUpstreamProxyFunc, UpstreamHttpProtocol? upstreamProtocol = null)
@@ -139,6 +140,7 @@ internal bool CanUseH1TerminateLite(ProxyEndPoint endPoint, Request request, boo
request.Locked = true;
request.IsBodyReceived = true;
+ CaptureTerminateLiteCacheIdentity(endPoint, request, out var cacheMethod, out var cacheHost, out var cachePath);
// Remember before stripping hop-by-hop Connection for the origin write — NC clients send
// Connection: close; forwarding it forces the origin to close and defeats pooling (Bare still
@@ -209,7 +211,7 @@ internal bool CanUseH1TerminateLite(ProxyEndPoint endPoint, Request request, boo
if (response.StatusCode is >= 100 and <= 199)
{
// Full session path has the interim 1xx loop; keep the origin connection for it.
- closeConnection = false;
+ // closeConnection is already false, so the fallback catch releases the socket for reuse.
throw new H1TerminateLiteFallbackException(
"H1 terminate lite does not handle interim 1xx responses.");
}
@@ -229,6 +231,7 @@ internal bool CanUseH1TerminateLite(ProxyEndPoint endPoint, Request request, boo
HttpVersion = request.HttpVersion
};
badGateway.Headers.AddHeader(KnownHeaders.Connection, KnownHeaders.ConnectionClose);
+ ApplyResponseHeaderContributor(badGateway);
await clientStream.WriteResponseAsync(badGateway, cancellationToken);
return false;
}
@@ -237,7 +240,7 @@ internal bool CanUseH1TerminateLite(ProxyEndPoint endPoint, Request request, boo
// Known-CL ≤64 KiB: one coalesced client write without assigning Response.Body.
// Larger / chunked bodies: still stream via CopyBody with a throwaway session (hooks unused).
- const int coalesceBodyLimit = 64 * 1024;
+ const int coalesceBodyLimit = Caching.HttpResponseCacheMiddleware.MaxCachedBodyBytes;
if (response.HasBody
&& !response.IsChunked
&& !response.HasTrailingHeaders
@@ -263,7 +266,13 @@ internal bool CanUseH1TerminateLite(ProxyEndPoint endPoint, Request request, boo
if (response.ContentLength != read)
response.ContentLength = read;
}
+ else
+ {
+ TryStoreH1TerminateLiteCache(cacheMethod, cacheHost, cachePath, response,
+ body.AsSpan(0, read));
+ }
+ ApplyResponseHeaderContributor(response);
await clientStream.WriteResponseWithWireBodyAsync(response, body.AsMemory(0, read),
cancellationToken);
}
@@ -274,6 +283,7 @@ await clientStream.WriteResponseWithWireBodyAsync(response, body.AsMemory(0, rea
}
else if (response.HasBody)
{
+ ApplyResponseHeaderContributor(response);
await clientStream.WriteResponseAsync(response, cancellationToken);
var copySession = openSession ?? CreateH1TerminateLiteColdSession(endPoint, clientStream);
try
@@ -295,6 +305,9 @@ await connection.Stream.CopyBodyAsync(response, false, clientStream, Transformat
}
else
{
+ if (response.StatusCode == 200)
+ TryStoreH1TerminateLiteCache(cacheMethod, cacheHost, cachePath, response, ReadOnlySpan.Empty);
+ ApplyResponseHeaderContributor(response);
await clientStream.WriteResponseAsync(response, cancellationToken);
}
@@ -347,6 +360,7 @@ await connection.Stream.CopyBodyAsync(response, false, clientStream, Transformat
var request = args.HttpClient.Request;
request.Locked = true;
request.IsBodyReceived = true;
+ CaptureTerminateLiteCacheIdentity(endPoint, request, out var cacheMethod, out var cacheHost, out var cachePath);
var clientRequestedClose = H1TerminateClientRequestedClose(request);
request.StripHopByHopConnectionForTransparentOrigin();
@@ -410,7 +424,7 @@ await connection.Stream.CopyBodyAsync(response, false, clientStream, Transformat
var response = http.Response;
if (response.StatusCode is >= 100 and <= 199)
{
- closeConnection = false;
+ // closeConnection is already false, so the fallback catch releases the socket for reuse.
throw new H1TerminateLiteFallbackException(
"H1 terminate MITM lite does not handle interim 1xx responses.");
}
@@ -451,6 +465,8 @@ await connection.Stream.CopyBodyAsync(response, false, clientStream, Transformat
var mayNeedBodyAfterWrite = AfterResponse != null || HasOnResponseBodyWriteSubscribers;
if (response.HasBody && response.IsBodyRead)
{
+ await TryFillHttpResponseCacheAsync(args, cancellationToken);
+ ApplyResponseHeaderContributor(response);
await clientStream.WriteResponseAsync(response, cancellationToken);
}
else if (knownClCoalesce && !response.IsBodyRead)
@@ -475,7 +491,13 @@ await connection.Stream.CopyBodyAsync(response, false, clientStream, Transformat
if (response.ContentLength != read)
response.ContentLength = read;
}
+ else
+ {
+ TryStoreH1TerminateLiteCache(cacheMethod, cacheHost, cachePath, response,
+ body.AsSpan(0, read));
+ }
+ ApplyResponseHeaderContributor(response);
if (mayNeedBodyAfterWrite)
{
byte[] exact;
@@ -509,6 +531,7 @@ await clientStream.WriteResponseWithWireBodyAsync(response, body.AsMemory(0, rea
}
else if (response.HasBody)
{
+ ApplyResponseHeaderContributor(response);
await clientStream.WriteResponseAsync(response, cancellationToken);
await connection.Stream.CopyBodyAsync(response, false, clientStream, TransformationMode.None,
false, args, cancellationToken);
@@ -516,6 +539,9 @@ await connection.Stream.CopyBodyAsync(response, false, clientStream, Transformat
}
else
{
+ if (response.StatusCode == 200)
+ TryStoreH1TerminateLiteCache(cacheMethod, cacheHost, cachePath, response, ReadOnlySpan.Empty);
+ ApplyResponseHeaderContributor(response);
await clientStream.WriteResponseAsync(response, cancellationToken);
}
@@ -619,7 +645,7 @@ private SessionEventArgs CreateH1TerminateLiteColdSession(TransparentBaseProxyEn
/// . Returns keep-alive when middleware handled the request;
/// null when the origin forward should continue.
///
- private static async Task TryRunTerminateLiteMiddlewareAsync(
+ private async Task TryRunTerminateLiteMiddlewareAsync(
TransparentBaseProxyEndPoint endPoint,
HttpClientStream clientStream,
Request request,
@@ -680,7 +706,7 @@ private static MiddlewareRequestView CreateTerminateLiteMiddlewareRequest(
};
}
- private static async Task WriteTerminateLiteMiddlewareResponseAsync(
+ private async Task WriteTerminateLiteMiddlewareResponseAsync(
HttpClientStream clientStream,
Request request,
ProxyMiddlewareContext ctx,
@@ -697,11 +723,49 @@ private static async Task WriteTerminateLiteMiddlewareResponseAsync(
response.Headers.AddHeader(h.Key, h.Value);
}
- response.Body = response.Encoding.GetBytes(ctx.HandledBody ?? string.Empty);
+ response.Body = ctx.HandledBodyBytes ?? response.Encoding.GetBytes(ctx.HandledBody ?? string.Empty);
+ ApplyResponseHeaderContributor(response);
response.Headers.AddHeader(KnownHeaders.Connection,
H1TerminateClientRequestedClose(request)
? KnownHeaders.ConnectionClose
: KnownHeaders.ConnectionKeepAlive);
await clientStream.WriteResponseAsync(response, cancellationToken).ConfigureAwait(false);
}
+
+ private static void CaptureTerminateLiteCacheIdentity(
+ TransparentBaseProxyEndPoint endPoint,
+ Request request,
+ out string method,
+ out string host,
+ out string path)
+ {
+ var view = CreateTerminateLiteMiddlewareRequest(endPoint, request);
+ method = view.Method;
+ host = view.Host;
+ path = view.Path;
+ }
+
+ private void ApplyResponseHeaderContributor(Response response) =>
+ ResponseHeaderContributor?.Invoke(response);
+
+ private void TryStoreH1TerminateLiteCache(
+ string method,
+ string host,
+ string path,
+ Response response,
+ ReadOnlySpan body)
+ {
+ var middleware = ReverseProxy?.Middleware;
+ if (middleware == null)
+ return;
+
+ for (var i = 0; i < middleware.Count; i++)
+ {
+ if (middleware[i] is Caching.HttpResponseCacheMiddleware cache)
+ {
+ cache.TryStore(method, host, path, response.StatusCode, response.Headers, body);
+ return;
+ }
+ }
+ }
}
diff --git a/src/Titanium.Web.Proxy/Handlers/Http11ToHttp2BridgeHandler.cs b/src/Titanium.Web.Proxy/Handlers/Http11ToHttp2BridgeHandler.cs
index db8d22a63..cdd3b9612 100644
--- a/src/Titanium.Web.Proxy/Handlers/Http11ToHttp2BridgeHandler.cs
+++ b/src/Titanium.Web.Proxy/Handlers/Http11ToHttp2BridgeHandler.cs
@@ -748,7 +748,6 @@ await clientStream.WriteBodyAsync(fastBody, response.IsChunked,
exchange.Response.StreamBodyWriter = null;
// Drain and release origin resources; the replacement body is unrelated.
await originStreamBody(Stream.Null, cancellationToken);
- originStreamBody = null;
}
if (response.Locked)
diff --git a/src/Titanium.Web.Proxy/Handlers/Http2ToHttp11BridgeHandler.cs b/src/Titanium.Web.Proxy/Handlers/Http2ToHttp11BridgeHandler.cs
index 32e88f448..7d4e30922 100644
--- a/src/Titanium.Web.Proxy/Handlers/Http2ToHttp11BridgeHandler.cs
+++ b/src/Titanium.Web.Proxy/Handlers/Http2ToHttp11BridgeHandler.cs
@@ -1135,9 +1135,10 @@ private static async ValueTask RelayStreamToClientAsync(
try
{
var readVt = source.ReadAsync(buf.AsMemory(), CancellationToken.None);
- read = readVt.IsCompletedSuccessfully
- ? readVt.Result
- : await readVt.ConfigureAwait(false);
+ if (readVt.IsCompletedSuccessfully)
+ read = readVt.Result;
+ else
+ read = await readVt.ConfigureAwait(false);
}
catch (Exception readEx)
{
diff --git a/src/Titanium.Web.Proxy/Handlers/ResponseHandler.cs b/src/Titanium.Web.Proxy/Handlers/ResponseHandler.cs
index a347b1731..b59eaaab1 100644
--- a/src/Titanium.Web.Proxy/Handlers/ResponseHandler.cs
+++ b/src/Titanium.Web.Proxy/Handlers/ResponseHandler.cs
@@ -123,6 +123,8 @@ and not (int)HttpStatusCode.SwitchingProtocols)
// it may changed in the user event
response = args.HttpClient.Response;
+ await TryFillHttpResponseCacheAsync(args, cancellationToken);
+ ApplyResponseHeaderContributor(response);
var clientStream = args.ClientStream;
@@ -335,6 +337,50 @@ private async Task OnBeforeResponse(SessionEventArgs args)
await BeforeResponse.InvokeAsync(this, args, logger).ConfigureAwait(false);
}
+ ///
+ /// Buffer a small known-length 200 and store it when a cache filler is set.
+ /// No-op when cache is off, so the hot path does not read the body.
+ ///
+ private async Task TryFillHttpResponseCacheAsync(SessionEventArgs args, CancellationToken cancellationToken)
+ {
+ var fill = HttpResponseCacheFiller;
+ if (fill == null)
+ return;
+
+ var response = args.HttpClient.Response;
+ if (response.StatusCode != 200)
+ return;
+
+ if (!response.IsBodyRead
+ && response.HasBody
+ && !response.IsChunked
+ && !response.HasTrailingHeaders
+ && response.ContentLength is > 0 and <= Caching.HttpResponseCacheMiddleware.MaxCachedBodyBytes)
+ {
+ try
+ {
+ response.KeepBody = true;
+ await args.GetResponseBody(cancellationToken).ConfigureAwait(false);
+ }
+ catch
+ {
+ return;
+ }
+ }
+
+ if (!response.IsBodyRead)
+ return;
+
+ try
+ {
+ fill(args);
+ }
+ catch
+ {
+ // Cache best-effort only.
+ }
+ }
+
///
/// Records learnable MITM HTTP blocks. For document navigations, activates bypass immediately and
/// replaces the response with a meta-refresh so the browser opens a new CONNECT (opaque tunnel).
diff --git a/src/Titanium.Web.Proxy/Http/HttpHeaderHygiene.cs b/src/Titanium.Web.Proxy/Http/HttpHeaderHygiene.cs
index dabeea3ec..1510a3587 100644
--- a/src/Titanium.Web.Proxy/Http/HttpHeaderHygiene.cs
+++ b/src/Titanium.Web.Proxy/Http/HttpHeaderHygiene.cs
@@ -22,13 +22,10 @@ public static bool ContainsForbiddenDelimiter(string? value)
{
if (string.IsNullOrEmpty(value))
return false;
- foreach (var c in value)
- {
- if (c is '\0' or '\r' or '\n')
- return true;
- }
- return false;
+ // IndexOfAny is vectorized and allocation-free. A LINQ Any() walk would box a delegate
+ // on a path that runs for every header written to an HTTP/1.1 wire.
+ return value.AsSpan().IndexOfAny('\0', '\r', '\n') >= 0;
}
public static void ThrowIfForbidden(ByteString name, ByteString value)
diff --git a/src/Titanium.Web.Proxy/Http2/Http2DeferredOutboundData.cs b/src/Titanium.Web.Proxy/Http2/Http2DeferredOutboundData.cs
index b239749e1..16ccbdbf0 100644
--- a/src/Titanium.Web.Proxy/Http2/Http2DeferredOutboundData.cs
+++ b/src/Titanium.Web.Proxy/Http2/Http2DeferredOutboundData.cs
@@ -137,7 +137,7 @@ public void CancelStream(int streamId)
/// runs after the lock is released with the same stream id and
/// may remove the stream (which cancels this queue).
///
- public bool TryDrain(Http2FlowController flow, Http2FrameWriter writer,
+ public bool TryDrain(Http2FlowController flow, Http2FrameWriter writer, // NOSONAR S3776 -- Drain holds the queue lock and must keep DATA frame order; splitting it risks a short-window reorder.
Action? onEndStreamSent = null, Action? onEndStreamQueued = null)
{
var wrote = false;
diff --git a/src/Titanium.Web.Proxy/Http2/Http2Helper.Copy.Headers.cs b/src/Titanium.Web.Proxy/Http2/Http2Helper.Copy.Headers.cs
index ea23fb4a5..deb6146fd 100644
--- a/src/Titanium.Web.Proxy/Http2/Http2Helper.Copy.Headers.cs
+++ b/src/Titanium.Web.Proxy/Http2/Http2Helper.Copy.Headers.cs
@@ -55,7 +55,7 @@ internal partial class Http2Helper
int maxDecodedHeaderListBytes,
ILogger logger,
Action removeAndFinalizeStream,
- Func, ValueTask> lockedOutputWrite,
+ Func, ValueTask> lockedOutputWrite, // NOSONAR S1172 -- Kept so this dispatch stays argument-aligned with CopyHttp2FrameAsync.
bool forceStaticHpackTable,
Http2Settings localSettings, // NOSONAR S1172 -- retained for CopyHttp2FrameAsync call-site IL match.
HeaderCollection headerDecodeScratch,
diff --git a/src/Titanium.Web.Proxy/Http2/Http2Helper.Copy.Relay.cs b/src/Titanium.Web.Proxy/Http2/Http2Helper.Copy.Relay.cs
index b9d20a312..2f101649e 100644
--- a/src/Titanium.Web.Proxy/Http2/Http2Helper.Copy.Relay.cs
+++ b/src/Titanium.Web.Proxy/Http2/Http2Helper.Copy.Relay.cs
@@ -141,7 +141,7 @@ private static Task RelayCompressedWithOriginPoolAsync( // NOSONAR S107 -- Origi
hbStreamId, blockToRelay, endStreamFlag, appendSuffix);
}
- private static async Task AssignOriginAndEnqueueAsync(
+ private static async Task AssignOriginAndEnqueueAsync( // NOSONAR S107 -- Cold origin assign keeps the relay signature explicit (no per-open context object).
Http2ConnectionState connectionState,
bool isClient,
Http2Settings remoteSettings,
@@ -151,11 +151,12 @@ private static async Task AssignOriginAndEnqueueAsync(
{
var assignTask = connectionState.OriginRelayPool!
.AssignStreamAsync(hbStreamId, cancellationToken);
+ // AsTask consumes the ValueTask. Await that Task; a second await of assignTask is undefined.
var tracked = assignTask.AsTask();
connectionState.PendingOriginAssignments[hbStreamId] = tracked;
try
{
- var assignment = await assignTask.ConfigureAwait(false);
+ var assignment = await tracked.ConfigureAwait(false);
EnqueueRelayedHeaderBlock(connectionState, isClient, remoteSettings,
assignment.OriginStreamId, blockToRelay, endStreamFlag, appendSuffix,
assignment.Leg.Writer, assignment.Leg.WriteLock, assignment.Leg.Stream);
diff --git a/src/Titanium.Web.Proxy/Http2/Http2Helper.Copy.cs b/src/Titanium.Web.Proxy/Http2/Http2Helper.Copy.cs
index efa743269..b0627f52e 100644
--- a/src/Titanium.Web.Proxy/Http2/Http2Helper.Copy.cs
+++ b/src/Titanium.Web.Proxy/Http2/Http2Helper.Copy.cs
@@ -684,16 +684,13 @@ async Task TrySendGracefulGoAwayAsync()
{
if (length > 0)
ArrayPool.Shared.Return(payloadRented);
- if (dataEndStream)
+ // already RequestClosed; may now finalize if response half is done
+ if (dataEndStream && compressedDataState.IsClosed)
{
- // already RequestClosed; may now finalize if response half is done
- if (compressedDataState.IsClosed)
- {
- connectionState.OriginRelayPool?.ReleaseStream(dataStreamId);
- connectionState.RemoveStream(dataStreamId);
- ScheduleFinalize(compressedDataState, onAfterResponse, logger,
- connectionState);
- }
+ connectionState.OriginRelayPool?.ReleaseStream(dataStreamId);
+ connectionState.RemoveStream(dataStreamId);
+ ScheduleFinalize(compressedDataState, onAfterResponse, logger,
+ connectionState);
}
continue;
@@ -798,7 +795,7 @@ void EnqueueWireFrame(ReadOnlySpan payload, bool endStream)
if (ownedPayload)
payloadRented = Array.Empty(); // already returned
ReportException(logger, new ProxyHttpException(
- "HTTP/2 deferred DATA queue exceeded its per-stream cap.", null, null));
+ DeferredDataQueueCapExceededMessage, null, null));
await lockedOwnLegWrite(() => SendRstStreamAsync(new Http2FrameHeader(),
new byte[9], peerStreamId != 0 ? peerStreamId : dataStreamId,
Http2ErrorCode.EnhanceYourCalm, input));
@@ -842,7 +839,7 @@ await lockedOwnLegWrite(() => SendRstStreamAsync(new Http2FrameHeader(),
{
ArrayPool.Shared.Return(payloadRented);
ReportException(logger, new ProxyHttpException(
- "HTTP/2 deferred DATA queue exceeded its per-stream cap.", null, null));
+ DeferredDataQueueCapExceededMessage, null, null));
await lockedOwnLegWrite(() => SendRstStreamAsync(new Http2FrameHeader(),
new byte[9], peerStreamId != 0 ? peerStreamId : dataStreamId,
Http2ErrorCode.EnhanceYourCalm, input));
@@ -863,7 +860,7 @@ await lockedOwnLegWrite(() => SendRstStreamAsync(new Http2FrameHeader(),
{
ArrayPool.Shared.Return(payloadRented);
ReportException(logger, new ProxyHttpException(
- "HTTP/2 deferred DATA queue exceeded its per-stream cap.", null, null));
+ DeferredDataQueueCapExceededMessage, null, null));
await lockedOwnLegWrite(() => SendRstStreamAsync(new Http2FrameHeader(),
new byte[9], peerStreamId != 0 ? peerStreamId : dataStreamId,
Http2ErrorCode.EnhanceYourCalm, input));
@@ -1685,7 +1682,7 @@ await lockedOwnLegWrite(() => SendRstStreamAsync(
else if (queued == QueueSendDataResult.CapExceeded)
{
ReportException(logger, new ProxyHttpException(
- "HTTP/2 deferred DATA queue exceeded its per-stream cap.", null, args));
+ DeferredDataQueueCapExceededMessage, null, args));
await lockedOwnLegWrite(() => SendRstStreamAsync(new Http2FrameHeader(),
new byte[9], streamId, Http2ErrorCode.EnhanceYourCalm, input));
RemoveAndFinalizeStream(streamId);
@@ -2310,7 +2307,7 @@ await lockedOwnLegWrite(async () =>
|| absorbState.ResponseClosed;
if (responseStillInFlight)
{
- sendPacket = false;
+ // continue below skips the sendPacket check for this frame.
connectionState.ServerOutboundDeferred.CancelStream(streamId);
absorbState.RequestClosed = true;
if (absorbState.IsClosed)
@@ -2718,7 +2715,7 @@ void EnqueueNonRelayWire(ReadOnlySpan payload, bool endStreamFlag)
async ValueTask RejectDeferredCapAsync()
{
ReportException(logger, new ProxyHttpException(
- "HTTP/2 deferred DATA queue exceeded its per-stream cap.", null, args));
+ DeferredDataQueueCapExceededMessage, null, args));
await lockedOwnLegWrite(() => SendRstStreamAsync(new Http2FrameHeader(),
new byte[9], streamId, Http2ErrorCode.EnhanceYourCalm, input));
// Cap overflow must drop the stream. Leaving it tracked stalls the peer
diff --git a/src/Titanium.Web.Proxy/Http2/Http2Helper.Send.cs b/src/Titanium.Web.Proxy/Http2/Http2Helper.Send.cs
index 29f36e403..ba61fcfb0 100644
--- a/src/Titanium.Web.Proxy/Http2/Http2Helper.Send.cs
+++ b/src/Titanium.Web.Proxy/Http2/Http2Helper.Send.cs
@@ -115,7 +115,7 @@ private static void QueueDataFrame(Http2ConnectionState connectionState, Stream
/// Off-loop variant used by : may await flow-control credit because it
/// does not run on the shared frame reader.
///
- private static async ValueTask QueueSendDataAsync(Http2ConnectionState connectionState, bool towardServer,
+ private static async ValueTask QueueSendDataAsync(Http2ConnectionState connectionState, bool towardServer, // NOSONAR S107 -- DATA send keeps frame fields explicit; a context object would allocate on this path.
SemaphoreSlim writeLock, int streamId, ReadOnlyMemory data, bool endStream, int maxFrameSize,
Http2FlowController flow, Stream output, CancellationToken cancellationToken)
{
@@ -154,7 +154,7 @@ private static async ValueTask QueueSendDataAsync(Http2ConnectionState connectio
/// must reset the stream (silently dropping them would hang the peer).
///
///
- private static QueueSendDataResult QueueSendData(Http2ConnectionState connectionState, bool towardServer,
+ private static QueueSendDataResult QueueSendData(Http2ConnectionState connectionState, bool towardServer, // NOSONAR S107, S3776 -- Per-frame DATA queue shares flow-control state; splitting or boxing the args would change the send path.
SemaphoreSlim writeLock, int streamId, ReadOnlyMemory data, bool endStream, int maxFrameSize,
Http2FlowController flow, Http2DeferredOutboundData deferred, Stream output)
{
@@ -271,7 +271,7 @@ internal static void QueueRstStreamFrame(Http2ConnectionState connectionState, S
/// would let this block overtake earlier-encoded HEADERS still queued on the dedicated writer,
/// and the peer's HPACK decoder would desynchronize (COMPRESSION_ERROR).
///
- internal static void QueueSendTrailer(Http2ConnectionState connectionState, bool towardServer,
+ internal static void QueueSendTrailer(Http2ConnectionState connectionState, bool towardServer, // NOSONAR S107 -- Trailer encode keeps the same explicit frame fields as QueueSendHeader.
SemaphoreSlim writeLock, Http2Settings settings, Http2FrameHeader frameHeader,
byte[] frameHeaderBuffer, int streamId, HeaderCollection trailingHeaders, bool endStream, Stream output)
{
@@ -433,7 +433,7 @@ private static ValueTask WriteHeaderBlockAsync(Http2FrameHeader frameHeader, byt
hasPriority, data, maxFrameSize, output);
}
- private static async ValueTask WriteHeaderBlockMultiAsync(Http2FrameHeader frameHeader,
+ private static async ValueTask WriteHeaderBlockMultiAsync(Http2FrameHeader frameHeader, // NOSONAR S107 -- CONTINUATION writer keeps the same explicit frame fields as WriteHeaderBlockAsync.
byte[] frameHeaderBuffer, int streamId, Http2FrameType type, bool endStream, bool hasPriority,
ReadOnlyMemory data, int maxFrameSize, Stream output)
{
@@ -878,7 +878,7 @@ internal static void EnqueueControlFrame(Http2FrameWriter writer, Http2FrameType
writer.EnqueueRented(rented, total);
}
- private static ValueTask AsValueTask(Task task) => new(task);
+ private static ValueTask AsValueTask(Task task) => new(task); // NOSONAR S1144 -- Reflection test seam for the completed-task ValueTask wrapper.
///
/// Writes two buffers back-to-back without an async state machine when both complete synchronously.
@@ -954,7 +954,7 @@ internal static async Task EmitInterimResponseAsync(SessionEventArgs args, int s
/// HTTP/2 frames the body with DATA/END_STREAM (Transfer-Encoding is never used over h2), so the
/// chunked header is always stripped regardless of which shape applies.
///
- internal static async Task EmitSyntheticResponseAsync(SessionEventArgs args, int streamId,
+ internal static async Task EmitSyntheticResponseAsync(SessionEventArgs args, int streamId, // NOSONAR S107 -- Synthetic response keeps the caller’s frame buffers explicit.
Http2ConnectionState connectionState, Stream clientStream, CancellationToken cancellationToken,
Func? onAfterResponse = null, ILogger? logger = null,
ReadOnlyMemory? wireBody = null)
@@ -1075,7 +1075,7 @@ await EmitBufferedSyntheticResponseAsync(response, streamId, connectionState, fr
/// Emits HEADERS + DATA from an already-buffered wire body without requiring
/// (coalesce path for unread MITM lite / reverse).
///
- private static async Task EmitBufferedSyntheticResponseAsync(Response response, int streamId,
+ private static async Task EmitBufferedSyntheticResponseAsync(Response response, int streamId, // NOSONAR S107 -- Buffered synthetic emit keeps the caller’s frame buffers explicit.
Http2ConnectionState connectionState, Http2FrameHeader frameHeader, byte[] frameHeaderBuffer,
Stream clientStream, ReadOnlyMemory wireBody, CancellationToken cancellationToken)
{
diff --git a/src/Titanium.Web.Proxy/Http2/Http2Helper.cs b/src/Titanium.Web.Proxy/Http2/Http2Helper.cs
index 1d10b9c07..84308a69f 100644
--- a/src/Titanium.Web.Proxy/Http2/Http2Helper.cs
+++ b/src/Titanium.Web.Proxy/Http2/Http2Helper.cs
@@ -46,6 +46,8 @@ internal partial class Http2Helper
private static readonly byte[] ConnectMethodBytes = "CONNECT"u8.ToArray();
private const string SyntheticResponseFailedMessage = "HTTP/2 synthetic response failed";
+ private const string DeferredDataQueueCapExceededMessage =
+ "HTTP/2 deferred DATA queue exceeded its per-stream cap.";
///
/// Connection-level WINDOW_UPDATE increment matching Chrome/Edge (0xEF0001). Grows the peer's
diff --git a/src/Titanium.Web.Proxy/Http2/Http2OriginConnection.cs b/src/Titanium.Web.Proxy/Http2/Http2OriginConnection.cs
index f7a4f0842..5b5c8f792 100644
--- a/src/Titanium.Web.Proxy/Http2/Http2OriginConnection.cs
+++ b/src/Titanium.Web.Proxy/Http2/Http2OriginConnection.cs
@@ -1317,44 +1317,69 @@ private void ApplySettings(ReadOnlySpan payload)
{
var identifier = (payload[i] << 8) | payload[i + 1];
var value = (int)BinaryPrimitives.ReadUInt32BigEndian(payload.Slice(i + 2, 4));
+ if (!TryApplySetting(identifier, value))
+ return;
+ }
+ }
- if (identifier == (int)Http2SettingsId.HeaderTableSize)
- originSettings.UpdateHeaderTableSize(value);
- else if (identifier == (int)Http2SettingsId.MaxFrameSize)
- {
- // RFC 7540 §6.5.2: values outside [16384, 16777215] are a connection-level PROTOCOL_ERROR.
- if (value < 16384 || value > 16777215)
- {
- Fail(new IOException(
- $"HTTP/2 protocol error: SETTINGS_MAX_FRAME_SIZE value {value} is out of range [16384, 16777215]."));
- return;
- }
+ /// Returns false when the setting is a connection error and the rest of the frame must be ignored.
+ private bool TryApplySetting(int identifier, int value)
+ {
+ if (identifier == (int)Http2SettingsId.HeaderTableSize)
+ originSettings.UpdateHeaderTableSize(value);
+ else if (identifier == (int)Http2SettingsId.MaxFrameSize)
+ {
+ if (!TryApplyMaxFrameSize(value))
+ return false;
+ }
+ else if (identifier == (int)Http2SettingsId.InitialWindowSize)
+ {
+ if (!TryApplyInitialWindowSize(value))
+ return false;
+ }
+ else if (identifier == (int)Http2SettingsId.MaxConcurrentStreams)
+ originSettings.MaxConcurrentStreams = value;
+ else if (identifier == (int)Http2SettingsId.EnableConnectProtocol)
+ ApplyEnableConnectProtocolSetting(value);
- originSettings.MaxFrameSize = value;
- }
- else if (identifier == (int)Http2SettingsId.InitialWindowSize)
- {
- // RFC 7540 §6.5.2: values above 2^31-1 are a connection-level FLOW_CONTROL_ERROR.
- // A wire value > 2^31-1 wraps to a negative int when cast; checking < 0 catches that.
- if (value < 0)
- {
- Fail(new IOException(
- $"HTTP/2 protocol error: SETTINGS_INITIAL_WINDOW_SIZE value exceeds the maximum of 2,147,483,647."));
- return;
- }
+ return true;
+ }
- if (sendFlow.OnInitialWindowSizeChanged(value))
- {
- Fail(new IOException(
- "HTTP/2 protocol error: SETTINGS_INITIAL_WINDOW_SIZE drove a stream window above 2^31-1."));
- return;
- }
- }
- else if (identifier == (int)Http2SettingsId.MaxConcurrentStreams)
- originSettings.MaxConcurrentStreams = value;
- else if (identifier == (int)Http2SettingsId.EnableConnectProtocol)
- ApplyEnableConnectProtocolSetting(value);
+ /// RFC 7540 §6.5.2: values outside [16384, 16777215] are a connection-level PROTOCOL_ERROR.
+ private bool TryApplyMaxFrameSize(int value)
+ {
+ if (value < 16384 || value > 16777215)
+ {
+ Fail(new IOException(
+ $"HTTP/2 protocol error: SETTINGS_MAX_FRAME_SIZE value {value} is out of range [16384, 16777215]."));
+ return false;
}
+
+ originSettings.MaxFrameSize = value;
+ return true;
+ }
+
+ ///
+ /// RFC 7540 §6.5.2: values above 2^31-1 are a connection-level FLOW_CONTROL_ERROR.
+ /// A wire value > 2^31-1 wraps to a negative int when cast; checking < 0 catches that.
+ ///
+ private bool TryApplyInitialWindowSize(int value)
+ {
+ if (value < 0)
+ {
+ Fail(new IOException(
+ "HTTP/2 protocol error: SETTINGS_INITIAL_WINDOW_SIZE value exceeds the maximum of 2,147,483,647."));
+ return false;
+ }
+
+ if (sendFlow.OnInitialWindowSizeChanged(value))
+ {
+ Fail(new IOException(
+ "HTTP/2 protocol error: SETTINGS_INITIAL_WINDOW_SIZE drove a stream window above 2^31-1."));
+ return false;
+ }
+
+ return true;
}
/// RFC 8441 §3: value MUST be 0 or 1; a sender MUST NOT send 0 after previously sending 1.
diff --git a/src/Titanium.Web.Proxy/Http3/H3H1QpackResponseReader.cs b/src/Titanium.Web.Proxy/Http3/H3H1QpackResponseReader.cs
index 6610fa039..5a21b66c3 100644
--- a/src/Titanium.Web.Proxy/Http3/H3H1QpackResponseReader.cs
+++ b/src/Titanium.Web.Proxy/Http3/H3H1QpackResponseReader.cs
@@ -42,23 +42,15 @@ internal readonly struct Result
var isChunked = false;
var connectionClose = false;
- while (reader.TryConsumeHeaderLineFromBuffer(out var emptyLine, out var lineBytes))
- {
- if (emptyLine)
- return Finish(builder, contentLength, isChunked, connectionClose);
- ConsumeLine(lineBytes, builder, ref contentLength, ref isChunked, ref connectionClose,
- alsoPopulate);
- }
+ if (TryFinishFromBuffer(reader, builder, ref contentLength, ref isChunked, ref connectionClose,
+ alsoPopulate, out var buffered))
+ return buffered;
while (true)
{
- while (reader.TryConsumeHeaderLineFromBuffer(out var emptyLine, out var lineBytes))
- {
- if (emptyLine)
- return Finish(builder, contentLength, isChunked, connectionClose);
- ConsumeLine(lineBytes, builder, ref contentLength, ref isChunked, ref connectionClose,
- alsoPopulate);
- }
+ if (TryFinishFromBuffer(reader, builder, ref contentLength, ref isChunked, ref connectionClose,
+ alsoPopulate, out buffered))
+ return buffered;
// Prefer byte-buffer fill + TryConsume on the hot path. When the buffer is full without
// an LF (or EOF leaves a partial line), fall back to ReadLine spanning (cold path).
@@ -81,6 +73,31 @@ internal readonly struct Result
}
}
+ private static bool TryFinishFromBuffer(
+ HttpStream reader,
+ QpackEncoder.ResponseBlockBuilder builder,
+ ref long contentLength,
+ ref bool isChunked,
+ ref bool connectionClose,
+ HeaderCollection? alsoPopulate,
+ out Result? result)
+ {
+ while (reader.TryConsumeHeaderLineFromBuffer(out var emptyLine, out var lineBytes))
+ {
+ if (emptyLine)
+ {
+ result = Finish(builder, contentLength, isChunked, connectionClose);
+ return true;
+ }
+
+ ConsumeLine(lineBytes, builder, ref contentLength, ref isChunked, ref connectionClose,
+ alsoPopulate);
+ }
+
+ result = null;
+ return false;
+ }
+
private static Result Finish(
QpackEncoder.ResponseBlockBuilder builder,
long contentLength,
diff --git a/src/Titanium.Web.Proxy/Http3/Http3Frame.cs b/src/Titanium.Web.Proxy/Http3/Http3Frame.cs
index cd3979af3..abe86afe5 100644
--- a/src/Titanium.Web.Proxy/Http3/Http3Frame.cs
+++ b/src/Titanium.Web.Proxy/Http3/Http3Frame.cs
@@ -132,6 +132,15 @@ public static ValueTask WriteAsync(
return WriteLargeAsync(stream, frameType, payload, completeWrites, cancellationToken);
}
+ ///
+ /// Writes a zero-payload frame (used for GOAWAY and some SETTINGS without parameters).
+ ///
+ public static ValueTask WriteAsync(
+ Stream stream,
+ ulong frameType,
+ CancellationToken cancellationToken)
+ => WriteAsync(stream, frameType, ReadOnlyMemory.Empty, cancellationToken);
+
///
/// Copies the frame into and does not release it until the
/// write has been consumed. Sync completion calls GetResult
@@ -240,15 +249,6 @@ private static async ValueTask AwaitWriteAndReturnAsync(ValueTask writeVt, byte[
}
}
- ///
- /// Writes a zero-payload frame (used for GOAWAY and some SETTINGS without parameters).
- ///
- public static ValueTask WriteAsync(
- Stream stream,
- ulong frameType,
- CancellationToken cancellationToken)
- => WriteAsync(stream, frameType, ReadOnlyMemory.Empty, cancellationToken);
-
///
/// Writes HEADERS then DATA as a single stream write when the combined frames fit a modest
/// buffer. Used for already-buffered medium/large bodies (lossy / bodies ≥ 16 KiB) so the
diff --git a/src/Titanium.Web.Proxy/Http3/Http3OriginBridge.Http2.cs b/src/Titanium.Web.Proxy/Http3/Http3OriginBridge.Http2.cs
index 038327ac6..4340b927e 100644
--- a/src/Titanium.Web.Proxy/Http3/Http3OriginBridge.Http2.cs
+++ b/src/Titanium.Web.Proxy/Http3/Http3OriginBridge.Http2.cs
@@ -57,6 +57,30 @@ internal static async Task ForwardOverHttp2FastAsync(
if (request.Authority.Length == 0 && !string.IsNullOrEmpty(request.Host))
request.Authority = request.Host.GetByteString();
+ try
+ {
+ // Origin resolution stays inside the restore so a throw cannot leave the
+ // request stamped as HTTP/2.
+ var origin = ResolveFastHttp2Origin(fwd, request, server);
+ var target = new Http2OriginTarget(
+ origin.Host, origin.Port, origin.ConnectHost, origin.ConnectPort, origin.PoolKey);
+ var exchange = await SendHttp2OriginFastWithGoAwayRetryAsync(
+ server, logger, fwd, target, coldOpenSessionFactory, cancellationToken);
+
+ StampForwardedHttp2Response(fwd, request, exchange);
+ }
+ finally
+ {
+ request.HttpVersion = clientHttpVersion;
+ }
+ }
+
+ private readonly record struct FastHttp2Origin(
+ string Host, int Port, string PoolKey, string? ConnectHost, int? ConnectPort);
+
+ private static FastHttp2Origin ResolveFastHttp2Origin(
+ H3H2FastForward fwd, Request request, ProxyServer server)
+ {
string? connectHost = null;
int? connectPort = null;
if (fwd.ProxyEndPoint is TransparentBaseProxyEndPoint transparent
@@ -66,75 +90,65 @@ internal static async Task ForwardOverHttp2FastAsync(
connectPort = transparent.ForwardPort;
}
- string host;
- int port;
- string poolKey;
if (fwd.ProxyEndPoint is TransparentBaseProxyEndPoint fastEp
&& fastEp.CachedH2OriginPoolKey != null
&& request.Authority.Equals(fastEp.CachedH2OriginAuthority))
{
- host = fastEp.CachedH2OriginHost!;
- port = fastEp.CachedH2OriginPort;
- poolKey = fastEp.CachedH2OriginPoolKey;
- }
- else
- {
- (host, port) = ResolveH2OriginAuthority(request);
- poolKey = Http2OriginConnectionPool.BuildPoolKey(
- server, fwd.ProxyEndPoint, fwd.CustomUpStreamProxy, fwd.UpStreamEndPoint,
- host, port, connectHost, connectPort);
- if (fwd.ProxyEndPoint is TransparentBaseProxyEndPoint cacheEp)
- {
- cacheEp.CachedH2OriginAuthority = request.Authority;
- cacheEp.CachedH2OriginHost = host;
- cacheEp.CachedH2OriginPort = port;
- cacheEp.CachedH2OriginPoolKey = poolKey;
- }
+ return new FastHttp2Origin(
+ fastEp.CachedH2OriginHost!, fastEp.CachedH2OriginPort, fastEp.CachedH2OriginPoolKey,
+ connectHost, connectPort);
}
- try
+ var (host, port) = ResolveH2OriginAuthority(request);
+ var poolKey = Http2OriginConnectionPool.BuildPoolKey(
+ server, fwd.ProxyEndPoint, fwd.CustomUpStreamProxy, fwd.UpStreamEndPoint,
+ host, port, connectHost, connectPort);
+ if (fwd.ProxyEndPoint is TransparentBaseProxyEndPoint cacheEp)
{
- var target = new Http2OriginTarget(host, port, connectHost, connectPort, poolKey);
- var exchange = await SendHttp2OriginFastWithGoAwayRetryAsync(
- server, logger, fwd, target, coldOpenSessionFactory, cancellationToken);
+ cacheEp.CachedH2OriginAuthority = request.Authority;
+ cacheEp.CachedH2OriginHost = host;
+ cacheEp.CachedH2OriginPort = port;
+ cacheEp.CachedH2OriginPoolKey = poolKey;
+ }
- var response = exchange.Response;
- response.HttpVersion = HttpHeader.Version30;
- response.RequestMethod = request.Method;
- if (response.StreamBodyWriter == null)
- {
- response.IsBodyRead = true;
- response.ContentLength = exchange.Body.Length;
- // Interception-off: skip response.Body — emit via Preencoded HEADERS+DATA.
- if (fwd.PreencodedQpackHeaders == null)
- fwd.PreencodedQpackHeaders = QpackEncoder.EncodeResponse(response, context: null);
- fwd.PreencodedBody = exchange.Body;
- fwd.PreencodedBodyLength = exchange.Body.Length;
- // MITM unchanged-lite seeds Response before the call. Stamp Body onto the
- // exchange Response (which replaces that seed) so BeforeResponse /
- // FinishMitm fallback can read it — match Tcp ForwardOverTcpFastAsync.
- // Reverse (Response null) keeps skip-Body coalesce.
- if (fwd.Response != null)
- {
- response.Body = exchange.Body;
- response.BodyIsWireEncoded = true;
- response.IsBodyReceived = true;
- }
- }
- // else: leave StreamBodyWriter + IsBodyRead alone so SendResponseAsync streams DATA.
+ return new FastHttp2Origin(host, port, poolKey, connectHost, connectPort);
+ }
- if (exchange.TrailingHeaders != null && !response.HasTrailingHeaders)
+ private static void StampForwardedHttp2Response(
+ H3H2FastForward fwd, Request request, Http2OriginExchange exchange)
+ {
+ var response = exchange.Response;
+ response.HttpVersion = HttpHeader.Version30;
+ response.RequestMethod = request.Method;
+ if (response.StreamBodyWriter == null)
+ {
+ response.IsBodyRead = true;
+ response.ContentLength = exchange.Body.Length;
+ // Interception-off: skip response.Body — emit via Preencoded HEADERS+DATA.
+ if (fwd.PreencodedQpackHeaders == null)
+ fwd.PreencodedQpackHeaders = QpackEncoder.EncodeResponse(response, context: null);
+ fwd.PreencodedBody = exchange.Body;
+ fwd.PreencodedBodyLength = exchange.Body.Length;
+ // MITM unchanged-lite seeds Response before the call. Stamp Body onto the
+ // exchange Response (which replaces that seed) so BeforeResponse /
+ // FinishMitm fallback can read it — match Tcp ForwardOverTcpFastAsync.
+ // Reverse (Response null) keeps skip-Body coalesce.
+ if (fwd.Response != null)
{
- foreach (var header in exchange.TrailingHeaders)
- response.TrailingHeaders.AddHeader(header);
+ response.Body = exchange.Body;
+ response.BodyIsWireEncoded = true;
+ response.IsBodyReceived = true;
}
-
- fwd.Response = response;
}
- finally
+ // else: leave StreamBodyWriter + IsBodyRead alone so SendResponseAsync streams DATA.
+
+ if (exchange.TrailingHeaders != null && !response.HasTrailingHeaders)
{
- request.HttpVersion = clientHttpVersion;
+ foreach (var header in exchange.TrailingHeaders)
+ response.TrailingHeaders.AddHeader(header);
}
+
+ fwd.Response = response;
}
private static async Task SendHttp2OriginFastWithGoAwayRetryAsync(
diff --git a/src/Titanium.Web.Proxy/Http3/Http3OriginBridge.Tcp.cs b/src/Titanium.Web.Proxy/Http3/Http3OriginBridge.Tcp.cs
index 1927fb6d6..9a3b2813c 100644
--- a/src/Titanium.Web.Proxy/Http3/Http3OriginBridge.Tcp.cs
+++ b/src/Titanium.Web.Proxy/Http3/Http3OriginBridge.Tcp.cs
@@ -689,11 +689,12 @@ async ValueTask CopyResponseAsync()
}
// Keep request upload live while copying the response (true duplex).
- var copyVt = CopyResponseAsync();
+ // Each branch consumes its own ValueTask once. The no-upload path awaits
+ // directly so it does not allocate the Task that WhenAll needs.
if (pendingUpload != null)
- await Task.WhenAll(pendingUpload, copyVt.AsTask());
+ await Task.WhenAll(pendingUpload, CopyResponseAsync().AsTask());
else
- await copyVt;
+ await CopyResponseAsync();
};
}
else if (uploadTask != null)
diff --git a/src/Titanium.Web.Proxy/Http3/Http3RequestStream.cs b/src/Titanium.Web.Proxy/Http3/Http3RequestStream.cs
index cce2cf555..ec7f9e341 100644
--- a/src/Titanium.Web.Proxy/Http3/Http3RequestStream.cs
+++ b/src/Titanium.Web.Proxy/Http3/Http3RequestStream.cs
@@ -366,11 +366,12 @@ SessionEventArgs ColdOpenSessionFactory()
var stub = new SessionEventArgs(server, endPoint, nullStream, null, stubCts);
stub.IsFastPath = true;
stub.CustomUpStreamProxy = fwd.CustomUpStreamProxy;
- stub.UpstreamHttpProtocol = mitmUnchangedH3H3
- ? UpstreamHttpProtocol.Http3
- : mitmUnchangedH3H2
- ? UpstreamHttpProtocol.Http2
- : UpstreamHttpProtocol.Http11;
+ if (mitmUnchangedH3H3)
+ stub.UpstreamHttpProtocol = UpstreamHttpProtocol.Http3;
+ else if (mitmUnchangedH3H2)
+ stub.UpstreamHttpProtocol = UpstreamHttpProtocol.Http2;
+ else
+ stub.UpstreamHttpProtocol = UpstreamHttpProtocol.Http11;
return stub;
}
@@ -715,8 +716,10 @@ await SendPreencodedResponseAsync(stream, fwd.PreencodedQpackHeaders,
{
try
{
+ // Sync Cancel runs registered callbacks on this thread and returns before the
+ // CTS is pooled. CancelAsync queues those callbacks and allocates a Task.
if (!cts.IsCancellationRequested)
- cts.Cancel();
+ cts.Cancel(); // NOSONAR S6966 -- Keep teardown inline on the H3 fast path.
}
catch (ObjectDisposedException)
{
diff --git a/src/Titanium.Web.Proxy/Properties/AssemblyInfo.cs b/src/Titanium.Web.Proxy/Properties/AssemblyInfo.cs
index f834194ac..f63b23397 100644
--- a/src/Titanium.Web.Proxy/Properties/AssemblyInfo.cs
+++ b/src/Titanium.Web.Proxy/Properties/AssemblyInfo.cs
@@ -77,5 +77,5 @@
// file-properties version disagreed with the package it was published in. Keep both of the values
// below equal to (as Major.Minor.Build.0) whenever that property changes.
-[assembly: AssemblyVersion("7.0.16.0")]
-[assembly: AssemblyFileVersion("7.0.16.0")]
+[assembly: AssemblyVersion("7.0.17.0")]
+[assembly: AssemblyFileVersion("7.0.17.0")]
diff --git a/src/Titanium.Web.Proxy/ProxyServer.cs b/src/Titanium.Web.Proxy/ProxyServer.cs
index e2be1bb7b..0afeb2355 100644
--- a/src/Titanium.Web.Proxy/ProxyServer.cs
+++ b/src/Titanium.Web.Proxy/ProxyServer.cs
@@ -1499,6 +1499,19 @@ internal Task InvokeOnResponseBodyWriteAsync(object sender, BeforeBodyWriteEvent
///
public Func? ShouldInterceptHttp { get; set; }
+ ///
+ /// Fills the GET/HEAD response cache from a session that is already on the interception path.
+ /// Does not subscribe or , so it does
+ /// not turn on.
+ ///
+ public Action? HttpResponseCacheFiller { get; set; }
+
+ ///
+ /// Adds response headers on the way to the client without subscribing .
+ /// Plus CORS uses this so a listener can stay on H1 terminate-lite.
+ ///
+ public Action? ResponseHeaderContributor { get; set; }
+
///
/// Returns when the global interception gate is active for the given
/// endpoint: any session event handler is subscribed, is
diff --git a/src/Titanium.Web.Proxy/PublicAPI.Unshipped.txt b/src/Titanium.Web.Proxy/PublicAPI.Unshipped.txt
index 2512676d4..9fd2277e6 100644
--- a/src/Titanium.Web.Proxy/PublicAPI.Unshipped.txt
+++ b/src/Titanium.Web.Proxy/PublicAPI.Unshipped.txt
@@ -265,6 +265,10 @@ Titanium.Web.Proxy.ProxyServer.EnableDecryptFailureBypass.get -> bool
Titanium.Web.Proxy.ProxyServer.EnableDecryptFailureBypass.set -> void
Titanium.Web.Proxy.ProxyServer.ForceDecryptFailureBypass(string! host) -> bool
Titanium.Web.Proxy.ProxyServer.GetDecryptFailureBypassEntries() -> System.Collections.Generic.IReadOnlyList!
+Titanium.Web.Proxy.ProxyServer.HttpResponseCacheFiller.get -> System.Action?
+Titanium.Web.Proxy.ProxyServer.HttpResponseCacheFiller.set -> void
Titanium.Web.Proxy.ProxyServer.RemoveDecryptFailureBypass(string! host) -> bool
+Titanium.Web.Proxy.ProxyServer.ResponseHeaderContributor.get -> System.Action?
+Titanium.Web.Proxy.ProxyServer.ResponseHeaderContributor.set -> void
Titanium.Web.Proxy.ProxyServer.ShouldBypassDecryptForLearnedHost(string? host) -> bool
static Titanium.Web.Proxy.Network.FirefoxCertificateTrust.LastEnterpriseRootsStep.get -> string?
diff --git a/src/Titanium.Web.Proxy/Titanium.Web.Proxy.csproj b/src/Titanium.Web.Proxy/Titanium.Web.Proxy.csproj
index 6eb89e241..072e45039 100644
--- a/src/Titanium.Web.Proxy/Titanium.Web.Proxy.csproj
+++ b/src/Titanium.Web.Proxy/Titanium.Web.Proxy.csproj
@@ -13,7 +13,7 @@
- 7.0.16
+ 7.0.17