Skip to content

TransportHttp: Include HTTP response body in 403 Forbidden error messages #262

@sarat-krk

Description

@sarat-krk

Description

Summary

When JGit receives an HTTP 403 (Forbidden) response during git-upload-pack or git-receive-pack, it constructs a generic error message:

git-upload-pack not permitted on https://github.example.com/org/repo.git

The actual reason for the rejection — which the server provides in the HTTP response body — is discarded. In contrast, the native git client reads and displays the response body, providing a much more informative error:

the repository owner has an IP allow list enabled, and <IP> is not permitted to access this repository.

This makes it significantly harder to diagnose access issues when using JGit-based tooling.

Steps to Reproduce

  1. Configure a GitHub repository (or GitHub Enterprise) with an IP allow list that blocks the client's IP address.
  2. Attempt to clone or fetch using JGit over HTTPS.
  3. Observe the error message.

JGit result:

org.eclipse.jgit.errors.TransportException: https://github.example.com/org/repo.git: git-upload-pack not permitted on https://github.example.com/org/repo.git/

Native git result:

fatal: unable to access 'https://github.example.com/org/repo.git/': The requested URL returned error: 403
the repository owner has an IP allow list enabled, and <IP> is not permitted to access this repository.

Expected Behavior

JGit should read the HTTP error response body on 403 responses and include it in the TransportException message, e.g.:

git-upload-pack not permitted on https://github.example.com/org/repo.git/: the repository owner has an IP allow list enabled, and <IP> is not permitted to access this repository.

Root Cause Analysis

In TransportHttp.java, both HTTP_FORBIDDEN handlers (in connect() at ~line 701 and in Service.sendRequest() at ~line 1710) construct the error message using only the URL and service name:

case HttpConnection.HTTP_FORBIDDEN:
    throw new TransportException(uri, MessageFormat.format(
            JGitText.get().serviceNotPermitted, baseUrl, service));

The HTTP response body (error stream) is never read. The HttpConnection interface also does not expose a getErrorStream() method, so even if the transport wanted to read it, there is no API to do so.

Proposed Fix

Three changes are needed:

1. Add getErrorStream() to HttpConnection interface

File: org.eclipse.jgit/src/org/eclipse/jgit/transport/http/HttpConnection.java

Add a default method (to maintain backward compatibility with custom implementations):

/**
 * Returns an error stream for reading the HTTP error response body
 * on error status codes (4xx, 5xx).
 *
 * @return the error stream, or {@code null} if not available
 * @see java.net.HttpURLConnection#getErrorStream()
 * @since 7.1
 */
default InputStream getErrorStream() {
    return null;
}

2. Implement getErrorStream() in JDKHttpConnection

File: org.eclipse.jgit/src/org/eclipse/jgit/transport/http/JDKHttpConnection.java

@Override
public InputStream getErrorStream() {
    return wrappedUrlConnection.getErrorStream();
}

3. Read error body in TransportHttp on 403 responses

File: org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java

Add a helper method:

private static String readErrorBody(HttpConnection conn) {
    try {
        InputStream errorStream = conn.getErrorStream();
        if (errorStream == null) {
            return null;
        }
        try (BufferedReader reader = new BufferedReader(
                new InputStreamReader(errorStream, UTF_8))) {
            StringBuilder body = new StringBuilder();
            String line;
            while ((line = reader.readLine()) != null) {
                if (body.length() > 0) {
                    body.append(' ');
                }
                body.append(line);
                if (body.length() > 4096) {
                    break; // Limit how much we read
                }
            }
            String result = body.toString().trim();
            // If response is HTML, strip tags for readability
            if (result.contains("<") && result.contains(">")) {
                result = result.replaceAll("<[^>]+>", " ")
                        .replaceAll("\\s+", " ")
                        .trim();
            }
            return result.isEmpty() ? null : result;
        }
    } catch (IOException e) {
        return null;
    }
}

Then update both HTTP_FORBIDDEN cases to:

case HttpConnection.HTTP_FORBIDDEN:
    String forbiddenMsg = MessageFormat.format(
            JGitText.get().serviceNotPermitted, baseUrl, service);
    String errorBody = readErrorBody(conn);
    if (errorBody != null && !errorBody.isEmpty()) {
        forbiddenMsg += ": " + errorBody;
    }
    throw new TransportException(uri, forbiddenMsg);

Motivation

This issue was seen when an enterprise organisation decides to add IP allow list for that specific organisation. And when a clone was done it was not clear what the exact issue was since we thought it was due to token not having enough permission.

Alternatives considered

No response

Additional context

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions