Skip to content

Commit 0cb0ca7

Browse files
committed
fix: follow redirects when downloading skills in GcpSkillRegistry
The Agent Registry media download endpoint (alt=media) responds with a 302 redirect to a short-lived GCS signed URL instead of streaming the archive directly. httpx does not follow redirects by default and its raise_for_status() raises on 3xx responses, so _make_request treated the 302 as a failure and get_skill() could never download the skill archive. Following redirects is safe here: httpx drops the Authorization header on cross-origin redirects, so the OAuth token is not forwarded to the signed-URL host (GCS would reject a signed URL carrying extra credentials anyway).
1 parent e4ba704 commit 0cb0ca7

2 files changed

Lines changed: 29 additions & 3 deletions

File tree

src/google/adk/integrations/skill_registry/gcp_skill_registry.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,9 +154,13 @@ async def _make_request(
154154

155155
def _create_httpx_client(self) -> httpx.AsyncClient:
156156
"""Creates a new httpx.AsyncClient with appropriate SSL/mTLS configuration."""
157+
# The Agent Registry media download (alt=media) replies with a 302 to a
158+
# short-lived GCS signed URL, so the client must follow redirects; httpx
159+
# drops the Authorization header on cross-origin redirects, so the OAuth
160+
# token is not forwarded to the signed-URL host.
157161
if self._ssl_context is not None:
158-
return httpx.AsyncClient(verify=self._ssl_context)
159-
return httpx.AsyncClient()
162+
return httpx.AsyncClient(verify=self._ssl_context, follow_redirects=True)
163+
return httpx.AsyncClient(follow_redirects=True)
160164

161165
async def get_skill(self, *, name: str) -> models.Skill:
162166
"""Fetches a skill from the registry.

tests/unittests/integrations/skill_registry/test_gcp_skill_registry.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import io
1818
import os
19+
import ssl
1920
from unittest import mock
2021
import zipfile
2122

@@ -455,7 +456,9 @@ async def mock_get(url, *unused_args, **kwargs):
455456
skill = await registry.get_skill(name="my-skill")
456457

457458
# Verify AsyncClient was instantiated with verify=mock_ssl_context
458-
mock_client_class.assert_called_with(verify=mock_ssl_context)
459+
mock_client_class.assert_called_with(
460+
verify=mock_ssl_context, follow_redirects=True
461+
)
459462

460463
assert skill.frontmatter.name == "my-skill"
461464

@@ -491,3 +494,22 @@ async def test_use_custom_credentials():
491494
}),
492495
params={"search_string": "query"},
493496
)
497+
498+
499+
@pytest.mark.asyncio
500+
async def test_create_httpx_client_follows_redirects():
501+
"""Clients follow the 302 redirect issued by the media download endpoint."""
502+
registry = gcp_skill_registry.GCPSkillRegistry()
503+
504+
client = registry._create_httpx_client()
505+
try:
506+
assert client.follow_redirects is True
507+
finally:
508+
await client.aclose()
509+
510+
registry._ssl_context = ssl.create_default_context()
511+
client = registry._create_httpx_client()
512+
try:
513+
assert client.follow_redirects is True
514+
finally:
515+
await client.aclose()

0 commit comments

Comments
 (0)