# Erratum — draft-fossil-sync-protocol-00 §4 (redirect, single /xfer strip)
| | |
|---|---|
| **Document** | `draft-fossil-sync-protocol-00` |
| **Section** | 4 — Transport (redirect following) |
| **Type** | Technical (correction — Fossil strips one trailing `/xfer`, not repeatedly) |
| **Status** | Proposed — operator decision pending |
| **Reported** | 2026-10-11 |
| **Submitter** | Dan Mestas |
| **Found via** | go-libfossil's conformance suite against Fossil 2.28 (scope note SP-4-6, case `sync.4.redirect-strips-xfer`); confirmed by reading Fossil's source and by redirecting a Fossil client |
| **Amends** | `sync-protocol-00-s4-redirect-xfer-strip.md` (2026-07-10), whose "repeated while such a segment remains" wording zit's draft carries |
## 1. Summary
§4 says to "Repeatedly strip trailing /xfer from the redirect target". Fossil
strips exactly one: a redirect to `/b/xfer/xfer` is followed to `/b/xfer`.
The source has a `while` loop, but the loop cannot run twice, so the
observable rule is a single strip.
The 10 July erratum that introduced the `/xfer` rule said the strip "is
repeated while such a segment remains". It was right about `/xfer` and wrong
about the repetition; zit's draft and the 29 July copy both carry that
wording.
## 2. Location
§4, redirect paragraph (~lines 847-849):
> Repeatedly strip trailing /xfer from the redirect target, then parse the
> result as the new base; apply no other path transformation.
## 3. The divergence
Source, Fossil 2.28 `http_exchange()` (`src/http.c:633-637`):
```c
j = strlen(zLine) - 1;
while( j>4 && fossil_strcmp(&zLine[j-4],"/xfer")==0 ){
j -= 4;
zLine[j] = 0;
}
```
`j` starts on the last character, and the test compares the five characters
ending at `j`. A match truncates at the `/`, which is `j-4`, but leaves `j` on
the new terminator rather than the new last character. The next test then
compares a four-character tail with the five-character `"/xfer"`, which can
never match. One strip, whatever the target.
Run: a scripted HTTP peer answered the first POST of `fossil pull` with
`307 Location: http://127.0.0.1:PORT/b/xfer/xfer`. Fossil printed
`redirect with status 307 to http://127.0.0.1:PORT/b/xfer` and sent its second
POST to `/b/xfer`.
## 4. Impact
Only targets ending in `/xfer/xfer` are affected, which in practice means a
repository whose own URL ends in `/xfer`. For those, a client following the
draft goes one level too far (`/b`) where Fossil goes to `/b/xfer`. Fossil's
result is the one that keeps the repository path: only the endpoint suffix
the server appended is removed.
## 5. Correction
```diff
- or local-file transports. Repeatedly strip trailing /xfer from the
- redirect target, then parse the result as the new base; apply no
- other path transformation. For an accepted redirect, Fossil parses
+ or local-file transports. Strip one trailing /xfer, if present, from
+ the redirect target, then parse the result as the new base; apply no
+ other path transformation. For an accepted redirect, Fossil parses
```
## 6. Rationale
- **It is what Fossil does,** and the case cannot be written to match the
current text.
- **It is the better rule.** The suffix being undone is the single `/xfer` a
server appends to name its sync endpoint. Repeating the strip can remove
part of a repository path that happens to end in `xfer`.
- **Keeping the loop's apparent intent would buy nothing.** No server needs a
second strip, so describing the loop as written by its effect is safe.
## 7. Verification
- Source: Fossil 2.28 `src/http.c:616-637` (the 301/302/307/308 branch and the
strip loop).
- Run: Fossil 2.28 `fossil pull` against a scripted peer that redirects the
first request to `/b/xfer/xfer`; the second request's path is `/b/xfer`.
- go-libfossil's case uses a single `/xfer` for this reason (comment above
`stripXfer` in `conformance/sync_http_cases_test.go`).
---
*Prepared against `draft-fossil-sync-protocol-00`. The RFC remains the oracle;
the reference implementation is used only to confirm the observable behavior.*