Skip to content

fix(api): registry search/integrity reject explicit empty query params that runtime accepts #517

@galuis116

Description

@galuis116

Goal

Accept empty-string query params (?category=, ?platform=, ?artifact=, ?hash=) on the public Registry Search and Registry Integrity endpoints, treating them as "no filter / no specific value" — matching the runtime code already in place and the precedent set by the public Jobs schema.

Why this matters

The website API contracts declare these four query params as .optional().default(""), which signals "absent or empty is fine". The runtime handlers are built around that contract:

  • apps/web/src/lib/api/registry-search-filters.ts:62 short-circuits matchesPlatform when platform is empty.
  • apps/web/src/lib/api/registry-search-filters.ts:103 short-circuits the category predicate when filters.category is empty.
  • apps/web/src/app/api/registry/integrity/route.ts destructures { artifact = "", hash = "" } and emits "snapshot" status when artifact is empty, mapping empty back to null in the JSON body.

But the Zod schemas in apps/web/src/lib/api/contracts.ts apply .regex(...) BEFORE .optional().default(""). Because default(...) only fires when the field is undefined, an explicit empty string ?platform= reaches the regex and gets rejected with HTTP 400 invalid_payload. That is exactly what an HTML form posts when filter inputs are left blank, what Raycast emits between filter resets, and what existing API clients that round-trip platform: null → "" send today. The other public list schema in the same file — publicJobsQuerySchema at line 118 — explicitly accepts "" (z.enum(["all", "true", "false", ""])), proving the maintainer intent is empty == no-filter for public query params.

This is a small contract violation with a real user impact: every blank-field form submission against /api/registry/search returns a 400 today.

Current behavior

  • File: apps/web/src/lib/api/contracts.ts
    • platformSchema (lines 7–13): z.string().trim().toLowerCase().regex(/^[a-z0-9][a-z0-9 -]{0,48}$/).optional().default("")
    • categorySchema (line 14): safeSlugSchema.optional().default("") where safeSlugSchema = z.string().regex(/^[a-z0-9-]+$/)
    • registryIntegrityQuerySchema.artifact (line 276): z.string().trim().max(160).regex(/^\/?(?:[a-z0-9][a-z0-9._-]*\/)*(?:[a-z0-9][a-z0-9._-]*)$/).optional()
    • registryIntegrityQuerySchema.hash (line 277): z.string().trim().toLowerCase().regex(/^[a-f0-9]{64}$/).optional()
  • Routes:
    • GET /api/registry/search?q=foo&platform= → 400 invalid_payload, Zod issue path ["platform"]
    • GET /api/registry/search?q=foo&category= → 400 invalid_payload, Zod issue path ["category"]
    • GET /api/registry/integrity?artifact= → 400 invalid_payload, Zod issue path ["artifact"]
    • GET /api/registry/integrity?hash= → 400 invalid_payload, Zod issue path ["hash"]
  • Note that the runtime in both routes already handles empty correctly when the param is absent — the Zod gate is the only thing blocking explicit empty.

Desired behavior

Each of the four schemas should accept "" as a valid input (with the existing regex still gating non-empty values), matching the runtime semantics. The OpenAPI advertised default: "" becomes a real default both when the param is absent and when it is an explicit empty string. The four endpoints respond with 200:

  • Search treats empty category / platform as "no filter on this dimension" (already implemented in matchesPlatform / entryMatchesFilters).
  • Integrity treats empty artifact / hash as "snapshot listing" (already implemented in the route handler).
  • Non-empty inputs continue to be regex-validated exactly as today (no relaxation of the format checks).

Scope

  • apps/web/src/lib/api/contracts.ts — modify only the four schemas listed above. Keep safeSlugSchema untouched (it is also used as a path-param validator at lines 281, 282, 450, 522, 1156, 1165, where empty IS invalid).
  • tests/registry-search-api.test.ts — extend existing describe block with empty-param coverage.
  • tests/registry-integrity-api.test.ts — extend with empty artifact= / hash= coverage.

Out of scope

  • safeSlugSchema itself (path-param use must stay strict).
  • Any other Zod schema in contracts.ts.
  • The OpenAPI YAML at cloudflare/api-schema-heyclaude-openapi.yaml (generated; let the generator re-emit on a normal release cycle — schema shape is unchanged in semantics).
  • Generated artifacts under apps/web/src/generated/** and apps/web/public/data/**.
  • Synonym / typo-tolerance / ranking work (that is issue feat(search): add weighted ranked registry search #487 / feat(registry): add query suggestions and synonym aliases #502).
  • Any changes to publicJobsQuerySchema (it already does the right thing).

Acceptance criteria

  • PR includes Closes #<issue>.
  • GET /api/registry/search?q=fixture&platform= returns 200 and treats platform as no-filter.
  • GET /api/registry/search?q=fixture&category= returns 200 and treats category as no-filter.
  • GET /api/registry/integrity?artifact= returns 200 with status: "snapshot" and artifact: null in the body.
  • GET /api/registry/integrity?hash= returns 200 with the existing snapshot/match/mismatch logic, treating empty hash as "no hash provided".
  • Existing rejection of malformed non-empty values is unchanged (e.g. ?platform=%21bad still returns 400).
  • pnpm exec vitest run tests/registry-search-api.test.ts tests/registry-integrity-api.test.ts tests/api-contracts.test.ts passes.

Quality evidence required in the PR

  • No visual impact (API-only fix; no UI / page changes).
  • Before/after curl traces for each of the four endpoints showing 400 → 200 on empty inputs and 400 → 400 preserved on malformed non-empty inputs.
  • Test diff in PR description listing the added cases and the pattern used (z.union([z.literal(""), ...regex...]) or equivalent).
  • Backward-compatibility note: the public response shape and status codes for absent / valid params are unchanged; only the previously-400 explicit-empty path becomes a normal 200.

Validation

pnpm install
pnpm exec vitest run tests/registry-search-api.test.ts tests/registry-integrity-api.test.ts tests/api-contracts.test.ts
pnpm lint
git diff --check

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions