Skip to content

Conversation

@babakks
Copy link
Member

@babakks babakks commented Oct 13, 2025

Fixes #11891

This is a fix + refactor around how gh instantiates special HTTP clients for authentication purposes.

Below is a bit about the changes, and then instructions to verify the PR fixes the UNIX socket support during login.

Rationale

As a bit of context, almost every gh command uses f.HttpClient to get a new HTTP client instance. The client is created by cli/go-gh and is equipped with various layers of custom roundtrip overrides that take care of:

  • Using UNIX sockets, if applicable
  • Setting HTTP request headers (e.g. Authorization, User-Agent, Accept, or Content-Type)
    • The Authorization header is a bit tricky as it gets set at gh side.
  • Adding verbose logging, if applicable

However, there were two places where a zero HTTP client (i.e. http.Client{}) was used instead of the central one; in auth login and in auth refresh. Both cases needed a plain HTTP client for their job and hence the workaround. The problem with these two outliers were:

  1. UNIX socket config was ignored (the underlying issue)
  2. The debug logging had to be duplicated
  3. The User-Agent header was left to Golang's default Go-http-client/2.0
  4. It wasn't testable

Changes

To fix 1 and 2, we needed to use the HTTP client created by cli/go-gh. Thankfully, there is an option named SkipDefaultHeaders in cli/go-gh, that we could just expose and use in cli/cli to create plain HTTP clients.

To resolve 3, we needed to pull up the HTTP client instantiation to the command level where we have the information about the gh version (i.e. f.AppVersion). However, if we were going to do that exactly at the command-level, then we had to something similar to what is done in the factory package, in httpClientFunc (here). The better approach seemed to pull up the changes to the factory package and then provide a plain counterpart to HttpClient in the factory struct. This is what's done in this PR; a new field, named PlainHttpClient is now added to factory.

Problem 4 is now resolved by adding an http.Client parameter to the OAuthFlow function.

Note that in the auth login command, both HTTP client (ordinary and plain) are used. The plain one is only used for the OAuth flow, and the rest of the API calls are made through the ordinary one. Although usages of the ordinary client could be changed to use the plain one (except for one case where a function in ssh add command package is called), but I decided to not do that in case we break things unknowingly; it'd probably be fine, though.

Testing UNIX socket support

Against GHES

It's best to make a testing directory and do these within:

  1. Run make and copy the built binary to the testing directory.
  2. Make a UNIX socket via socat:
    socat -v unix-listen:temp.sock,fork openssl:<HOST>:443
    This will block and show verbose logs, so keep the terminal window open. A UNIX socket, named temp.sock will be created in the working directory.
  3. Open a new shell in the same directory and set the gh config directory once for all.
    export GH_CONFIG_DIR=$(pwd)
    export GH_DEBUG=api
  4. Configure gh to use the UNIX socket:
    ./gh config set http_unix_socket temp.sock
  5. Run auth login, select Other and enter the hostname:
    ./gh auth login -c
    Verify: the login process finished successfully and socat logs contain the same calls as in gh debug output (note the responses in socat logs are gzip-ed so it's fine if request bodies match). FWIW in the issue (gh auth ignores the http_unix_socket config value #11891), the OAuth flow calls didn't pass through the socket.
  6. Run the followings one-by-one and verify:
    ./gh auth refresh -c
    ./gh auth refresh -c --scopes read:public_key
    ./gh auth refresh -c --remove-scopes read:public_key
    ./gh auth status
    Verify: the command finished successfully and the calls are visible on socat logs.
  7. Run arbitrary non-auth commands and verify they work as expected:
    ./gh repo list
    ./gh api user
    ./gh issue list --repo <OWNER>/<REPO>
    ./gh pr list --repo <OWNER>/<REPO>

Against github.com

Testing against github.com is a bit tricky, since we use two different domains during the login process; github.com for OAuth and api.github.com for secondary calls (e.g. discovering scopes). It's best to make a testing directory and do these within:

  1. Run make and copy the built binary to the testing directory.
  2. Make a script, named hack.sh, to dynamically handle arbitrary domains:
    #!/bin/bash
    
    _request="$(timeout 1 cat)"
    _host="$(echo "$_request" | grep -iE "^Host: " | cut -d' ' -f2 | tr -d '\r\n')"
    
    if [ -z "$_host" ]; then
        echo -e "no Host header in request"
        exit 1
    fi
    
    echo "$_request" | openssl s_client -quiet -verify_quiet -tls1_3 -connect "$_host:443"
  3. Make a UNIX socket and socat it to the script:
    socat -v unix-listen:temp.sock,fork system:'bash hack.sh'
    This will block and show verbose logs, so keep the terminal window open. A UNIX socket, named temp.sock will be created in the working directory.
  4. Open a new shell in the same directory and set the gh config directory once for all.
    export GH_CONFIG_DIR=$(pwd)
    export GH_DEBUG=api
  5. Configure gh to use the UNIX socket:
    ./gh config set http_unix_socket temp.sock
  6. Run auth login and select GitHub.com for host:
    ./gh auth login -c
    Verify: the login process finished successfully and socat logs contain the same calls as in gh debug output (note the responses in socat logs are gzip-ed so it's fine if request bodies match). FWIW in the issue (gh auth ignores the http_unix_socket config value #11891), the OAuth flow calls didn't pass through the socket.
  7. Run the followings one-by-one and verify:
    ./gh auth refresh -c
    ./gh auth refresh -c --scopes read:public_key
    ./gh auth refresh -c --remove-scopes read:public_key
    ./gh auth status
    Verify: the command finished successfully and the calls are visible on socat logs.
  8. Run arbitrary non-auth commands and verify they work as expected:
    ./gh repo list
    ./gh api user
    ./gh issue list --repo cli/cli
    ./gh pr list --repo cli/cli

The assertions should check for header values instead of single string,
because when getting a single header value, we get an empty string if
the header is not set or is set to an empty string. So, to make sure a
header is not set we should check the `Values` slice which is `nil` only
if the header is missing.

Signed-off-by: Babak K. Shandiz <babakks@github.com>
Signed-off-by: Babak K. Shandiz <babakks@github.com>
Signed-off-by: Babak K. Shandiz <babakks@github.com>
Signed-off-by: Babak K. Shandiz <babakks@github.com>
Signed-off-by: Babak K. Shandiz <babakks@github.com>
Signed-off-by: Babak K. Shandiz <babakks@github.com>
…lient`

Signed-off-by: Babak K. Shandiz <babakks@github.com>
@babakks babakks requested a review from a team as a code owner October 13, 2025 19:02
@babakks babakks requested review from BagToad and Copilot October 13, 2025 19:02
@babakks babakks linked an issue Oct 13, 2025 that may be closed by this pull request
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR fixes an issue where gh auth login and gh auth refresh commands were not respecting UNIX socket configuration due to using zero HTTP clients instead of the configured ones. The fix refactors HTTP client instantiation to use the factory pattern and adds a new PlainHttpClient that supports UNIX sockets while skipping default headers for authentication flows.

  • Introduces a new PlainHttpClient factory field that creates HTTP clients without automatic headers for OAuth flows
  • Updates authentication commands to use the proper HTTP clients from the factory
  • Removes duplicate HTTP client creation and logging code from the authflow package

Reviewed Changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated no comments.

Show a summary per file
File Description
pkg/cmdutil/factory.go Adds PlainHttpClient field to Factory struct for header-free HTTP clients
pkg/cmd/factory/default.go Implements plainHttpClientFunc to create clients with UNIX socket support
pkg/cmd/factory/default_test.go Adds test coverage for the new plain HTTP client functionality
pkg/cmd/auth/login/login.go Updates login command to use factory-provided HTTP clients
pkg/cmd/auth/refresh/refresh.go Updates refresh command to use factory-provided HTTP clients
pkg/cmd/auth/shared/login_flow.go Adds PlainHTTPClient parameter to LoginOptions
internal/authflow/flow.go Simplifies AuthFlow to accept HTTP client parameter and removes duplicate logging
api/http_client.go Adds SkipDefaultHeaders option to HTTPClientOptions
api/http_client_test.go Updates tests to cover the new SkipDefaultHeaders functionality

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Signed-off-by: Babak K. Shandiz <babakks@github.com>
Comment on lines +735 to +740
assert.Equal(t, 204, res.StatusCode)
assert.Equal(t, []string{"GitHub CLI v1.2.3"}, receivedHeaders.Values("User-Agent"))
assert.Nil(t, receivedHeaders.Values("Authorization"))
assert.Nil(t, receivedHeaders.Values("Content-Type"))
assert.Nil(t, receivedHeaders.Values("Accept"))
assert.Nil(t, receivedHeaders.Values("Time-Zone"))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice 🔥

Comment on lines +33 to +36
// PlainHttpClient is a special HTTP client that does not automatically set
// auth and other headers. This is meant to be used in situations where the
// client needs to specify the headers itself (e.g. during login).
PlainHttpClient func() (*http.Client, error)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thx for the comment 🙇

Copy link
Member

@BagToad BagToad left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Thanks for the write-up and context; it made it straightforward to follow the reasoning behind the PR and the current implementation's problems.

@babakks
Copy link
Member Author

babakks commented Oct 15, 2025

I further tested the changes, and I'm going to merge this as it seems all good.

The only practical difference (beside OAuth flow now passing through the UNIX socket, if any) is that the OAuth flow API calls now have the right User-Agent header.

@babakks babakks merged commit d253589 into trunk Oct 15, 2025
11 checks passed
@babakks babakks deleted the babakks/fix-login-through-unix-socket branch October 15, 2025 13:51
man751186-ui

This comment was marked as spam.

tmeijn pushed a commit to tmeijn/dotfiles that referenced this pull request Oct 17, 2025
This MR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [cli/cli](https://github.com/cli/cli) | minor | `v2.81.0` -> `v2.82.0` |

MR created with the help of [el-capitano/tools/renovate-bot](https://gitlab.com/el-capitano/tools/renovate-bot).

**Proposed changes to behavior should be submitted there as MRs.**

---

### Release Notes

<details>
<summary>cli/cli (cli/cli)</summary>

### [`v2.82.0`](https://github.com/cli/cli/releases/tag/v2.82.0): GitHub CLI 2.82.0

[Compare Source](cli/cli@v2.81.0...v2.82.0)

##### ✨ Features

- `gh pr edit`: Only fetch org teams for reviewers when required by [@&#8203;BagToad](https://github.com/BagToad) in [#&#8203;11835](cli/cli#11835)

##### 🐛 Fixes

- fix(cache delete): report correct deleted count for key and key+ref deletions by [@&#8203;luxass](https://github.com/luxass) in [#&#8203;11838](cli/cli#11838)
- `gh agent-task create`: Fix `--follow` not killing the progress indicator by [@&#8203;BagToad](https://github.com/BagToad) in [#&#8203;11879](cli/cli#11879)
- `gh agent-task create`: Fix targetting upstream instead of default repo by [@&#8203;BagToad](https://github.com/BagToad) in [#&#8203;11896](cli/cli#11896)
- Fix `auth login` and `auth refresh` to use UNIX socket by [@&#8203;babakks](https://github.com/babakks) in [#&#8203;11922](cli/cli#11922)

**Full Changelog**: <cli/cli@v2.81.0...v2.82.0>

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever MR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this MR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this MR, check this box

---

This MR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS4xNDguNiIsInVwZGF0ZWRJblZlciI6IjQxLjE0OC42IiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJSZW5vdmF0ZSBCb3QiXX0=-->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

gh auth ignores the http_unix_socket config value

4 participants