Debug Git HTTP Remotes with the Protocol's First Requests
You will check a Git repository served over HTTP by following the same first exchange that a Git client uses: discover references, identify the smart or dumb response, then test a read or write operation. 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 that you are allowed to inspect. The checks are 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 that a user would give to Git. In the examples, replace the placeholder with your own value and do not include a trailing slash:
repo_url='https://git.example.test/team/project.git'
printf '%s\n' "$repo_url"
The protocol treats the user-supplied URL as a base and appends paths such as /info/refs. A trailing slash would create an empty path component, so compatible clients remove it before constructing a request. Keep the repository path and any query string intact while checking 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 diagnosing it. A typo in the host or path should be fixed only after you have recorded the value that 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. The useful rule from the protocol is that a missing repository must not return 200 OK; a server should normally use 404 Not Found or 410 Gone. If the repository exists but access is denied, the required status is 403 Forbidden. A login page with 200 OK is therefore not a successful Git endpoint, even if a browser displays it neatly.
This request does not authenticate or change repository state. If the server redirects to HTTPS, follow that redirect only when you understand the trust boundary:
$ curl --silent --show-error --include --location --output /dev/null "$repo_url"
HTTP/2 200
...
Do not put passwords in the URL or paste authenticated headers into a ticket. Basic authentication over plain HTTP exposes credentials; use HTTPS when password authentication is involved.
3. Test smart ref discovery first
Modern Git clients request the 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 has a content type of 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. The 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 parameter while testing. A server that does not recognise or has disabled the service should return 403 Forbidden, which is different 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 to be enabled by the server for their respective operations. A successful fetch discovery does not prove that pushes are allowed.
4. Recognise a dumb response and its limits
Older or simpler HTTP hosting can expose the dumb protocol. Its reference discovery request has 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 containing an object ID, a tab, and a reference name on each line. The file should not advertise HEAD. A smart client may fall back to this response when the smart request returns another content type, provided it supports the dumb protocol.
Use Git itself to see whether the endpoint is usable:
$ git ls-remote "$repo_url"
95dcfa3633004da0049d3d0fa03f80589cbcaf31 HEAD
95dcfa3633004da0049d3d0fa03f80589cbcaf31 refs/heads/main
Your object IDs and branch names will differ. A successful command proves that Git could discover references and authenticate, but it does not prove that a complete clone or push will work.
5. Trace a safe read before attempting a push
Run a normal read operation after discovery succeeds:
$ git clone --filter=blob:none "$repo_url" project-check
Cloning into 'project-check'...
...
The HTTP fetch uses a POST to /git-upload-pack. The request body contains at least one want object, followed by any have objects and a flush packet. The response is a pack stream; it must not be reused from a cache. If discovery works but cloning fails, inspect the server's handling of POST, request bodies, response buffering and the application/x-git-upload-pack-request content type.
The clone creates files and a new directory, so check that project-check does not already exist. If the clone is only a diagnostic and you no longer need it, remove that directory after checking its contents. This is the first destructive command in this guide: verify the path before running it, and never replace a real checkout with the example path.
6. Treat a push as a separate security decision
Only test a push when you have explicit permission and a disposable branch. The write request uses git-receive-pack, sends a command describing old and new object IDs, and then sends a pack. A working read path does not grant write access.
$ git switch -c http-transport-check
$ git commit --allow-empty -m 'Test HTTP push path'
$ git push --set-upstream origin http-transport-check
Before the last command, confirm origin, the branch name, and the server's retention policy. This changes remote state. If the push 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, preserve the error and check whether git-receive-pack is disabled, the authenticated identity has write permission, or a proxy rejects 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 has been disabled. Check server policy and credentials rather than changing the URL at random.
- 200 with HTML: likely a login page, error page or catch-all application. Git needs the protocol response, not a browser-friendly page.
- Smart discovery works but 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 perspective. 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, but discovery can return 304 Not Modified, which clients treat like 200 OK by reusing the cached entity. Upload-pack and receive-pack results must not be reused or revalidated as cached responses.
Done means
- The repository URL has no accidental trailing slash and has been recorded before changes.
- The base request distinguishes missing resources, forbidden access and a misleading HTML
200. - Smart discovery returns the expected service content type and pkt-line markers, or dumb discovery returns ref records without a query string.
git ls-remotesucceeds before a clone is attempted.- Any clone directory and push branch were explicitly checked, and remote writes were treated as privileged changes.