From 2bc0c865f877380968bc7990beba758c8d9ac114 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 25 Oct 2026 10:57:25 +0000 Subject: [PATCH 1/7] Initial plan From 25025da121922ccdb09fe47b1fa64c5e54a65de1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 25 Oct 2026 10:58:59 +0000 Subject: [PATCH 2/7] Apply remaining changes Co-authored-by: yanyongyu <42488585+yanyongyu@users.noreply.github.com> --- githubkit/auth/app.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/githubkit/auth/app.py b/githubkit/auth/app.py index acd70e7719..2a9a84fd1f 100644 --- a/githubkit/auth/app.py +++ b/githubkit/auth/app.py @@ -104,7 +104,9 @@ async def aget_jwt(self) -> str: await cache.aset(cache_key, token, timedelta(minutes=8)) return token - def _build_installation_auth_request(self) -> httpx.Request: + def _build_installation_auth_request( + self, request: httpx.Request + ) -> httpx.Request: from githubkit_schemas.latest.models import ( AppInstallationsInstallationIdAccessTokensPostBody, ) @@ -138,6 +140,7 @@ def _build_installation_auth_request(self) -> httpx.Request: "User-Agent": self.github.config.user_agent, "Accept": self.github.config.accept, }, + extensions=dict(request.extensions), ) def _parse_installation_auth_response( @@ -207,7 +210,7 @@ def sync_auth_flow( cache = self.github.config.cache_strategy.get_cache_storage() key = self._get_installation_cache_key() if not (token := cache.get(key)): - token_request = self._build_installation_auth_request() + token_request = self._build_installation_auth_request(request) token_request.headers["Authorization"] = f"Bearer {self.get_jwt()}" response = yield token_request response.read() @@ -249,7 +252,7 @@ async def async_auth_flow( cache = await self.github.config.cache_strategy.get_async_cache_storage() key = self._get_installation_cache_key() if not (token := await cache.aget(key)): - token_request = self._build_installation_auth_request() + token_request = self._build_installation_auth_request(request) token_request.headers["Authorization"] = f"Bearer {await self.aget_jwt()}" response = yield token_request await response.aread() From 996040e2efbe3a2756173322f9c105fe49e2492b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 25 Oct 2026 11:04:57 +0000 Subject: [PATCH 3/7] Apply remaining changes Co-authored-by: yanyongyu <42488585+yanyongyu@users.noreply.github.com> --- githubkit/auth/app.py | 7 ++++--- githubkit/auth/base.py | 23 +++++++++++++++++++++- githubkit/auth/oauth.py | 42 ++++++++++++++++++++++++++++++----------- 3 files changed, 57 insertions(+), 15 deletions(-) diff --git a/githubkit/auth/app.py b/githubkit/auth/app.py index 2a9a84fd1f..8017ee1429 100644 --- a/githubkit/auth/app.py +++ b/githubkit/auth/app.py @@ -11,7 +11,7 @@ from githubkit.utils import UNSET, Unset, exclude_unset from ._url import require_app_auth, require_basic_auth, require_bypass -from .base import BaseAuthStrategy +from .base import BaseAuthStrategy, build_auth_request from .oauth import OAuthAppAuthStrategy try: @@ -132,15 +132,16 @@ def _build_installation_auth_request( }, ) ) - return httpx.Request( + return build_auth_request( + self.github, "POST", url, + request=request, json=exclude_unset(body), headers={ "User-Agent": self.github.config.user_agent, "Accept": self.github.config.accept, }, - extensions=dict(request.extensions), ) def _parse_installation_auth_response( diff --git a/githubkit/auth/base.py b/githubkit/auth/base.py index 953fd6bd5d..3f59073d41 100644 --- a/githubkit/auth/base.py +++ b/githubkit/auth/base.py @@ -1,5 +1,5 @@ import abc -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any import httpx @@ -11,3 +11,24 @@ class BaseAuthStrategy(abc.ABC): @abc.abstractmethod def get_auth_flow(self, github: "GitHubCore") -> httpx.Auth: raise NotImplementedError + + +def build_auth_request( + github: "GitHubCore", + method: str, + url: httpx.URL, + *, + request: httpx.Request | None = None, + **kwargs: Any, +) -> httpx.Request: + """Build a request used inside auth flow, keeping the client options. + + httpx only sets extensions (e.g. timeout) on the request passed to `send`, + so copy them from the original request, or fall back to client config. + """ + extensions = ( + dict(request.extensions) + if request is not None + else {"timeout": github.config.timeout.as_dict()} + ) + return httpx.Request(method, url, extensions=extensions, **kwargs) diff --git a/githubkit/auth/oauth.py b/githubkit/auth/oauth.py index 3c662eacd1..d47419031e 100644 --- a/githubkit/auth/oauth.py +++ b/githubkit/auth/oauth.py @@ -11,7 +11,7 @@ from githubkit.utils import is_async from ._url import get_oauth_base_url, require_basic_auth, require_bypass -from .base import BaseAuthStrategy +from .base import BaseAuthStrategy, build_auth_request try: import anyio @@ -29,7 +29,8 @@ def create_device_code( - github: "GitHubCore", client_id: str, scopes: Sequence[str] | None = None + github: "GitHubCore", client_id: str, scopes: Sequence[str] | None = None, + request: httpx.Request | None = None, ) -> Generator[httpx.Request, httpx.Response, dict[str, Any]]: """Create a device code for OAuth.""" base_url = get_oauth_base_url(github.config.base_url) @@ -37,9 +38,11 @@ def create_device_code( body = {"client_id": client_id} if scopes: body["scope"] = " ".join(scopes) - response = yield httpx.Request( + response = yield build_auth_request( + github, "POST", url, + request=request, json=body, headers={ "User-Agent": github.config.user_agent, @@ -56,6 +59,7 @@ def exchange_web_flow_code( client_secret: str, code: str, redirect_uri: str | None = None, + request: httpx.Request | None = None, ) -> Generator[httpx.Request, httpx.Response, dict[str, Any]]: """Exchange web flow code for token.""" base_url = get_oauth_base_url(github.config.base_url) @@ -67,9 +71,11 @@ def exchange_web_flow_code( } if redirect_uri: body["redirect_uri"] = redirect_uri - response = yield httpx.Request( + response = yield build_auth_request( + github, "POST", url, + request=request, json=body, headers={ "User-Agent": github.config.user_agent, @@ -81,14 +87,18 @@ def exchange_web_flow_code( def exchange_device_code( - github: "GitHubCore", client_id: str, device_code: str + github: "GitHubCore", client_id: str, + device_code: str, + request: httpx.Request | None = None, ) -> Generator[httpx.Request, httpx.Response, dict[str, Any]]: """Exchange device code for token.""" base_url = get_oauth_base_url(github.config.base_url) url = base_url.copy_with(raw_path=base_url.raw_path + b"login/oauth/access_token") - response = yield httpx.Request( + response = yield build_auth_request( + github, "POST", url, + request=request, json={ "client_id": client_id, "grant_type": "urn:ietf:params:oauth:grant-type:device_code", @@ -108,13 +118,16 @@ def refresh_token( client_id: str, client_secret: str | None, # client secret is optional in device flow refresh_token: str, + request: httpx.Request | None = None, ) -> Generator[httpx.Request, httpx.Response, dict[str, Any]]: """Refresh token.""" base_url = get_oauth_base_url(github.config.base_url) url = base_url.copy_with(raw_path=base_url.raw_path + b"login/oauth/access_token") - response = yield httpx.Request( + response = yield build_auth_request( + github, "POST", url, + request=request, json={ "client_id": client_id, "client_secret": client_secret, @@ -287,6 +300,7 @@ def auth_flow( self.client_id, self.client_secret, cast(str, self.refresh_token), + request, ) result = _parse_token_exchange_response(data) self.token = token = result["token"] @@ -343,6 +357,7 @@ def sync_auth_flow( self.client_secret, self.code, self.redirect_uri, + request, ) exchange_request = next(flow) while True: @@ -374,6 +389,7 @@ async def async_auth_flow( self.client_secret, self.code, self.redirect_uri, + request, ) exchange_request = next(flow) while True: @@ -442,7 +458,9 @@ def sync_auth_flow( # exchange token for the first time if (auth_strategy := self._token_auth_strategy) is None: # create device code - flow = create_device_code(self.github, self.client_id, self.scopes) + flow = create_device_code( + self.github, self.client_id, self.scopes, request + ) create_request = next(flow) while True: response = yield create_request @@ -462,7 +480,7 @@ def sync_auth_flow( if datetime.now(timezone.utc) > result["expire_time"]: raise AuthExpiredError("Device code expired.") flow = exchange_device_code( - self.github, self.client_id, result["device_code"] + self.github, self.client_id, result["device_code"], request ) auth_request = next(flow) while True: @@ -503,7 +521,9 @@ async def async_auth_flow( # exchange token for the first time if (auth_strategy := self._token_auth_strategy) is None: # create device code - flow = create_device_code(self.github, self.client_id, self.scopes) + flow = create_device_code( + self.github, self.client_id, self.scopes, request + ) create_request = next(flow) while True: response = yield create_request @@ -523,7 +543,7 @@ async def async_auth_flow( if datetime.now(timezone.utc) > result["expire_time"]: raise AuthExpiredError("Device code expired.") flow = exchange_device_code( - self.github, self.client_id, result["device_code"] + self.github, self.client_id, result["device_code"], request ) auth_request = next(flow) while True: From d08d44904273ef8930a2f7ce49f0f75ff687bb8e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 25 Oct 2026 11:08:44 +0000 Subject: [PATCH 4/7] Apply remaining changes Co-authored-by: yanyongyu <42488585+yanyongyu@users.noreply.github.com> --- githubkit/auth/app.py | 4 ++-- githubkit/auth/base.py | 2 +- githubkit/auth/oauth.py | 10 +++++----- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/githubkit/auth/app.py b/githubkit/auth/app.py index 8017ee1429..eaf0af5f88 100644 --- a/githubkit/auth/app.py +++ b/githubkit/auth/app.py @@ -11,7 +11,7 @@ from githubkit.utils import UNSET, Unset, exclude_unset from ._url import require_app_auth, require_basic_auth, require_bypass -from .base import BaseAuthStrategy, build_auth_request +from .base import BaseAuthStrategy, build_request_with_options from .oauth import OAuthAppAuthStrategy try: @@ -132,7 +132,7 @@ def _build_installation_auth_request( }, ) ) - return build_auth_request( + return build_request_with_options( self.github, "POST", url, diff --git a/githubkit/auth/base.py b/githubkit/auth/base.py index 3f59073d41..15903f6e17 100644 --- a/githubkit/auth/base.py +++ b/githubkit/auth/base.py @@ -13,7 +13,7 @@ def get_auth_flow(self, github: "GitHubCore") -> httpx.Auth: raise NotImplementedError -def build_auth_request( +def build_request_with_options( github: "GitHubCore", method: str, url: httpx.URL, diff --git a/githubkit/auth/oauth.py b/githubkit/auth/oauth.py index d47419031e..42ac37c484 100644 --- a/githubkit/auth/oauth.py +++ b/githubkit/auth/oauth.py @@ -11,7 +11,7 @@ from githubkit.utils import is_async from ._url import get_oauth_base_url, require_basic_auth, require_bypass -from .base import BaseAuthStrategy, build_auth_request +from .base import BaseAuthStrategy, build_request_with_options try: import anyio @@ -38,7 +38,7 @@ def create_device_code( body = {"client_id": client_id} if scopes: body["scope"] = " ".join(scopes) - response = yield build_auth_request( + response = yield build_request_with_options( github, "POST", url, @@ -71,7 +71,7 @@ def exchange_web_flow_code( } if redirect_uri: body["redirect_uri"] = redirect_uri - response = yield build_auth_request( + response = yield build_request_with_options( github, "POST", url, @@ -94,7 +94,7 @@ def exchange_device_code( """Exchange device code for token.""" base_url = get_oauth_base_url(github.config.base_url) url = base_url.copy_with(raw_path=base_url.raw_path + b"login/oauth/access_token") - response = yield build_auth_request( + response = yield build_request_with_options( github, "POST", url, @@ -123,7 +123,7 @@ def refresh_token( """Refresh token.""" base_url = get_oauth_base_url(github.config.base_url) url = base_url.copy_with(raw_path=base_url.raw_path + b"login/oauth/access_token") - response = yield build_auth_request( + response = yield build_request_with_options( github, "POST", url, From 2dd97619aad1f557d51786f702aafaabf19caf1b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 25 Oct 2026 11:12:34 +0000 Subject: [PATCH 5/7] :rotating_light: auto fix by pre-commit hooks --- githubkit/auth/app.py | 4 +--- githubkit/auth/oauth.py | 15 +++++++-------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/githubkit/auth/app.py b/githubkit/auth/app.py index eaf0af5f88..286d6d678e 100644 --- a/githubkit/auth/app.py +++ b/githubkit/auth/app.py @@ -104,9 +104,7 @@ async def aget_jwt(self) -> str: await cache.aset(cache_key, token, timedelta(minutes=8)) return token - def _build_installation_auth_request( - self, request: httpx.Request - ) -> httpx.Request: + def _build_installation_auth_request(self, request: httpx.Request) -> httpx.Request: from githubkit_schemas.latest.models import ( AppInstallationsInstallationIdAccessTokensPostBody, ) diff --git a/githubkit/auth/oauth.py b/githubkit/auth/oauth.py index 42ac37c484..afe08775eb 100644 --- a/githubkit/auth/oauth.py +++ b/githubkit/auth/oauth.py @@ -29,7 +29,9 @@ def create_device_code( - github: "GitHubCore", client_id: str, scopes: Sequence[str] | None = None, + github: "GitHubCore", + client_id: str, + scopes: Sequence[str] | None = None, request: httpx.Request | None = None, ) -> Generator[httpx.Request, httpx.Response, dict[str, Any]]: """Create a device code for OAuth.""" @@ -87,7 +89,8 @@ def exchange_web_flow_code( def exchange_device_code( - github: "GitHubCore", client_id: str, + github: "GitHubCore", + client_id: str, device_code: str, request: httpx.Request | None = None, ) -> Generator[httpx.Request, httpx.Response, dict[str, Any]]: @@ -458,9 +461,7 @@ def sync_auth_flow( # exchange token for the first time if (auth_strategy := self._token_auth_strategy) is None: # create device code - flow = create_device_code( - self.github, self.client_id, self.scopes, request - ) + flow = create_device_code(self.github, self.client_id, self.scopes, request) create_request = next(flow) while True: response = yield create_request @@ -521,9 +522,7 @@ async def async_auth_flow( # exchange token for the first time if (auth_strategy := self._token_auth_strategy) is None: # create device code - flow = create_device_code( - self.github, self.client_id, self.scopes, request - ) + flow = create_device_code(self.github, self.client_id, self.scopes, request) create_request = next(flow) while True: response = yield create_request From d7b1ae8dcfdcbb4898321b61c2008b7c9f5c4ce5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 25 Oct 2026 11:18:50 +0000 Subject: [PATCH 6/7] Apply remaining changes Co-authored-by: yanyongyu <42488585+yanyongyu@users.noreply.github.com> --- githubkit/auth/base.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/githubkit/auth/base.py b/githubkit/auth/base.py index 15903f6e17..ac1333df66 100644 --- a/githubkit/auth/base.py +++ b/githubkit/auth/base.py @@ -25,9 +25,15 @@ def build_request_with_options( httpx only sets extensions (e.g. timeout) on the request passed to `send`, so copy them from the original request, or fall back to client config. + The `hishel_*` extensions are per-request cache controls and are not copied, + so they never leak onto the auth requests (e.g. forcing a token to be cached). """ extensions = ( - dict(request.extensions) + { + key: value + for key, value in request.extensions.items() + if not key.startswith("hishel_") + } if request is not None else {"timeout": github.config.timeout.as_dict()} ) From ba9d8baf65c49e8a2a64a2d1058e4f1bf658be7b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 25 Oct 2026 11:21:57 +0000 Subject: [PATCH 7/7] Apply remaining changes Co-authored-by: yanyongyu <42488585+yanyongyu@users.noreply.github.com> --- githubkit/auth/base.py | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/githubkit/auth/base.py b/githubkit/auth/base.py index ac1333df66..3f0e724f18 100644 --- a/githubkit/auth/base.py +++ b/githubkit/auth/base.py @@ -21,20 +21,13 @@ def build_request_with_options( request: httpx.Request | None = None, **kwargs: Any, ) -> httpx.Request: - """Build a request used inside auth flow, keeping the client options. + """Build a request used inside auth flow, keeping the client timeout. - httpx only sets extensions (e.g. timeout) on the request passed to `send`, - so copy them from the original request, or fall back to client config. - The `hishel_*` extensions are per-request cache controls and are not copied, - so they never leak onto the auth requests (e.g. forcing a token to be cached). + httpx only sets the timeout extension on the request passed to `send`, + not on requests yielded by an auth flow, so copy it from the original + request, or fall back to the client config. """ - extensions = ( - { - key: value - for key, value in request.extensions.items() - if not key.startswith("hishel_") - } - if request is not None - else {"timeout": github.config.timeout.as_dict()} - ) - return httpx.Request(method, url, extensions=extensions, **kwargs) + timeout = ( + request.extensions.get("timeout") if request is not None else None + ) or github.config.timeout.as_dict() + return httpx.Request(method, url, extensions={"timeout": timeout}, **kwargs)