fix(llma): redirect skill lookups by UUID to name-based URL#58697
fix(llma): redirect skill lookups by UUID to name-based URL#58697pauldambra wants to merge 4 commits into
Conversation
When `GET /api/.../llma/skills/name/<id>` is called with a UUID instead of a slug, look up the skill by primary key and return a 301 to the name-based URL. Query params (e.g. `?version=`) are preserved. This allows links to skills copied from the API response `id` field (e.g. from `llma-skill-get` MCP output) to still resolve correctly. The canonical URL remains `name/<slug>`. Generated-By: PostHog Code Task-Id: 74232348-8744-42fa-9f79-15467540921a
Switch from `HttpResponsePermanentRedirect` (which required widening the return type annotation) to a plain DRF `Response` with `status=HTTP_301_MOVED_PERMANENTLY` and a `Location` header. Keeps the return type as `-> Response` and avoids mixing Django and DRF response types in the same view. Generated-By: PostHog Code Task-Id: 74232348-8744-42fa-9f79-15467540921a
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f1c878fef7
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
The author is not on the owning team (@PostHog/team-llm-analytics) and this is a behavioral change to an API endpoint — adding a 301 redirect path to the get_by_name action. Per ownership rules, behavioral API contract changes from non-team members require a human review from the owning team.
Prompt To Fix All With AIFix the following 3 code review issues. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 3
products/llm_analytics/backend/api/skills.py:317
**Unsafe path reconstruction via `str.replace`**
`str.replace(skill_name, skill.name)` replaces ALL occurrences of the UUID string in the path without boundary checking. If the path ever contains the UUID more than once, every occurrence is replaced and the resulting location is wrong. Additionally, `skill.name` is substituted raw into the path before `build_absolute_uri`/`iri_to_uri` runs. `iri_to_uri` leaves `?`, `#`, `&`, and `=` unencoded (they're in its safe set), so a skill name containing any of those characters would corrupt the URL's query-string or fragment boundary. Use `urllib.parse.quote(skill.name, safe="")` when inserting the name into the path, and anchor the replacement to the specific URL segment, e.g. `request.path.replace(f"/name/{skill_name}", f"/name/{urllib.parse.quote(skill.name, safe='')}", 1)`.
### Issue 2 of 3
products/llm_analytics/backend/api/skills.py:313-323
**Redirect drops version context for non-latest skill versions**
`LLMSkill.id` is the primary key of a specific version row — the API exposes it per-version, not per-skill. The redirect uses `skill.name` without appending a `?version=` parameter, so the redirected URL resolves to the _latest_ version via `get_skill_by_name_from_db`. If the UUID the caller provided belongs to a non-latest version, the redirect silently serves a different version than requested. Consider appending `?version_id={skill.id}` (or `?version={skill.version}`) to preserve the exact version the UUID identified, rather than silently upgrading to latest.
### Issue 3 of 3
products/llm_analytics/backend/api/skills.py:321
**301 (permanent) redirect will be cached indefinitely by clients**
HTTP 301 is aggressively cached by browsers, HTTP libraries, and reverse proxies. If a skill is ever renamed, any client that cached the UUID → old-name 301 will continue sending requests to the stale name URL, producing 404s until the cache entry expires or is cleared. A `302 Found` (or `307 Temporary Redirect` to preserve the HTTP method) is safer here.
```suggestion
response = Response(status=status.HTTP_302_FOUND)
```
Reviews (1): Last reviewed commit: "refactor(llma): use DRF Response for the..." | Re-trigger Greptile |
- Check name lookup first, UUID fallback only on not-found; eliminates ambiguity when a skill is deliberately named with a UUID-shaped slug. - Use HTTP_302_FOUND instead of HTTP_301_MOVED_PERMANENTLY; 301 caches permanently in browsers and would break if a skill is renamed/deleted. - Use `request.get_full_path().replace(..., 1)` which carries the query string and limits substitution to the first occurrence. - Add parameterized tests covering: UUID redirect, UUID with query string, unknown UUID -> 404, UUID-shaped name -> 200 (no redirect). Generated-By: PostHog Code Task-Id: 74232348-8744-42fa-9f79-15467540921a
|
Note 🤖 Automated comment by QA Swarm — not written by a human Multi-perspective review: qa-team (specialists + generalists), paul-reviewer, xp-reviewer Updated: All findings addressed in Verdict: ✅ APPROVEAll HIGH findings were fixed before merge. The PR is clean. Findings & resolution
Reviewer summaries
Automated by QA Swarm — not a human review |
|
…open redirect request.build_absolute_uri() embeds the request Host header in the Location response value. A crafted request with a spoofed Host could aim the redirect at an attacker-controlled domain. Using the relative path from get_full_path() directly eliminates this vector entirely — no domain in the Location header means no host to spoof. Generated-By: PostHog Code Task-Id: 74232348-8744-42fa-9f79-15467540921a
|
Reviews (3): Last reviewed commit: "fix(llma): use relative path in UUID red..." | Re-trigger Greptile |
There was a problem hiding this comment.
The implementation is technically correct and all bot-raised inline concerns are resolved, but the author is not on the owning team (@PostHog/team-llm-analytics) and this is a behavioral change to an API endpoint (adding a 302 redirect path to get_by_name). That combination requires a human review from the owning team before auto-approval.

Problem
When sharing a link to a skill from the API response (which contains the skill's UUID
idfield), the URL like/api/.../llma/skills/name/019e35ad-8c0c-752d-9260-1e7a5c303248returns 404. The canonical lookup key is the skill's slug name, but callers naturally reach for the ID when constructing links.Changes
get_by_namenow detects when theskill_namepath segment is a UUID (via_is_uuid())Locationpointing to the name-based URL. 302 is used rather than 301 because permanent redirects are cached by browsers indefinitely; if a skill is renamed or deleted after a UUID link is followed, a cached 301 would silently misdirect clients forever.?version=) are preserved on the redirect viarequest.get_full_path().How did you test this code?
I'm an agent. Parameterized tests added cover the key paths. Manual verification was not performed.
Publish to changelog?
no
🤖 Agent context
Authored by PostHog Code (Claude Opus 4.7). The user noticed that skill links generated from the MCP's
llma-skill-getresponse (which returns the UUIDidfield) returned 404 when used as a URL.Initial implementation used
HTTP_301_MOVED_PERMANENTLY. QA swarm (paul-reviewer, xp-reviewer, qa-team) independently flagged this: 301 caches permanently in browsers — if a skill is renamed or deleted, cached redirects would silently misdirect clients. Switched toHTTP_302_FOUND.QA swarm also found: UUID-shaped skill names could be shadowed (fixed by running name lookup first),
str.replacereplacing all occurrences (fixed withcount=1viaget_full_path()), and missing tests (added 4 parameterized cases).Created with PostHog Code