Skip to content

RestServer.normalizeConfig still casts crud / metadata / batch / routes instead of parsing them — batch.maxBatchSize bounds and three declared enums never execute #11984

Description

@os-zhuang

Found while implementing #11637 (making the same method parse its api sub-object). Filed, not fixed — #11637 deliberately puts one narrowing in front of contract review rather than five, since each sub-object refuses a different set of previously-accepted configs.

What was measured

On origin/main @ 7899f5745. RestServer.normalizeConfig opens with five casts, one per sub-object:

constapi=(config.api??{})asPartial<RestApiConfig>;constcrud=(config.crud??{})asPartial<CrudEndpointsConfig>;constmetadata=(config.metadata??{})asPartial<MetadataEndpointsConfig>;constbatch=(config.batch??{})asPartial<BatchEndpointsConfig>;constroutes=(config.routes??{})asPartial<RouteGenerationConfig>;

#11637 replaces the first line's unvalidated status by running RestApiConfigSchema before it. The other four are untouched, and each has declared constraints that consequently never execute (packages/spec/src/api/rest-server.zod.ts):

declared constraintwhat the cast admits today
batch.maxBatchSize: z.number().int().min(1).max(1000).default(200)0, -5, 10_000, 2.5 — the value is used as the batch cap, so an out-of-range one is a live policy, not a typo
routes.nameTransform: z.enum(['none','plural','kebab-case','camelCase'])any string; an unrecognised one silently falls through the transform switch
crud.objectParamStyle: z.enum(['path','query'])any string
metadata.cacheTtl: z.number().int().default(3600)a float, or a negative TTL

Same seam, same mechanism, same reason nothing runs them: both hops into @objectstack/rest are casts and the plugin declares no configSchema (see #11637's chain, and #11982 for why the kernel validator could not have covered it either).

api.version was the urgent member of the family because it is spliced into getApiBasePath() and therefore into the mount of every route — api.version: '' mounted the whole API at /api//. These four are ordinary knobs by comparison, which is why they were split out rather than folded in.

Not prejudged

The obvious shape is to extend #11637's assertDeclaredApiConfig to the four siblings — but it is not a mechanical repeat, and each needs its own measurement first:

Each of these is contract accept/reject behaviour change, so whatever lands wants a changeset naming the newly-refused shapes and the contract-review tier, exactly as #11637 did.


Generated by Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions