fix: make the generated OpenAPI client work, inherit path-level parameters, repair the website - #437
Merged
Merged
Conversation
A path item's shared `parameters` were never merged into the operations
under it, so declaring a path parameter once - the idiomatic way to avoid
repeating it per method - produced an operation with no parameters at
all. The generated context interface came out empty, giving the caller no
way to supply the value, and the URL kept the raw template: requests went
out to `/pets/{petId}` with the literal placeholder in the path. It
compiled cleanly and nothing warned, despite docs/inputs/openapi.md
promising that unsupported constructs are "reported with a warning rather
than dropped silently".
Add `collectOperationParameters`, which merges path-item and operation
parameters with the specification's override rule (an operation wins over
an inherited parameter of the same `name` + `in`), and use it for
parameters, headers and the Swagger 2.0 body parameter.
While here, stop treating non-method path item keys as operations. All
three processors iterated `Object.entries(pathItem)`, so `parameters`,
`summary`, `servers` and `$ref` were each processed as if they were an
HTTP method - harmless until path-item parameters started being read, and
wrong regardless. `HTTP_METHODS` moves to the shared utils so the filter
module and the processors agree on one list.
Also warn when a non-JSON content type is dropped because a JSON one was
picked alongside it. The existing warnings only fire when JSON is absent
entirely, so `application/json` + `multipart/form-data` on one operation
silently generated a JSON-only client.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…rdinary documents Three independent failures, each triggered by a shape that is entirely ordinary in an OpenAPI document. A component schema named `Error` - the conventional name for one - broke compilation. Its generated model is imported into `http_client.ts`, which shadows the global `Error` for the whole module, and the same file declares `class HttpError extends Error` plus several `new Error(...)` calls. Six TypeScript errors, none of them in code the user wrote. Renaming the schema and changing nothing else made the identical document compile. The emitted file now captures the global once, as `HttpGlobalError`, and routes every reference through it. An array-typed success response threw on the success path. `extractStatusCodeValue` read the status decoration only from a reference to an object model, so an array member was filtered out and the emitted dispatch had no branch for its code - a plain `200` from a list endpoint raised "No matching type found for status code: 200", while the schema constant in the same file proved the generator knew about it. The decoration is now read from the member itself for arrays, primitives and enums, which have no `unmarshal` of their own and so are parsed structurally. Responses with no body were mishandled twice. An operation whose declared responses are all bodyless - the common `DELETE` -> `204` - was dropped entirely, leaving no function and a diagnostic telling the user to set `protocols`, which they already had. One that mixed `204` with an error code was typed with the *error* model as its success type and threw `SyntaxError: Unexpected end of JSON input`, because `.json()` on an empty body throws. Bodyless success codes are now collected per operation, the function is always generated, `data` is typed `undefined` (or widened to include it), and the body is read through `readOptionalJsonBody`. Also read the base URL from a Swagger 2.0 `host`/`basePath`/`schemes`. Only `servers` was consulted, an OpenAPI 3.x field, so every 2.0 document silently fell through to `http://localhost:3000`. No tier could see any of this: test/blackbox/test_files.ts built its corpus from `schemas/asyncapi` and `schemas/jsonschema` only, so the harness's `inputType === 'openapi'` arm was dead code and the OpenAPI client was compile-checked at breadth nowhere. Add an OpenAPI corpus carrying all of these shapes, two configs, and an `openapi` CI bucket. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…point The landing page shipped unedited Docusaurus scaffolding: the tab read "Hello from The Codegen Project" and every social preview and search snippet read "Description will go into a meta tag in <head />". Twenty-odd links were dead, including three of the four "Explore Further" links closing the quickstart that the homepage's only call to action points at. Rather than rewrite each one, give the `protocols` and `inputs` categories real routes via `slug` (relative to the docs `routeBasePath`), which fixes them at the source and makes `/docs/protocols` resolve for the blog too. `examples/` is not published to the site - move_docs.js copies only `docs/` and `assets/` - so those links now point at GitHub, which works from the site and the repo alike. Blog URLs are flat, so `../slug` and `./slug` both escaped `/blog`; they are absolute now, which also fixes them on the tag and author listing pages that re-render the excerpts. Three broken anchors: `#dependencies` targeted the page's only h1 among h2 siblings, and the ToCs of docs/usage.md and docs/migrations/v0.md linked their own page title, which gets no anchor. Moving those titles into front matter leaves markdown-toc nothing to mislink, so regeneration keeps them correct. `/api/mcp` - which docs/ai-assistants.md tells every visitor to add to Claude Code, Cursor or Windsurf - returned `DEPLOYMENT_NOT_FOUND`, because vercel.json rewrote it to a preview deployment that no longer exists. It now points at a live deployment (verified: the MCP initialize handshake returns 200). None of this could be caught. The website workflow was path-filtered to `website/**`, but the published docs live at the repo root and are copied in at build time, so a docs-only change never built the site - and broken links were configured as warnings, letting them accumulate. Broken links and markdown links now fail the build, and `docs/**`/`assets/**` trigger the workflow. Anchors stay a warning, since the two generated ToCs surface here before anyone has had a chance to regenerate them. Also refresh the playground's copied configuration schema, which predated the root-config `filter` feature, and label the feature-card SVGs from their card titles - the stock illustrations carry unrelated internal titles, so a screen reader announced "Powered by React" for "Powered by Open Source". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Deployment failed with the following error: Learn More: https://vercel.com/docs/concepts/projects/project-configuration |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Vercel validates vercel.json against a schema with
`additionalProperties: false`, so the `_comment` key I added alongside the
/api/mcp rewrite failed the deployment outright ("Deployment failed" on
the-codegen-project, while the-codegen-project-mcp built fine).
The note it carried - that the rewrite targets a deployment-specific URL
and so needs updating by hand after each MCP deploy - moves to
mcp-server/README.md, which can hold prose, along with the reason it
cannot live in vercel.json.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Brings in the v0.81.0 release (#436). One conflict, in docs/migrations/v0.md, and only over trailing blank lines - repeated `generate:readme:toc` runs append one each time, so both sides had accumulated a different number. Resolved to main's count so this branch adds none. The release also refreshed website/src/schemas/configuration-schema.json for the root-config `filter` feature, which is the same refresh this branch had made, so that change is now redundant and the file matches main exactly. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Contributor
Author
|
🎉 This PR is included in version 0.81.1 🎉 The release is available on: Your semantic-release bot 📦🚀 |
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.
Related Issue: none (sourced from
.claude/thoughts/shared/improvements/2026-07-30-1558-improvements.md, ideas 1–3)What
Three problems, each reproduced before and after, and in each case the guardrail that should have caught it was also missing:
Errorbroke compilation, an array-typed success response threw on the success path, and responses with no body either vanished or crashed.parameterswere silently dropped, so requests went out with a literal{petId}in the URL and the generated API offered no way to supply the value.Why
A single realistic document hit all three of (1) at once — that is how they were found. A list endpoint, a create, a delete, and a component schema called
Error:Erroris the conventional name for an error schema. Its generated model is imported intohttp_client.ts, which shadows the globalErrorfor the whole module — and the same file declaresclass HttpError extends Errorplus severalnew Error(...)calls. SixTS2345/TS2339errors, none of them in code the user wrote. Renaming the schema toApiErrorand changing nothing else made the identical document compile: the schema name was the only variable.extractStatusCodeValueread thex-modelina-status-codesdecoration only from aConstrainedReferenceModelwrapping an object, so an array member was filtered out and the emitted dispatch had no branch for its code. A plain200from a list endpoint raisedNo matching type found for status code: 200, whiletheCodeGenSchematwo lines below it in the same file carried$id: listBooks_Response_200— the generator knew about the response and only the dispatch dropped it. The trigger is narrower than it looks: an array-only200produces no union and works, so it needs a non-object success response plus at least one other documented status code — i.e. any list endpoint that documents a400.payloads.tsgates its response entry onresponseSchemas.length > 0and a response withoutcontentlegitimately yields no schema, sochannels/openapi.tshitif (!replyMessageType) return undefinedand dropped the operation. ADELETE→204API therefore generated no client file, and the diagnostic told the user to setprotocols— which they already had. Mixed with an error code it was worse: the function was typedHttpClientResponse<DeleteBookResponse_404>, the error model as the success type, and a real 204 threwSyntaxError: Unexpected end of JSON input, because.json()on an empty body throws. There was no no-content handling anywhere to fall back on.(2) is the same class of silence in the input layer.
parameters.tsandheaders.tsboth carried the comment// Collect parameters from operation and path-leveldirectly aboveconst allParameters = operationObj.parameters ?? []—pathItem.parameterswas never read. Declaring a path parameter once on the path item is the idiomatic way to avoid repeating it per method, and it produced an emptyGetPetContext extends HttpClientContext {}withurl = \${config.baseUrl}/pets/{petId}`. Verified against a stub transport: the request went tohttps://api.pets.example/pets/{petId}`. It compiled, nothing warned, and there was no workaround — the parameter could not be passed at any price. All of this sits underdocs/inputs/openapi.md, which promises unsupported constructs are "reported with a warning rather than dropped silently".None of this was catchable.
test/blackbox/test_files.tsbuilt its corpus from./schemas/asyncapiand./schemas/jsonschemaonly. There was noschemas/openapi/and every config declaredasyncapiorjsonschema, so the harness'sinputType === 'openapi'arm was dead code — the OpenAPI HTTP client, the largest generator surface, was compile-checked at breadth nowhere.scripts/generateExamples.jseven justifies not compiling examples on the grounds that "compilation of generated output is covered by the blackbox tier". No fixture anywhere declared a schema namedErroror a"204"response, and the one fixture with array-typed 200s (examples/openapi-filtering/openapi.json) documents no other status code, landing it on the safe side of the boundary.Same story for (3):
website-pr-testing.ymlwas path-filtered towebsite/**, but the published docs live at the repo root and are copied in bymove_docs.jsat build time — so a docs-only PR never built the site. AndonBrokenLinks/onBrokenMarkdownLinkswere'warn', so even a triggered build shipped 404s with console noise. Meanwhile the landing page readHello from The Codegen Projectwith the meta descriptionDescription will go into a meta tag in <head />— in production, on every social preview and search snippet — and/api/mcp, whichdocs/ai-assistants.mdtells every visitor to add to Claude Code, Cursor or Windsurf four separate times, returnedDEPLOYMENT_NOT_FOUNDbecausevercel.jsonrewrote it to a preview deployment that no longer existed.Changes
The generated HTTP client compiles and succeeds (
fix(openapi), commit 2)const HttpGlobalError = globalThis.Error; type HttpGlobalError = InstanceType<typeof globalThis.Error>;— and all 19 references acrosscommon-types.ts,client.tsandsecurity.tsgo through it, so an imported payload namedErroris harmless.extractStatusCodeValuereads the decoration from the member itself when it is not a reference, so arrays, primitives and enums get a branch. Those have nounmarshalof their own, so they parse structurally (JSON.parse(json) as Book[]), mirroring the union's genericunmarshal.getNoContentStatusCodescollects bodyless success codes per operation;channels/openapi.tsnow skips an operation only when it declares no responses at all.RenderHttpParameters.replyMessageTypebecame optional and gainednoContentStatusCodes, sodatais typedundefinedfor a wholly bodyless operation and widened toT | undefinedfor a mixed one. New emitted helperreadOptionalJsonBodyreturnsundefinedfor 204/205/304 and for an unparseable body;rawDatafalls back to{}so the publicHttpClientResponseshape is unchanged.host+basePath+schemesnow produce the base URL (httpspreferred), instead of falling through tohttp://localhost:3000for every 2.0 document.generateResponseParsingandresolveReplyTypewere extracted rather than nesting ternaries, which is what lint asked for and reads better anyway.Path-item parameters are inherited (
fix(openapi), commit 1)collectOperationParametersmerges path-item and operation parameters with the specification's override rule — an operation wins over an inherited parameter of the samename+in— used by parameters, headers, and the Swagger 2.0 body parameter.Object.entries(pathItem), soparameters,summary,serversand$refwere each processed as if they were an HTTP method — harmless until path-item parameters started being read, and wrong regardless.HTTP_METHODSmoved into the shared utils sofilter.tsand the processors agree on one list.application/json+multipart/form-dataon one operation silently generated a JSON-only client.The website is repaired, and the gate that would have caught it exists (
fix(website), commit 3)onBrokenLinksandonBrokenMarkdownLinksare'throw';docs/**andassets/**now trigger the website workflow. Anchors stay'warn'deliberately — the ToCs indocs/usage.mdanddocs/migrations/v0.mdare machine-generated, so a heading rename should surface rather than block.protocolsandinputsgot real routes via_category_.jsonslug(relative to the docsrouteBasePath— my first attempt at/docs/protocolslanded the page at/docs/docs/protocols), which fixes the docs links and/docs/protocolsfrom the blog in one change.examples/is not published, so those links point at GitHub, which works from the site and the repo alike. Blog URLs are flat, so../slugand./slugboth escaped/blog; absolute/blog/<slug>also fixes the tag and author listing pages that re-render those excerpts./docs/protocols/http→http_client, which is the page's real name.# Dependencieswas the only h1 among h2 siblings on its page, and the other two were generated ToCs linking their own page title, which gets no anchor — moving those titles into front matter leavesmarkdown-tocnothing to mislink, so regeneration keeps them correct./api/mcppoints at a live deployment. Feature-card SVGs are labelled from their card titles — the stock illustrations carry unrelated internal titles, so a screen reader announced "Powered by React" for "Powered by Open Source".The missing safety net (commit 2)
test/blackbox/schemas/openapi/:awkward-responses.jsoncarries all of the above shapes at once (array200+400, a204+404delete, a202/204-only operation, a primitive200, a schema namedError, path-item parameters, bearer auth) andswagger-2-legacy.jsoncovers 2.0 withhost/basePath/schemesand path-item parameters.openapi-http,openapi-sdk) and anopenapiCI bucket. Named to fall in exactly one bucket —openapi-http-clientwould also have matched theclientbucket's substring and run twice. Verified every config still lands in exactly one bucket.Testing
npm run buildnpm run lint(exit 0)npm run typecheck(exit 0)npm test: 772 passing, 1 skipped, 80 snapshots. Two snapshots updated for the intentional generated-code changes (rawData: rawData ?? {}andinstanceof HttpGlobalError); one existing assertion updated fromextends Errortoextends HttpGlobalError.openapi-http-client-responses.spec.ts(5 tests, driven through the in-memory pipeline so it asserts against the file contents a user actually receives: no bareErrorreference, astatusCode === 200branch that parses structurally, a204-only function typedHttpClientResponse<undefined>withreadOptionalJsonBody, path-item parameter substitution, Swagger 2.0 base URL) and 3 added totest/codegen/inputs/openapi/parameters.spec.ts(inheritance, override byname+in, non-method keys ignored).tsc --noEmitintest/runtime/typescript(exit 0), covering the 5 regeneratedhttp_client.tsfiles alongside the hand-maintained expected-output surfacesonBrokenLinks: 'throw'/api/mcptarget verified live — MCPinitializeoverPOSTreturns 200;GETreturns 405, correct for a POST-only endpointBlackBox testing - openapibucket, all 10 runtime jobs, PR testing on ubuntu/macos/windows, the examples smoke test, and both Vercel deployments/docs/protocolsand/docs/inputsreturn 200 (both were 404 on production), as do/docs/getting-startedand/docs/protocols/http_clientError-schema document goes from 6tscerrors to exit 0;listBooks 200 THREW: No matching type found for status code: 200→listBooks 200 OK;DELETE 204 THREW: SyntaxError→DELETE 204 OK, data = undefined;https://api.pets.example/pets/{petId}→.../pets/p1npm run prepare:pras a single command — not run; itsnpm cisteps fail on pre-existing runtime lockfile drift. Every stage was run directly instead (build,generate:assets,lint:fix,test:update,runtime:typescript:generate). See Notes.website/npm run typecheck— fails, pre-existing and unrelated. See Notes.Notes
No breaking changes to the public config surface. The generated HTTP client's shape changes in three visible ways, all of which fix outright failures:
HttpErrorextends an aliased global rather than the bareError(identical at runtime,instanceof Errorstill holds); theHttpHooks/RetryConfigcallback signatures name that alias instead ofError(structurally identical); and operations with bodyless responses now havedata: undefinedin their type where previously they either did not exist or were mistyped as the error model. A consumer who had worked around a missing 204 function will find it present.deleteBookstill types its error model in the success union (DeleteBookResponse_404 | undefined). That is pre-existing and consistent: error responses are part of the response union for every operation, solistBookslikewise returnsBook[] | ListBooksResponse_400even though a 400 throws before reachingdata. Excluding error codes from the success union is a larger design change to the shared payload union and was deliberately left out of scope.website/npm run typecheckcrashes withRangeError: Maximum call stack size exceededinsidetsc. Confirmed pre-existing by stashing the two website edits in this PR and reproducing. I had added it as a CI step and removed it again rather than turn CI red for an unrelated defect — worth a separate fix, since the script currently runs nowhere.generate:assetsremains non-idempotent on this checkout, independently of these changes:markdown-tocappends a stray blank line to several docs files, anddocs/usage.mdembeds the generating machine's platform string (generate:commandswrotedarwin-arm64 node-v24.15.0over the committedlinux-x64 node-v22.23.1). Both were reverted rather than folded in, sodocs/README.mdcarries no diff anddocs/usage.mdshows only the intended front-matter and ToC change. Still worth a separate fix.vercel.jsoncannot carry comments — I broke the deployment once proving it. My first attempt documented the rewrite with a_commentkey, and Vercel validates that file against a schema withadditionalProperties: false, so the website deployment failed outright while the MCP one built fine. The key is gone and the note moved tomcp-server/README.md, together with the reason it cannot live invercel.json. Both Vercel deployments pass now.The MCP rewrite is still a deployment-specific URL, so it will stop tracking the MCP server after that project's next deploy and need updating by hand; pointing it at the project's production alias would make it self-maintaining. Verified as far as is possible pre-merge: the destination answers the MCP
initializehandshake with 200 when called directly. Through the preview it answers 401"Protected deployment"— that is Vercel's preview SSO, not the rewrite.The canonical-host mismatch was left alone by decision —
docusaurus.config.tsdeclares the apex domain while production serveswww, so everyrel=canonical,og:urland sitemap entry 307-redirects (confirmed:https://the-codegen-project.org/api/mcpreturns 307 today). The config is right; the redirect direction is the thing to fix.Merged
mainin at v0.81.0 (#436). One conflict, indocs/migrations/v0.md, and only over trailing blank lines —generate:readme:tocappends one per run, so both sides had accumulated a different number; resolved to main's count so this branch adds none. The release also refreshedwebsite/src/schemas/configuration-schema.jsonfor thefilterfeature, which is the same refresh this branch had made, so that file now matches main exactly and carries no diff.Conflicts with #422. That PR (
feat(openapi): interface output + fetch-client profiles, open since Jul 16) touches eight of the same files, includingchannels/openapi.ts,protocols/http/client.ts,channels/types.tsandinputs/openapi/generators/parameters.ts. Whichever lands second will need a real merge, not a mechanical one — both change theRenderHttpParametersshape.Follow-ups not taken here, all verified and recorded with evidence in
.claude/thoughts/shared/improvements/2026-07-30-1558-improvements.md:.claude/is gitignored (.gitignore:17) —git ls-files .claudereturns 0 against 51 local files, so every skill, agent, rule and thoughts document is absent from a fresh clone and unfixable through a normal PR. That is the structural root cause of the instruction drift the 12:22 run documented.corePublish.ts:52-58emits a barePromise.reject(...)that is never returned from anasyncmethod, soawait client.publishToX(...)resolves as success while nothing was sent;coreSubscribe.ts:90-105does the same inside an executor that then never settles, so the await hangs forever.inputs/openapi/generators/parameters.ts:666-673wraps each value inencodeURIComponentthen hands it toURLSearchParams.append, which encodes again:tags: ['sci fi', 'a&b']goes out astags=sci%2520fi%2Ca%2526b.security, and multi-content-type request bodies are still not modelled (only warned about, for the last one).filtersuite is omitted from the CI matrix despite existing astest:filter— it passed locally here (8 tests) as part of the aggregate.modelsoutput samples indocs/inputs/jsonschema.mdanddocs/generators/models.mdadvertisemarshal()/unmarshal()the preset never emits,models.md:287crashes withTS_COMMON_PRESET is not defined,isolatedModulesis documented but in no Zod schema, andheaders.mddocuments a validation API that OpenAPI input cannot produce.