ExtraHeadersFor duplicates http.extraHeader on auth retry, causing infinite 401 loop · Issue #6202 · git-lfs/git-lfs · GitHub
Skip to content

ExtraHeadersFor duplicates http.extraHeader on auth retry, causing infinite 401 loop #6202

Closed

Description

@ypfaff

Describe the bug

ExtraHeadersFor() duplicates http.<url>.extraHeader values on every authentication retry, producing malformed headers that cause an infinite 401 loop.

When a custom authentication header is configured via http.extraHeader and lfs.access is not set to basic, Git LFS retries authentication using the same *http.Request object. Each retry calls ExtraHeadersFor(), which copies all existing headers (including previously appended extra headers) and appends the extra headers again. After two iterations the header value is already malformed, causing every subsequent request to fail with 401 — which triggers another retry, ad infinitum.

To Reproduce

  1. Configure a custom auth header:

    git config --global "http.https://git.example.com.extraHeader" "Authorization: bearer <token>"
  2. Clone a repo with LFS content, without lfs.access set to basic:

    git clone https://git.example.com/org/repo.git
  3. Git LFS prompts for credentials. Entering valid credentials leads to an infinite loop of password prompts.

Expected behavior

Extra headers should be applied exactly once per request, regardless of how many auth retries occur. Valid credentials should be accepted on the retry.

Root cause

Two issues interact:

1. Header duplication in ExtraHeadersFor (lfshttp/client.go:237–254)

func (c *Client) ExtraHeadersFor(req *http.Request) http.Header {
    extraHeaders := c.extraHeaders(req.URL)
    // ...
    copy := make(http.Header, len(req.Header))
    for k, vs := range req.Header {
        copy[k] = vs                        // copies ALL headers, including previous extra headers
    }
    for k, vs := range extraHeaders {
        for _, v := range vs {
            copy[k] = append(copy[k], v)    // appends extra headers AGAIN → duplicates
        }
    }
    return copy
}

This is called on every auth retry via doWithAuth (lfsapi/auth.go:68):

req.Header = c.client.ExtraHeadersFor(req)  // same req on every iteration

Result per retry:

Attempt 1: Authorization: bearer <token>
Attempt 2: Authorization: bearer <token>, bearer <token>           ← malformed
Attempt 3: Authorization: bearer <token>, bearer <token>, bearer …  ← malformed

2. No recursion depth limit in DoWithAuth (lfsapi/auth.go:26–43)

func (c *Client) DoWithAuth(remote string, access creds.Access, req *http.Request) (*http.Response, error) {
    count := 0  // fresh counter on every recursive call
    res, err := c.doWithAuth(remote, &count, access, req, nil)

    if errors.IsAuthError(err) {
        if len(req.Header.Get("Authorization")) == 0 {
            // ...
            return c.DoWithAuth(remote, newAccess, req)  // recursive, bypasses defaultMaxAuthAttempts
        }
    }
    return res, err
}

Each recursive call to DoWithAuth resets count to 0, so defaultMaxAuthAttempts is never reached.

Proposed fix

Replace append with assignment in ExtraHeadersFor so extra headers are set idempotently:

for k, vs := range extraHeaders {
    copy[k] = vs  // replace instead of append — idempotent across retries
}

This ensures extra headers are applied exactly once regardless of how many times the function is called with the same request.

Workaround

Set lfs.access to basic so credentials are included on the first request, avoiding the retry path:

git config --global lfs.access basic

System environment

Not environment-specific. The bug is in the auth retry logic and affects all platforms. Verified against current main (commit bb65882).

Output of git lfs env

N/A — the bug is reproducible with any environment that uses http.extraHeader for authentication without lfs.access=basic.

Activity

  1. chrisd8088 commented on Feb 9, 2026

    @chrisd8088
    Member

    Hey, thanks for the bug report and reproduction case!

    As it happens, we're already aware of the HTTP recursion issue; see the notes in PR #6018 for details. I have continued to expand that PR and expect we'll be able to merge it (with additional changes) in the next couple of weeks.

    Once that's done, we can revisit this issue and see what remains to be resolved with respect to the handling of extra headers.

  2. moved this to Bugs in Backlogon Feb 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    • Status
      Bugs

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions