Debug Git HTTP Remotes with the Protocol's First Requests
Debug a Git-over-HTTP remote by replaying the client's first few requests yourself, before you touch a write. Discover references, work out smart or dumb, then test a read. The examples describe Git 2.43.0, the version documented by the installed gitprotocol-http(5) manpage. Allow about 15 minutes. You need a shell, curl, Git, and a repository URL you are allowed to inspect. The checks stay read-only until you deliberately run a push.
The route
Jump straight to the step you need, or tick off Done means at the end.
1. Record the repository URL exactly
Start with the HTTP URL a user would hand to Git. Replace the placeholder with your own value, and leave off the trailing slash:
repo_url='https://git.example.test/team/project.git'
printf '%s\n' "$repo_url"
The protocol treats the URL you supply as a base and appends paths such as /info/refs. A trailing slash would create an empty path component, so compatible clients strip it before building a request. Keep the repository path and any query string intact while you check it; a gateway URL may deliberately look like https://git.example.test/daemon.cgi?svc=git&q=.
Checkpoint
If the URL came from a remote, compare it with the repository's configured value:
$ git remote -v
origin https://git.example.test/team/project.git (fetch)
origin https://git.example.test/team/project.git (push)
Do not edit the remote while you are still diagnosing it. Fix a typo in the host or path only after you have recorded the value that actually failed.
2. Check the base resource without pretending success
Ask the web server for the base URL and show its response headers:
$ curl --silent --show-error --include --output /dev/null "$repo_url"
HTTP/1.1 200 OK
...
The exact response depends on the web server, but the protocol's rule is firm: a missing repository must not return 200 OK, a server should normally answer 404 Not Found or 410 Gone. If the repository exists but access is denied, the required status is 403 Forbidden. A login page answering 200 OK is not a working Git endpoint, however tidy it looks in a browser.
This request neither authenticates nor changes repository state. If the server redirects to HTTPS, follow it only once you understand the trust boundary:
$ curl --silent --show-error --include --location --output /dev/null "$repo_url"
HTTP/2 200
...
Warning
Do not put passwords in the URL, and do not paste authenticated headers into a ticket. Basic authentication over plain HTTP exposes credentials outright; use HTTPS whenever password authentication is involved.
3. Test smart ref discovery first
Modern Git clients ask for references and name the service they want in one query parameter. For a fetch, that service is git-upload-pack:
$ curl --silent --show-error --include \
"$repo_url/info/refs?service=git-upload-pack" \
| sed -n '1,12p'
HTTP/1.1 200 OK
Content-Type: application/x-git-upload-pack-advertisement
Cache-Control: no-cache
...
001e# service=git-upload-pack
0000
...
A smart response carries the content type application/x-git-upload-pack-advertisement. Its body is a pkt-line stream: the first record names the requested service, the stream ends with 0000, and the first reference record carries capability declarations after a NUL byte. Those hexadecimal prefixes are lengths, not ordinary text to edit.
The request must contain exactly one query parameter, service=git-upload-pack. Do not add an unrelated one while testing. A server that does not recognise or has disabled the service should answer 403 Forbidden, which is a different problem from a missing repository.
For a push-capable endpoint, repeat the discovery request with git-receive-pack:
$ curl --silent --show-error --include \
"$repo_url/info/refs?service=git-receive-pack" \
| sed -n '1,12p'
Both services need enabling separately on the server. A successful fetch discovery does not prove pushes are allowed.
4. Recognise a dumb response and its limits
Older or simpler HTTP hosting can expose the dumb protocol instead. Its reference discovery request carries no query parameters:
$ curl --silent --show-error --include \
"$repo_url/info/refs" \
| sed -n '1,12p'
HTTP/1.1 200 OK
...
95dcfa3633004da0049d3d0fa03f80589cbcaf31 refs/heads/main
2cb58b79488a98d2721cea644875a8dd0026b115 refs/tags/v1.0
The body is UNIX-format text: an object ID, a tab, a reference name, one per line. It should never advertise HEAD. A smart client may fall back to this response when the smart request returns a different content type, provided it supports the dumb protocol at all.
Use Git itself to see whether the endpoint is actually usable:
$ git ls-remote "$repo_url"
95dcfa3633004da0049d3d0fa03f80589cbcaf31 HEAD
95dcfa3633004da0049d3d0fa03f80589cbcaf31 refs/heads/main
Your object IDs and branch names will differ. Success here proves Git could discover references and authenticate, nothing more: it does not prove a full clone or push will work.
5. Trace a safe read before attempting a push
Run a normal read once discovery succeeds:
$ git clone --filter=blob:none "$repo_url" project-check
Cloning into 'project-check'...
...
The HTTP fetch sends a POST to /git-upload-pack. The request body carries at least one want object, any have objects, then a flush packet. The response is a pack stream that must never be reused from a cache. If discovery works but the clone fails, look at the server's handling of POST, request bodies, response buffering and the application/x-git-upload-pack-request content type.
Warning
The clone creates a new directory, so check project-check does not already exist. This is the first destructive command in this guide: verify the path first, and never let it replace a real checkout. Remove the directory afterwards if it was only a diagnostic.
6. Treat a push as a separate security decision
Only test a push with explicit permission and a disposable branch. The write request uses git-receive-pack, sends a command describing old and new object IDs, then sends a pack. A working read path grants nothing on the write side.
$ git switch -c http-transport-check
$ git commit --allow-empty -m 'Test HTTP push path'
$ git push --set-upstream origin http-transport-check
Warning
Confirm origin, the branch name and the server's retention policy before that last command runs; it changes remote state. If it succeeds, remove the temporary branch through the repository's normal review and administration process, not by guessing at a raw HTTP request. If it fails, keep the error and check whether git-receive-pack is disabled, the authenticated identity has write permission, or a proxy is rejecting the request body. Never retry a write blindly against a production branch.
7. Interpret the common failure patterns
- 404 or 410 at the base URL: the path does not identify a repository, or the web server is not mapping it where you expect.
- 403 on smart discovery: access is forbidden, or that service is disabled. Check server policy and credentials rather than changing the URL at random.
- 200 with HTML: probably a login page, error page or catch-all application. Git needs the protocol response, not a browser-friendly page.
- Smart discovery works but the clone fails: investigate the upload-pack POST and proxy handling, including request size and buffering limits.
- Fetch works but push fails: check receive-pack enablement and authorisation; fetch and push are separate services.
- Intermittent results behind a load balancer: HTTP Git is stateless from the server's side. The client retains operation state, so servers should not require cookies or session affinity.
Caching is another frequent distraction. Servers may send ETag or Last-Modified headers for cacheable entities, and discovery can return 304 Not Modified, which clients treat like 200 OK by reusing the cached entity. Upload-pack and receive-pack results must never be reused or revalidated as cached responses.
Done means
- Recorded the repository URL with no accidental trailing slash, before making changes.
- Told missing, forbidden and misleading HTML responses apart at the base request.
- Confirmed the discovery response: smart discovery returns the expected content type and pkt-line markers, or dumb discovery returns ref records with no query string.
- Ran
git ls-remotesuccessfully before attempting a clone. - Checked the clone directory and push branch explicitly, and treated remote writes as privileged changes.