steid

@jamesgill /

docs: probe git http-backend's CGI contract

Step one of Milestone 4a, deliberately no application code: learn what the
binary actually wants before writing an adapter against a guess.

The finding that justifies the step is the gzip variable name. CGI prefixes
every request header except Content-Type and Content-Length with HTTP_, so
http-backend reads HTTP_CONTENT_ENCODING; passing CONTENT_ENCODING hands the
still-compressed body to upload-pack. Nothing in the resulting failure names
gzip or the environment — the client just says "expected 'packfile'" — and a
one-ref test repository never triggers it, because the client only compresses
once the request is large enough.

Also measured: GIT_HTTP_EXPORT_ALL is required, Status: appears only on
failure, headers are CRLF-terminated, receive-pack is refused by default, and
a non-zero exit can arrive after a successful-looking header block has already
been written.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JZwc7URWKVhkAuRTWiDmjA
JamesPatrickGill authored 8 days agoparent6a2a925Browse files8ed37b0133b400309aefed09eb79e7e02c0e2920

1 file changed+50 −0

plans/progress.md+50 −0View file
@@ -195,6 +195,56 @@ handle 404s rather than answering `[]`. `git clone` does not work yet — Milest
195195 database, so removing the column would have been a free in-place edit rather than a
196196 second migration.
197197
198+### Milestone 4a — Clone over HTTP · in progress
199+
200+`git http-backend`'s contract, probed against git 2.50.1 by driving the CGI from a
201+throwaway server and cloning through it. Everything below is measured, not read.
202+
203+#### The contract
204+
205+- **`HTTP_CONTENT_ENCODING`, not `CONTENT_ENCODING`.** CGI gives only `Content-Type` and
206+ `Content-Length` unprefixed names; every other request header is `HTTP_`-prefixed, and
207+ `http-backend` looks for the prefixed one. **This is the finding that would have cost a
208+ day.** With the wrong name, `http-backend` hands the still-compressed body to
209+ `upload-pack`, which dies with `bad line length character` and the client reports
210+ `fatal: expected 'packfile'` — nothing names gzip, or the environment, anywhere in the
211+ failure. And it only happens once a repository has enough refs for the client to bother
212+ compressing: a one-ref test repo passes.
213+- **`HTTP_GIT_PROTOCOL`** carries `version=2` through to `upload-pack`. Clones verified
214+ on v2 and on `protocol.version=0`, both 201 commits, both gzipped.
215+- **`GIT_PROJECT_ROOT` plus `PATH_INFO`**, where `PATH_INFO` is the on-disk path relative
216+ to the root. Steid's URL and its storage layout differ — `/{handle}/repos/{name}.git/…`
217+ against `{data_dir}/{handle}/{name}.git` — so the adapter rewrites the middle segment
218+ out. `GIT_PROJECT_ROOT` is the data directory.
219+- **`GIT_HTTP_EXPORT_ALL=1` is required.** Without it every repository answers `Status:
220+ 404 Not Found` and `Repository not exported`, unless a `git-daemon-export-ok` marker
221+ file sits in the bare repo (confirmed: dropping that file in re-enables it).
222+- **Headers are CRLF-terminated and end at `\r\n\r\n`.** No bare-LF variant was
223+ observed, but the adapter should accept one rather than hang.
224+- **`Status:` appears only on failure.** Its absence means 200, and it must be
225+ translated, not forwarded as a header.
226+- **`http-backend` sets its own `Content-Type` and cache headers** — `Expires: Fri, 01
227+ Jan 1980`, `Pragma: no-cache`, `Cache-Control: no-cache, max-age=0, must-revalidate`.
228+ Steid forwards them rather than inventing its own.
229+
230+#### Decisions worth remembering
231+
232+- **`GIT_HTTP_EXPORT_ALL`, never `git-daemon-export-ok`.** The marker file is git's own
233+ visibility mechanism and it looks tempting, but visibility lives in the `repositories`
234+ table and the use case is what enforces it. A marker file would be a second source of
235+ truth for the same question, free to drift from the first, and the drift direction is
236+ "private repository still clonable". Steid decides; git is told to stop asking.
237+- **`receive-pack` is refused by default** — `Status: 403 Forbidden`, `Service not
238+ enabled: 'receive-pack'`, without any configuration. Convenient for 4a, but the routes
239+ still refuse writes explicitly rather than leaning on it: a default that helpfully
240+ changes is not an authorization decision.
241+- **A non-zero exit can arrive after the headers are already out.** The gzip failure
242+ exited 1 having emitted a complete, successful-looking header block. So the exit code
243+ cannot gate the response — by the time it is known, the status is sent. It belongs in
244+ the log.
245+- **A missing repository is `Status: 404` with exit 0.** Failure is reported in the
246+ CGI stream, not the exit code, and the two disagree in both directions.
247+
198248 ---
199249
200250 ## Reference: what attempt #2 proved