Skip to content

Artifact API cannot load artifacts with path separators in names #6050

Closed

Description

@spectaclehong

🔴 Required Information

Please ensure all items in this section are completed to allow for efficient
triaging. Requests without complete information may be rejected / deprioritized.
If an item is not applicable to you - please mark it as N/A

Describe the Bug:

ADK artifact services already allow logical artifact names that contain /, such as reports/summary.txt. The API server also allows these names to be saved because POST /artifacts accepts the filename in the request body.

However, the path-based artifact endpoints use {artifact_name} as a single path segment. As a result, an artifact with / in its name can be saved and listed, but it cannot be loaded, deleted, or queried for versions through the REST API.

Steps to Reproduce:

  1. Start the ADK API server with any artifact service that accepts nested artifact names.
  2. Save an artifact using the REST API with a filename containing /:
POST /apps/{app_name}/users/{user_id}/sessions/{session_id}/artifacts
Content-Type: application/json
{
  "filename": "reports/summary.txt",
  "artifact": {
    "text": "hello"
  }
}
  1. List artifacts for the same session:
GET /apps/{app_name}/users/{user_id}/sessions/{session_id}/artifacts
  1. Try to load the artifact by path, either with a raw slash or an encoded slash:
GET /apps/{app_name}/users/{user_id}/sessions/{session_id}/artifacts/reports/summary.txt
GET /apps/{app_name}/users/{user_id}/sessions/{session_id}/artifacts/reports%2Fsummary.txt
  1. Try version-related endpoints for the same artifact:
GET /apps/{app_name}/users/{user_id}/sessions/{session_id}/artifacts/reports/summary.txt/versions
GET /apps/{app_name}/users/{user_id}/sessions/{session_id}/artifacts/reports/summary.txt/versions/0
GET /apps/{app_name}/users/{user_id}/sessions/{session_id}/artifacts/reports/summary.txt/versions/0/metadata

Expected Behavior:

Artifacts that can be saved and listed by the REST API should be loadable, deletable, and queryable for versions through the REST API using the same artifact name.

Observed Behavior:

The artifact is saved successfully and appears in list_artifact_keys, for example:

["reports/summary.txt"]

But the path-based load/version/metadata endpoints return 404 Not Found because {artifact_name} only matches a single path segment. Encoding the slash as %2F does not avoid the issue because the route still does not match after URL decoding.

Environment Details:

  • ADK Library Version (pip show google-adk): source checkout from main
  • Desktop OS: macOS
  • Python Version (python -V): Python 3.14.3

Model Information:

  • Are you using LiteLLM: N/A
  • Which model is being used: N/A

🟡 Optional Information

Regression:

N/A. The storage layer currently supports nested logical artifact names, and the bug is that the REST API cannot round-trip names it already accepts through the save endpoint.

Logs:

POST /artifacts with filename "reports/summary.txt" -> 200
GET /artifacts -> 200 ["reports/summary.txt"]
GET /artifacts/reports/summary.txt -> 404
GET /artifacts/reports%2Fsummary.txt -> 404

Screenshots / Video:

N/A

Additional Context:

This is not a request to treat artifact names as arbitrary filesystem paths. The storage layer should continue to reject unsafe names such as absolute paths or traversal outside the storage scope. The issue is about logical artifact names that already exist in ADK's artifact service contract, such as images/photo.png or reports/summary.txt.

A minimal backward-compatible fix would be to update the read/version/delete artifact routes to use FastAPI's path converter, for example {artifact_name:path}, so common nested artifact names can round-trip through the API.

Known limitation: this minimal fix does not fully remove all possible route ambiguity. For example, an artifact literally named foo/versions/0 is ambiguous with the existing versioned endpoint shape. A longer-term API shape could avoid putting artifact names in the path, for example by accepting the artifact name as a query or body parameter. For now, the minimal fix would align the REST API with artifact names that can already be saved and listed.

Minimal Reproduction Code:

from fastapi.testclient import TestClient
from google.adk.artifacts.in_memory_artifact_service import InMemoryArtifactService
from google.genai import types

# Conceptual reproduction:
# 1. Configure the ADK API server with InMemoryArtifactService.
# 2. Save filename="reports/summary.txt" through POST /artifacts.
# 3. GET /artifacts lists "reports/summary.txt".
# 4. GET /artifacts/reports%2Fsummary.txt returns 404.

How often has this issue occurred?:

  • Always (100%)

Activity

  1. added
    web[Component] This issue will be transferred to adk-web
    on Jun 10, 2026
  2. added theissue type on Jun 10, 2026
  3. surajksharma07 commented on Jun 10, 2026

    @surajksharma07
    Collaborator

    /adk-issue-analyze

  4. surajksharma07 commented on Jun 10, 2026

    @surajksharma07
    Collaborator

    @spectaclehong — both root causes are spot on, and the {artifact_name:path} route update paired with the URI regex fix in #6051 looks like the right approach to make nested artifact names fully round-trip through the REST API.

    Same situation as #6047 — the CLA check needs to be cleared before we can start the review, so please get that sorted when you get a chance.

    Once that's done, a quick end-to-end pass covering load, delete, list-versions, and version metadata all with nested names would help confirm everything's solid before we approve — then we'll move straight to review!

  5. spectaclehong commented on Jun 11, 2026

    @spectaclehong
    ContributorAuthor

    Thanks @surajksharma07. The CLA check is now passing.

    I also ran a local curl-based end-to-end check against a local ADK FastAPI server on 127.0.0.1:8765, backed by InMemoryArtifactService.

    Command:

    BASE='http://127.0.0.1:8765/apps/test_app/users/test_user/sessions/test_session'
    ARTIFACTS="$BASE/artifacts"
    
    curl -sS -w '\nHTTP %{http_code}\n' \
      -X POST "$BASE" \
      -H 'Content-Type: application/json' \
      -d '{"state":{}}'
    
    curl -sS -w '\nHTTP %{http_code}\n' \
      -X POST "$ARTIFACTS" \
      -H 'Content-Type: application/json' \
      -d '{"filename":"reports/summary.txt","artifact":{"text":"v0"},"customMetadata":{"rev":"zero"}}'
    
    curl -sS -w '\nHTTP %{http_code}\n' \
      -X POST "$ARTIFACTS" \
      -H 'Content-Type: application/json' \
      -d '{"filename":"reports/summary.txt","artifact":{"text":"v1"},"customMetadata":{"rev":"one"}}'
    
    curl -sS -w '\nHTTP %{http_code}\n' "$ARTIFACTS"
    curl -sS -w '\nHTTP %{http_code}\n' "$ARTIFACTS/reports/summary.txt"
    curl -sS -w '\nHTTP %{http_code}\n' "$ARTIFACTS/reports%2Fsummary.txt"
    curl -sS -w '\nHTTP %{http_code}\n' "$ARTIFACTS/reports%2Fsummary.txt?version=0"
    curl -sS -w '\nHTTP %{http_code}\n' "$ARTIFACTS/reports/summary.txt/versions"
    curl -sS -w '\nHTTP %{http_code}\n' "$ARTIFACTS/reports%2Fsummary.txt/versions/0"
    curl -sS -w '\nHTTP %{http_code}\n' "$ARTIFACTS/reports/summary.txt/versions/metadata"
    curl -sS -w '\nHTTP %{http_code}\n' "$ARTIFACTS/reports%2Fsummary.txt/versions/1/metadata"
    
    curl -sS -w '\nHTTP %{http_code}\n' \
      -X DELETE "$ARTIFACTS/reports%2Fsummary.txt"
    
    curl -sS -w '\nHTTP %{http_code}\n' "$ARTIFACTS/reports%2Fsummary.txt"
    
    # Result
    create_session: HTTP 200
    save_v0: HTTP 200
    save_v1: HTTP 200
    list_artifacts: HTTP 200 -> ["reports/summary.txt"]
    load_raw: HTTP 200 -> {"text":"v1"}
    load_encoded: HTTP 200 -> {"text":"v1"}
    load_query_version_0: HTTP 200 -> {"text":"v0"}
    list_versions_raw: HTTP 200 -> [0,1]
    load_version_0_encoded: HTTP 200 -> {"text":"v0"}
    list_versions_metadata_raw: HTTP 200
    get_version_1_metadata_encoded: HTTP 200
    delete_encoded: HTTP 200
    load_after_delete: HTTP 404

    This confirms nested artifact names round-trip through load, delete, list versions, and version metadata endpoints.

  6. surajksharma07 commented on Jun 11, 2026

    @surajksharma07
    Collaborator

    @spectaclehong CLA is green and that curl pass covers exactly the matrix we needed: raw + encoded slashes, query-version load, versions list/metadata, and delete with the post-delete 404.

    Went through #6051 as well — the version-specific routes registered ahead of the catch-all {artifact_name:path} is the right ordering, and the greedy (.+) with fullmatch in the URI regexes resolves names against the last /versions/N segment cleanly.

    The only residual ambiguity is the foo/versions/0 shape you already documented, which is fine to leave as a known edge case.

    Nothing blocking from my side — moving the PR into review now.

    Heads-up that merge goes through our internal Copybara flow, so once approved it'll be closed by the bot rather than the merge button!

    Team is looking more into it.
    Thanks!

  7. added
    needs review[Status] The PR/issue is awaiting review from the maintainer
    and removed
    request clarification[Status] The maintainer need clarification or more information from the author
    on Jun 11, 2026
  8. added a commit that references this issue on Jun 12, 2026
    b99546b
  9. added a commit that references this issue on Aug 11, 2026
    60277b4
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

needs review[Status] The PR/issue is awaiting review from the maintainerweb[Component] This issue will be transferred to adk-web

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions