perf(http): enforce max content length before fully downloading the body#180
Merged
Conversation
Both HTTP clients previously buffered the entire response body (memory or temp file) and only then checked it against the max content length, so an oversized or hostile response was fully fetched before being rejected. - Precheck the Content-Length header (when present and parseable) and reject an over-limit response without opening the body, mirroring the existing robots.txt precheck. - Wrap the entity stream in a bounded stream (cap = maxLength+1) so a chunked / unknown-length body is aborted mid-copy; the unchanged post-copy check remains the authoritative accept/reject and preserves the exact threshold. When there is no limit (no ContentLengthHelper, or Long.MAX_VALUE), the stream is used unwrapped. - A malformed/hostile Content-Length header is treated as unknown (the parse is guarded) rather than failing the URL, and when the Content-Type is not yet known the bound uses the overall maximum configured limit so it can never truncate a body the authoritative per-type check would accept. Also make CrawlerContext.robotsTxtUrlSet thread-safe (Collections.synchronizedSet) and collapse the clients' non-atomic contains()-then-add() robots.txt dedup into a single atomic add(), avoiding duplicate robots.txt fetches and concurrent modification of the backing LRU map. Add tests for the precheck, mid-stream capping, within-limit and unlimited cases, malformed headers, the unknown-content-type upper bound, ContentLengthHelper.getMaxLength(), and concurrent robots.txt dedup.
Drop the flaky wall-clock assertion (elapsed < 1000) and the artificial 2s server-side sleep from test_doGet_contentLengthHeaderExceedsMax_rejectedWithoutDownloading in both Hc4HttpClientTest and Hc5HttpClientTest. The precheck-vs-fallback path is already proven deterministically by the reported size: the precheck reports the declared Content-Length (1024 byte), whereas the bounded-stream fallback would report maxLength+1. Removes CI flakiness under load without weakening coverage.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Both HTTP clients (
Hc4HttpClient,Hc5HttpClient) previously copied the entire response body into memory / a temp file and only then compared it against the configured max content length — so an oversized or hostile response was fully downloaded and spilled to disk before being discarded. This enforces the limit before / during the download while preserving the exact accept/reject outcome, and fixes a concurrency issue in the robots.txt dedup.Changes
Max content length (efficiency / DoS mitigation)
Content-Lengththat exceeds the effective max, reject withMaxLengthExceededExceptionwithout opening the body — mirroring the existing robots.txt precheck.BoundedInputStreamcapped atmaxLength + 1, so a chunked / unknown-length oversized body is aborted mid-copy instead of fully buffered. The existing post-copycontentLength > maxLengthcheck is unchanged and remains the authoritative gate, so the accept/reject boundary is provably identical to the old full-download behavior (a body of exactlymaxLengthis accepted;maxLength + 1or larger is rejected).ContentLengthHelper, orLong.MAX_VALUE): the stream is used unwrapped and nothing is prechecked/capped.Content-Lengthheader is treated as unknown (guarded parse) rather than failing the URL. When theContent-Typeis not yet known (header absent), the precheck/bound use the overall maximum configured limit (ContentLengthHelper.getMaxLength()) rather than just the default, so the optimization can never truncate a body that the authoritative per-type check (run after sniffing) would accept. Under Fess's default config (default 10 MB, onlytext/html=2.5 MB ≤ default) this is a no-op.robots.txt dedup (thread-safety)
CrawlerContext.robotsTxtUrlSetis nowCollections.synchronizedSet(new LruHashSet<>(10000))(LRU bound/eviction unchanged), and the clients' non-atomiccontains()-then-add()is collapsed into a single atomicadd()(LruHashSet.addreturnstrueonly when newly added). This removes a race where two threads could both fetch the same host's robots.txt and could concurrently modify the backing map.Testing
Content-Lengthwithout awaiting the body; a chunked oversized body is capped mid-stream; within-limit and unlimited/no-helper cases are unaffected.Content-Lengthheader no longer fails the URL (within-limit body succeeds; oversized body is still capped/rejected by the bound + post-copy check).Content-Typewith a per-type limit larger than the default is not truncated-and-accepted.ContentLengthHelper.getMaxLength()returns the overall maximum; concurrent robots.txtadd()yields a single winner.fess-crawlermodule suite passes (a pre-existing 1 ms-timeout race test is intermittently flaky under parallel load and never touches the modified code);mvn formatter:format/license:format/javadoc:jarclean.