HTTP Garden: differential testing across HTTP server implementations
Differential testing framework for HTTP implementations
At a glance
- What is it?
- The HTTP Garden is a collection of HTTP servers and proxies, each in its own Docker image, that you drive from a REPL so the same payload can be sent to many implementations at once and their interpretations compared. It suits security engineers hunting request smuggling and parsing differences, and it suits anyone who needs to know exactly how a server reads a malformed request.
- Who is it for?
- Adopt the HTTP Garden if you are hunting HTTP parsing differences across real server implementations, or you need a defensible answer to how a specific server reads an ambiguous request, and you have Docker and a Linux host to run it on. Do not adopt it as a general fuzzing engine or a general HTTP test suite, since it compares implementations against each other rather than against a specification.
- Can I use it commercially?
- Yes, with conditions. GPL-3.0 is a copyleft licence: if you distribute software that includes it, you must release that software's source code under the same licence. Running it internally without distributing it does not trigger that obligation.
- Is it still maintained?
- Yes. The repository last received commits 148 days ago.
- What is it written in?
- Mainly Python, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A comparison bed for HTTP parsers, not a fuzzer
The HTTP Garden is a collection of HTTP servers and proxies configured to be composable, with scripts to interact with them in a way the README says makes finding vulnerabilities much easier. The description calls it a differential testing framework for HTTP implementations, and that phrase is the whole idea.
The problem it addresses is that HTTP/1.1 parsing has more than one reasonable reading. A request with a bare LF line ending in a chunked body, or a duplicated header, or an unusual whitespace pattern, can be rejected by one server, silently accepted by another, and interpreted differently by a third. A proxy and its origin server that disagree about where one message ends and the next begins is the classic request smuggling setup. Finding that by hand means writing a new client for every server you want to compare.
The Garden removes the repetition. One payload goes to every target, and the results are laid out next to each other so a disagreement is visible. The README also points to a ShmooCon 2024 talk with demos of the vulnerabilities the Garden can find, which is the best available sense of what a working session looks like.
This is a research and engineering tool, and it comes with the usual apparatus. The project was supported by DARPA under contract number HR0011-19-C-0076, and the acknowledgements name Galois, Trail of Bits, Narf Industries and Dartmouth College. The licence is GPL-3.0, and the repository is not archived, with the last push on 2026-05-28 and no GitHub releases published.
A REPL with four verbs: payload, transduce, fanout, grid
The user interface is a REPL, implemented in tools/repl.py, and the vocabulary is four words. A payload sets the raw bytes. A transduce pipes them through one server or proxy, so a proxy can sit in front of an origin. A fanout sends the same payload to every selected target. A grid renders the interpretations side by side.
The first example in the README sends a basic GET request through HAProxy and then to Gunicorn, Hyper and Nginx as origin servers, and displays whether their interpretations match.
garden> payload 'GET / HTTP/1.1\r\nHOST: a\r\n\r\n' | transduce haproxy | fanout | gridThe output is a matrix with one row and one column per target, with check marks where pairs of targets agree. The README explains the detail that makes the output usable: Nginx added content-length and content-type headers that the other targets did not, and the Garden is aware of that and does not let the insignificant discrepancy show up in the grid output.
The second example is where the tool earns its keep. The payload is a POST with a chunked body terminated by a bare LF, which the README notes is disallowed in the spec and links to the relevant errata. Gunicorn answers 400, Hyper does not answer at all, and Nginx accepts the request as a POST with a chunked transfer encoding.
The grid in that case is not a wall of green. It shows a disagreement, and the README draws the conclusion plainly: this is a spec violation by the Nginx authors, with a link to a mailing list thread where they say they are not going to fix it.
Every target is a Docker image pinned to a commit hash
The images directory holds a subdirectory for each HTTP server and transducer, and each target gets its own Docker image. The README states that all programs are built from source when possible, which is the point of the exercise: you are testing the actual server code, not a packaged binary with an unknown patch level.
The pinning scheme is the part worth copying. All targets are parametrized with a repository URL, a branch name and a commit hash, named APP_REPO, APP_BRANCH and APP_VERSION, so that multiple versions of the same target can be built side by side. The docker-compose.yml in the repository root shows the pattern for aiohttp.
aiohttp:
build:
args:
APP_BRANCH: master
APP_REPO: https://github.com/aio-libs/aiohttp
APP_VERSION: 7f0532c1df1915e4367ff4af25cd8453d2df9fc0
context: ./images/aiohttp
x-props:
role: originBecause the version is a commit hash rather than a tag, a result can be reproduced months later, and because the branch is a parameter, you can build the same server at two different points in its history. That is exactly what you want when you suspect a parsing change was introduced by a patch.
The tools directory also holds update.py, a script for updating the commit hashes in docker-compose.yml, so moving the fleet to new commits is a scripted operation rather than a search and replace across dozens of build blocks.
Quirks are enumerated so benign differences do not drown the signal
A differential tool is only useful if its output is mostly meaningful, and most differences between HTTP servers are boring. One server lowercases a header name, another adds a default header, a third normalises whitespace. If every one of those produced a red cell, the grid would be noise.
The Garden handles this with a quirks step. tools/probe_quirks.py is described as a script for enumerating benign HTTP parsing quirks in the systems under test to be ignored during fuzzing, and the findings are stored in a quirks.yml file at the repository root. The nginx content-length and content-type additions from the README example are exactly this kind of quirk.
There is a second level of equivalence, and it is the more interesting one. The README notes that Gunicorn and Hyper did not have exactly the same response, since one returned a 400 and the other returned nothing, yet they showed as agreeing in the grid. Their responses are essentially equivalent, in that both reject the message, and the Garden takes this into account.
That is a semantic judgement embedded in the comparison layer, not a textual one. A tool that compared raw bytes would show a difference here, and a security engineer looking at a wall of differences would learn nothing. Whether that equivalence model is right for your purposes is worth testing on a case you already understand before you trust it on a new one.
Origins and transducers are declared in the compose file
The compose file carries a small custom extension, an x-props key, that records the role of each service. Every service in the file has one, and the values are origin or transducer.
An origin is a server that parses requests and produces responses. A transducer is a component that sits in the path and relays, such as HAProxy in the README example, where the payload is transduced through haproxy and then fanned out to three origin servers. The apache_httpd entry appears twice in the compose file, once with role origin and once with role transducer, because the same source tree is configured two ways: a server.conf for the origin and a proxy.conf for the proxy, selected through the CONFIG_FILE build argument, with START_SCRIPT naming the entrypoint.
The compose file also shows the per target build arguments in action, including multi component targets. apache_traffic_server takes CACHE_CONFIG, RECORDS_YAML and REMAP_CONFIG, and apache_httpd pins its APR dependency with its own APR_REPO, APR_BRANCH and APR_VERSION, so the dependency moves with the target rather than floating.
The tool volume is mounted into the services, which is how the scripts in tools reach a running container. There is also an external-services.yml file at the repository root alongside the compose file, and a TROPHIES.md file that appears to collect what the project has found, which tells you the intended pace of work: build, compare, record, move on.
Running it: Docker, Python, and garden.sh
The README lists the dependencies plainly. The Garden runs on x86_64 and AArch64 Linux and is untested on other platforms, the target servers are built and run in Docker containers so Docker is required, and you need Python 3.12 or newer along with three packages from PyPI or a system package manager: docker for interacting with Docker, pyyaml for parsing yaml, and tqdm for progress bars. The README warns that a system package manager may prefix the names with py3-, python3- or python-.
The packaging metadata in pyproject.toml declares the tools package with those same three dependencies, at least docker 7.1.0, pyyaml 6.0.3 and tqdm 4.67.3, and it asks for Python 3.13 or newer while the README asks for 3.12 or newer. That mismatch is small, but it is the kind of thing that costs an afternoon if you follow the README on a 3.12 interpreter.
Starting the environment is one command, and it builds and starts the named servers and proxies.
./garden.sh start --build gunicorn hyper nginx haproxyThen, from another shell, you start the REPL.
./garden.sh replThat separation is deliberate. The servers are long lived containers and the REPL is the interactive session, so you keep one shell busy with the garden and another with the prompt. The repository also ships a uv.lock file and a pyproject.toml, so the Python side of the tools can be resolved reproducibly even though the README describes installing the three packages directly.
What the Garden will not tell you
The tool has a narrow job and it is worth being explicit about the edges.
It is a comparison tool. Every question it answers is of the form do these implementations agree, and the interesting failures it finds are disagreements between real servers. It is not a specification checker, so a server that is wrong in the same way as every other server, or wrong in a way the others also implement, will not show up as a red cell. The nginx example is a disagreement. A universal misreading is invisible here.
It is also not a general purpose fuzzer. The quirk probing step enumerates benign differences so they can be ignored during fuzzing, which means fuzzing is part of the intended workflow, but the README does not describe a mutation engine, a corpus or a crash triage flow. Treat the fuzzing reference as context for the design rather than as a documented feature list.
The platform constraint is real. x86_64 and AArch64 Linux, untested elsewhere, means this is not something to run on a Windows laptop without a Linux environment, and every target needs Docker, so the footprint on a machine with many targets is a container per target.
Finally, there is no tagged release history to pin to. The repository publishes no GitHub releases, and the last push was on 2026-05-28, so the practical upgrade path is to track the default branch, which for a security tool is a choice with consequences, since a target can move to a new commit hash and change its parsing behaviour between runs. Pin the versions you care about with APP_VERSION and treat the upstream branches as something you choose to follow, not something that follows you.
Editorial conclusion
Adopt the HTTP Garden if you are hunting HTTP parsing differences across real server implementations, or you need a defensible answer to how a specific server reads an ambiguous request, and you have Docker and a Linux host to run it on. Do not adopt it as a general fuzzing engine or a general HTTP test suite, since it compares implementations against each other rather than against a specification. Verify first that you are on x86_64 or AArch64 Linux, that Docker is running, and that your Python version matches what the tools package declares, since the README asks for 3.12 or newer while the packaging metadata asks for 3.13 or newer, then pin your targets with APP_REPO, APP_BRANCH and APP_VERSION before you read anything into a diff.
Frequently asked questions
What is the HTTP Garden used for?
It is a differential testing framework for HTTP implementations: a collection of HTTP servers and proxies, each in its own Docker image, that you drive from a REPL so the same payload can be sent to many of them and their interpretations compared. The goal is making it easier to find vulnerabilities such as request smuggling.
How do I start the HTTP Garden and its REPL?
Build and start the servers and proxies with ./garden.sh start --build gunicorn hyper nginx haproxy, then start the REPL from another shell with ./garden.sh repl. The target servers run in Docker containers, so Docker has to be running first.
What do payload, transduce, fanout and grid do in the HTTP Garden REPL?
A payload sets the raw request bytes, a transduce pipes them through one server or proxy, a fanout sends the same payload to every selected target, and a grid renders the resulting interpretations side by side so disagreements are visible.
How does the HTTP Garden avoid flagging harmless differences?
tools/probe_quirks.py enumerates benign HTTP parsing quirks in the systems under test, and they are recorded in quirks.yml to be ignored during fuzzing. The Garden also treats different-looking but equivalent responses, such as one server returning 400 and another returning nothing for the same malformed message, as agreeing.
Which versions of a target server does the HTTP Garden test?
Each target is built from source in its own Docker image and parametrized with a repository URL, branch name and commit hash named APP_REPO, APP_BRANCH and APP_VERSION, so several versions of the same server can be built side by side. A script in the tools directory updates the commit hashes in docker-compose.yml.
What are the system requirements for the HTTP Garden?
It runs on x86_64 and AArch64 Linux and is untested on other platforms. You need Docker, Python, and the docker, pyyaml and tqdm packages, and the README asks for Python 3.12 or newer while the packaging metadata in pyproject.toml asks for 3.13 or newer.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/narfindustries-http-garden)Was this useful?