Classify packager errors as 400 in skill build handler#5076
Merged
Conversation
v0.0.17 introduces sentinel errors in oci/skills (ErrSkillMDMissing, ErrInvalidFrontmatter, ErrInvalidSkillDir, ErrInvalidSkillFile, ErrTooManyFiles, ErrSkillTooLarge) so callers can classify packager failures with errors.Is instead of matching error strings. Pulled in ahead of the build-handler refactor that consumes them.
POST /api/v1beta/skills/build previously surfaced every packager failure — including user-input mistakes like a missing SKILL.md or a malformed frontmatter — as 500 Internal Server Error with no body, generating Sentry noise on what is plainly bad input. Wrap the s.packager.Package error with an errors.Is switch over the six toolhive-core v0.0.17 sentinels and map each one to 400 with the packager's message preserved. Anything else stays a 500. Add five TestBuild cases that mock the packager returning a wrapped sentinel and assert both the HTTP code and errors.Is on the sentinel, covering the limits the toolhive-side validator misses (symlink in skill dir, oversized dir).
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #5076 +/- ##
==========================================
- Coverage 69.81% 69.77% -0.04%
==========================================
Files 562 564 +2
Lines 56647 56652 +5
==========================================
- Hits 39548 39531 -17
- Misses 14063 14092 +29
+ Partials 3036 3029 -7 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
JAORMX
approved these changes
Apr 28, 2026
samuv
added a commit
to stacklok/toolhive-studio
that referenced
this pull request
Apr 28, 2026
…ral toast (#2123) * test(mocks): register user-error scenario for 400 backend errors Add a generic 'user-error' entry to MockScenarios so fixtures can model the 400 Bad Request shape introduced for the skill-build endpoint by stacklok/toolhive#5076, and wire the first instance on POST /api/v1beta/skills/build returning '{ error: "SKILL.md missing" }'. * feat(skills): persist build dialog errors inline instead of an ephemeral toast Render a destructive Alert below the dialog header that surfaces the backend message verbatim — including the actionable 400 packager errors from stacklok/toolhive#5076 (SKILL.md missing, invalid SKILL.md frontmatter, symlink/size violations) — so the user can keep reading it while they fix the path or tag. The alert clears on cancel and on resubmit. Mirrors the submitError pattern already used by DialogInstallSkill. * refactor(skills): drop redundant error toast from build mutation hook The build dialog now owns the user-visible error surface with an inline alert that preserves the backend message, so the hook's generic 'Failed to build skill' toast was duplicating the signal and hiding the more useful packager output. trackEvent('Skills: build failed') is preserved — analytics are unchanged. Matches useMutationInstallSkill, which already lets its dialog render the error inline.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
500pattern fixed for the content/install paths in Classify OCI pull errors and fall back to git for skills content API #5014 and Return 502 Bad Gateway when OCI skill pull fails #4956 — now applied toPOST /api/v1beta/skills/build. A user pointingthv skill buildat a directory without aSKILL.md, or with malformed frontmatter, currently gets a bare500 Internal Server Errorand a*fmt.wrapError: packaging skill: reading skill directory: SKILL.md not found in skill directoryexception in Sentry. That's plainly bad input, not a server bug.toolhive-corev0.0.17 (release, stacklok/toolhive-core#95) introduces six sentinel errors inoci/skillsso the packager's user-input failures can be classified witherrors.Isinstead of matching error strings.s.packager.Packagewith anerrors.Isswitch over those sentinels, mapping each one to400 Bad Requestwith the packager's message preserved (e.g.SKILL.md missing,invalid SKILL.md frontmatter,skill directory too large). Anything else stays a real500.Why this and not a
ValidateSkillDirpre-flightAn earlier draft pre-flighted the existing
skills.ValidateSkillDirvalidator before invoking the packager. That worked for the most common case (missingSKILL.md) but had two problems:maxSkillFiles(1,000) andmaxSkillTotalSize(100 MB) limits, nor the per-filevalidateSkillFilechecks (symlinks, non-regular files). Those failure modes still fell through to500.The sentinel switch covers every packager failure mode in one place, with no double-validation. The packager's existing error messages are preserved verbatim, so the client still sees actionable detail in the
400body.Mapping
ociskills.*)ErrSkillMDMissingSKILL.mdnot present in the directory400ErrInvalidFrontmattername400ErrInvalidSkillDir400ErrInvalidSkillFile400ErrTooManyFilesmaxSkillFiles(1,000)400ErrSkillTooLargemaxSkillTotalSize(100 MB)400500The pre-existing
validateLocalPathchecks (null bytes, traversal, relative paths) stay in place — they're cheap, run before any I/O, and double as a guard against packager bugs.Type of change
Test plan
go test ./pkg/skills/skillsvc/... -count=1)task lint-fix— 0 issues)go test ./pkg/skills/... ./pkg/api/...)TestBuildcases assert bothhttperr.Code(err) == 400anderrors.Is(err, ociskills.ErrXxx)for each sentinel: missingSKILL.md, invalid frontmatter, empty name in frontmatter, symlink in skill dir, oversized dir.nil packager returns 500,empty path returns 400,relative path returns 400,path traversal returns 400,invalid tag returns 400,packager error propagates, three success cases,invalid fallback config name returns 400) all still pass — none of them flow through the new switch.Changes
go.mod,go.sumgit.832008.xyz/stacklok/toolhive-corev0.0.16→v0.0.17pkg/skills/skillsvc/build.goerrors.Isswitch over the six packager sentinels; each →httperr.WithCode(err, http.StatusBadRequest)pkg/skills/skillsvc/build_test.gowantErrIs errorfield + five new sentinel cases; runner assertsassert.ErrorIswhen setThe OpenAPI annotation on
buildSkill(pkg/api/v1/skills.go) already advertises@Failure 400 ..., anddocs/server/{docs.go,swagger.json,swagger.yaml}already reflect it — no spec regeneration needed.Does this introduce a user-facing change?
Yes — clients calling
POST /api/v1beta/skills/buildon a directory the packager rejects (missingSKILL.md, malformed frontmatter, symlinks, size/count limit) now receive400 Bad Requestwith the packager's message in the body, instead of500 Internal Server Errorwith an empty body. Successful builds and genuine server failures are unchanged.