Class FCUtils
java.lang.Object
org.frontcache.core.FCUtils
-
Method Summary
Modifier and TypeMethodDescriptionstatic voidaddPublicForwardedHeaders(Map<String, List<String>> headers, RequestContext context) Adds X-Forwarded-Host / X-Forwarded-Proto naming the public site to an origin call, when front-cache.origin.forward-public-host is on.static voidapplyFallbackHeaders(jakarta.servlet.http.HttpServletResponse servletResponse, WebResponse webResponse) Apply a fallbackWebResponse's status code and headers (content type plus the non-cacheable Cache-Control/Pragma markers) to the servlet response.buildRequestHeaders(jakarta.servlet.http.HttpServletRequest request) buildRequestHeaders(jakarta.servlet.http.HttpServletRequest request, boolean normalizeAcceptEncoding) static StringbuildRequestURI(jakarta.servlet.http.HttpServletRequest request) static StringbuildRequestURI(String urlStr) http://localhost:8080/coin_instance_details.htm? -> /coin_instance_details.htm?static WebResponseBuilds aWebResponsefrom a body and a header map, applying the samex-frontcache-component-*reading every origin response goes through.static StringcanonicalizeURL(String urlStr) The URL a request forurlStrwill actually be made with: scheme and host untouched, path and query normalised exactly asbuildRequestURI(String)normalises them for the wire.static org.apache.hc.core5.http.Header[]convertHeaders(Map<String, List<String>> headers) static WebResponsedynamicCall(String urlStr, Map<String, List<String>> requestHeaders, FcHttpClient client, RequestContext context) GET method only for text requests for cache processor - it can use both (httpClient or filter)static StringgetClientIP(jakarta.servlet.http.HttpServletRequest request) static org.apache.hc.core5.http.HttpHostgetHttpHost(URL host) static StringgetRequestURL(jakarta.servlet.http.HttpServletRequest request) static booleanhasMalformedQueryParams(String queryString) Detects a structurally-invalid request query string whose parameter names carry the HTML-entity artifactamp;(raw, or percent-encoded asamp%3B).static WebResponsehttpResponse2WebComponent(String url, org.apache.hc.core5.http.ClassicHttpResponse response, RequestContext context) static WebResponsehttpResponse2WebComponent(String url, FrontCacheHttpResponseWrapper originWrappedResponse, RequestContext context) is used in ServletFilter modestatic WebResponseincludeDynamicCallHttpClient(String urlStr, Map<String, List<String>> requestHeaders, FcHttpClient client, RequestContext context) for includes ONLY - they allways use httpClientstatic booleanisAuthorizedApiKeyHeader(jakarta.servlet.http.HttpServletRequest request) Reads the credential throughApiKey.presented(HttpServletRequest)- so the stream acceptsAuthorization: Bearer <api-key>like everything else, and the deprecatedx-frontcache-site-keyfor as long as that is accepted anywhere.static booleanreturn true if the client requested gzip contentstatic booleanisWebComponentCacheableForClientType(Map<String, Long> expireTimeMap, String clientType) static booleanisWebComponentExpired(Map<String, Long> expireTimeMap, String clientType) Check with current time if expiredstatic booleanisWebComponentSubjectToCache(Map<String, Long> expireTimeMap) static org.apache.hc.client5.http.ConnectionKeepAliveStrategyThe keep-alive strategy shared by the engine's origin client and FrontCacheClient: honour the origin's ownKeep-Alive: timeout=N, defaulting to 10s when it does not say.static longmaxAgeStr2Int(String maxAgeStr) static StringpropagatedClientType(RequestContext context) The client type decided for the PAGE this request is a fragment of, when it can be believed.static StringThe same path and query, addressed at a differentscheme://host[:port].revertHeaders(jakarta.servlet.http.HttpServletResponse response) revertHeaders(org.apache.hc.core5.http.Header[] headers) revert header from HttpClient format (call to origin) to MapsplitQueryParameters(String paramsStr) static StringtransformRedirectURL(String originLocation, RequestContext context) Rewrites a Location the origin sent so it names this site rather than the origin.static voidwriteResponse(InputStream in, OutputStream out) Streamsintoout.
-
Method Details
-
isWebComponentSubjectToCache
-
isWebComponentCacheableForClientType
-
isWebComponentExpired
-
dynamicCall
public static WebResponse dynamicCall(String urlStr, Map<String, List<String>> requestHeaders, FcHttpClient client, RequestContext context) throws FrontCacheException GET method only for text requests for cache processor - it can use both (httpClient or filter)- Parameters:
urlStr-httpRequest-httpResponse-- Returns:
- Throws:
FrontCacheException
-
includeDynamicCallHttpClient
public static WebResponse includeDynamicCallHttpClient(String urlStr, Map<String, List<String>> requestHeaders, FcHttpClient client, RequestContext context) throws FrontCacheException for includes ONLY - they allways use httpClient- Throws:
FrontCacheException
-
propagatedClientType
The client type decided for the PAGE this request is a fragment of, when it can be believed. An<fc:include>re-entering a Frontcache is classified independently of the page that holds it. With a User-Agent-only bots.conf the two agree by accident - the UA survives the hop. They stop agreeing the moment a rule reads anything else: aclient-ip:rule sees the sibling node's address rather than the visitor's, and acookie:rule sees no cookies at all once the servlet request behind an async include has been recycled. Page and fragments then read different branches of the same WebResponse's expireTimeMap, and the fragments quietly stop being cached - with nothing in the log but a client-type column that disagrees between the two lines. Three conditions, all required, none of them optional:- the peer is trusted (
front-cache.client-ip.trusted-proxies). FCUtils forwards every header it does not blacklist, so without this a client could simply send the header and choose its own cache behaviour. Unset by default, which means propagation is OFF by default and classification falls back to the rules - the safe direction; - the request arrived as an include (it carried a Frontcache request id). A top-level request is a client's, and a client's claim about this is worth nothing;
- the value is a client type this node knows. Anything else is ignored rather than stored - an unknown client type would key nothing in expireTimeMap.
X-Frontcache-Client-Type- exactly as it should already be strippingX-Frontcache-Client-IP. These requests are decided BEFORE the rules run, so no bots.conf rule counts a hit for them. That is the intended accounting, and the same reason the rate limiter'sscope=toplevelexists: the visitor was already classified once, when their page was requested, and counting their fragments would count them again once per fragment.- Returns:
- the propagated client type, or null when this request must be classified on its own
- the peer is trusted (
-
getClientIP
-
hasMalformedQueryParams
Detects a structurally-invalid request query string whose parameter names carry the HTML-entity artifactamp;(raw, or percent-encoded asamp%3B).A broken crawler that fails to decode
&in the origin's hrefs turns a real parameter such as&pagingPage_ci=2into a bogus parameter namedamp;pagingPage_ci, and theamp;prefix compounds (amp;amp;...) on every crawl hop. These are never legitimate parameters; worse, each unique permutation is a distinct cache key, so left unchecked they all miss the cache and flood the origin (~26% of prod traffic on fc-ap). Such requests are rejected with 400 before reaching cache or origin.- Parameters:
queryString- raw request query string, optionally starting with '?'; may be null/empty- Returns:
- true if any parameter name is or starts with
amp;
-
httpResponse2WebComponent
public static WebResponse httpResponse2WebComponent(String url, FrontCacheHttpResponseWrapper originWrappedResponse, RequestContext context) throws FrontCacheException, IOException is used in ServletFilter mode- Parameters:
url-originWrappedResponse-- Returns:
- Throws:
FrontCacheExceptionIOException
-
httpResponse2WebComponent
public static WebResponse httpResponse2WebComponent(String url, org.apache.hc.core5.http.ClassicHttpResponse response, RequestContext context) throws FrontCacheException, IOException - Throws:
FrontCacheExceptionIOException
-
transformRedirectURL
Rewrites a Location the origin sent so it names this site rather than the origin. The origin's Location is treated as a path and query with a scheme hint: its host is always discarded (the origin knows itself by an internal name - the app seesHost: origin.example.combecause buildRequestHeaders drops the client's Host) and its port always comes from our own config. Three things changed here in docs/archive/guard-redirect-url-host-fix.md, all of them silent before:- the substituted host is the site's public name, not the Host this request arrived under - same distinction as GuardAction.publicHost, same reason;
- the port is omitted when it is the default for the scheme. It used to be appended
unconditionally, so every app redirect on a normal deployment went out as
https://www.example.com:443/...; - an absolute origin-host URL sitting INSIDE the query string (a
return=/next=parameter) is rebased too. Rewriting only the outer host is what let the origin's name reach browsers through the one part of the Location nobody was looking at.
-
revertHeaders
-
revertHeaders
-
convertHeaders
-
getRequestURL
- Parameters:
request-- Returns:
-
isGzipped
return true if the client requested gzip content- Parameters:
contentEncoding-- Returns:
- true if the content-encoding containg gzip
-
buildWebComponent
public static WebResponse buildWebComponent(String url, byte[] content, Map<String, List<String>> headers) Builds aWebResponsefrom a body and a header map, applying the samex-frontcache-component-*reading every origin response goes through. The public door ontoparseWebComponent(String, byte[], Map), for the one caller that has a fragment and its headers but no HTTP response to hand: the include processor unpacking a combined response (docs/archive/combine-reduce-proposal.md section 6). Routing it here rather than letting that code assemble a WebResponse itself is what keeps maxAge, tags, refresh type and cache level meaning exactly the same for a combined member as for a single include - the invariant in section 4.2, which is silent when broken.- Parameters:
headers- the part's headers; a case-insensitive map is expected (header names are case-insensitive per RFC 7230) and is what the caller is given
-
maxAgeStr2Int
-
buildRequestURI
-
buildRequestURI
-
canonicalizeURL
The URL a request forurlStrwill actually be made with: scheme and host untouched, path and query normalised exactly asbuildRequestURI(String)normalises them for the wire. This is the only correct cache key for a URL that is fetched through buildRequestURI, and the two must not be allowed to drift apart. buildRequestURI re-serializes the query and silently drops any parameter with no value -splitQueryParametersmapsid=to a null value, because its guard ispair.length() > idx + 1, which for"id="is3 > 3. So a URL keyed raw and fetched normalised is a cache entry that can never be read back:probe ?locale=zh&id= miss - nothing is ever stored under this fetch ?locale=zh the empty parameter is gone by the time it reaches the wire store ?locale=zh what the serving node receives, and stores probe ?locale=zh&id= miss again, for the life of the entry
That loop ran in production: 33 fragment URLs generating 608 origin fetches a second, each one re-fetching a fragment that was already cached, unexpired and cacheable - because the key being looked up had never existed. See docs/archive/input-request-count-fix.md section 3. Idempotent: canonicalizing an already-canonical URL returns it unchanged, so it is safe to apply on a path that may already have applied it. -
rebaseURL
The same path and query, addressed at a differentscheme://host[:port]. This is what splits an include's two jobs apart (docs/archive/include-fetch-fix.md section 5). A cache-missing<fc:include>is keyed on the public URL and fetched fromfront-cache.origin-host; the two differ only in the base, so the fetch URL is derived from the public one rather than re-concatenated from the tag - re-concatenating would give a second string to keep in step withcanonicalizeURL(String), and that drift is what section 2 of docs/archive/input-request-count-fix.md was. The base is expected without a trailing slash, which is whatFrontCacheEngine.makeURLandURL.toString()produce for a host-only URL.- Parameters:
urlStr- absolute URL, or a path (which is returned appended to the base)baseURL-scheme://host[:port]
-
splitQueryParameters
public static Map<String, List<String>> splitQueryParameters(String paramsStr) throws UnsupportedEncodingException - Throws:
UnsupportedEncodingException
-
keepAliveStrategy
public static org.apache.hc.client5.http.ConnectionKeepAliveStrategy keepAliveStrategy()The keep-alive strategy shared by the engine's origin client and FrontCacheClient: honour the origin's ownKeep-Alive: timeout=N, defaulting to 10s when it does not say. Extracted during the HttpClient 5 migration - the same anonymous class was pasted into FrontCacheEngine and FrontCacheClient (and into FrontCacheAgent, which cannot share it because the agent must not depend on frontcache-core). 5.x also replaced the hand-rolled BasicHeaderElementIterator with MessageSupport.iterate(). -
getHttpHost
-
buildRequestHeaders
-
buildRequestHeaders
public static Map<String, List<String>> buildRequestHeaders(jakarta.servlet.http.HttpServletRequest request, boolean normalizeAcceptEncoding) - Parameters:
normalizeAcceptEncoding- true to replace the client's Accept-Encoding withgzip(what every cacheable path wants - see below); false to forward the client's own, which only the bypass path does, and only when encoding passthrough is enabled
-
addPublicForwardedHeaders
public static void addPublicForwardedHeaders(Map<String, List<String>> headers, RequestContext context) Adds X-Forwarded-Host / X-Forwarded-Proto naming the public site to an origin call, when front-cache.origin.forward-public-host is on. The origin cannot otherwise know what the visitor typed:isIncludedHeader(String)drops the client's Host (it has to - the connection is to the origin, not to the site), so the app seesHost: origin.example.comand every absolute URL it builds names the origin. Apps work around that one entry point at a time; these two headers let the app's own framework (Spring's ForwardedHeaderFilter, Rails' trusted proxies, …) get it right everywhere at once. Off by default, because switching it on changes every absolute URL an app so configured emits. Adds nothing when the node names no public host - a made-up value here would be worse than the origin host the app already has. -
writeResponse
Streamsintoout. Neither is closed or flushed here - every caller already does that in afinally. The previous version had three problems, all of them on the path that streams a bypassed origin response to the client:- it called
out.flush()after EVERY chunk, so a 5 MB download meant thousands of flushes - a write syscall each, and a chunked-transfer boundary on the wire; - the buffer started at 1 KB and DOUBLED every time a read filled it, with no ceiling - allocating and abandoning 2 KB, 4 KB, ... up to megabytes while streaming from a fast origin;
- it caught IOException INSIDE the loop and printed a stack trace. A client disconnect (broken pipe) was therefore swallowed, and the loop kept pulling the whole origin body into a socket nobody was listening to, printing a trace per kilobyte.
finallyblocks release the pooled origin connection, and a disconnected client should end the transfer rather than be written to 5000 more times.- Throws:
IOException
- it called
-
isAuthorizedApiKeyHeader
public static boolean isAuthorizedApiKeyHeader(jakarta.servlet.http.HttpServletRequest request) Reads the credential throughApiKey.presented(HttpServletRequest)- so the stream acceptsAuthorization: Bearer <api-key>like everything else, and the deprecatedx-frontcache-site-keyfor as long as that is accepted anywhere. The DECISION stays here and stays stricter thanApiKey.isAuthorized(HttpServletRequest): a node with no API key configured refuses the stream, where it would allow a management call. That difference is long-standing and deliberate - see the ApiKey javadoc - so unifying the header did not unify it. -
applyFallbackHeaders
public static void applyFallbackHeaders(jakarta.servlet.http.HttpServletResponse servletResponse, WebResponse webResponse) Apply a fallbackWebResponse's status code and headers (content type plus the non-cacheable Cache-Control/Pragma markers) to the servlet response. Used by the commands that stream the fallback body directly, so that the 503 status and no-store headers set on the fallback actually reach the client / downstream caches. Must be called before the body is written.- Parameters:
servletResponse- target responsewebResponse- fallback response carrying status and headers
-