Description
A colon inside a path segment is treated as an Express-style path parameter. Routes that use AIP-136 custom methods, such as /v1/items:search or /v1/items/{itemId}:archive, come out with a required string argument that the spec does not declare. The colon suffix is also removed from the URL template.
This looks like the same problem as #532, which was filed as a question and is still open.
Version: 13.12.6. parseRouteName on main is unchanged.
Reproduction
{
"openapi": "3.0.3",
"info": { "title": "repro", "version": "1.0.0" },
"paths": {
"/v1/items:search": {
"post": {
"operationId": "searchItems",
"responses": { "200": { "description": "ok" } }
}
},
"/v1/items/{itemId}:archive": {
"post": {
"operationId": "archiveItem",
"parameters": [
{ "name": "itemId", "in": "path", "required": true, "schema": { "type": "string" } }
],
"responses": { "200": { "description": "ok" } }
}
}
}
}
await generateApi({ input: "spec.json", output: false, httpClientType: "fetch" });
Actual
searchItems: (search: string, params: RequestParams = {}) =>
this.request<void, any>({
path: `/v1/items${search}`,
method: "POST",
...params,
}),
archiveItem: (itemId: string, archive: string, params: RequestParams = {}) =>
this.request<void, any>({
path: `/v1/items/${itemId}${archive}`,
method: "POST",
...params,
}),
searchItems() requires an argument that has no meaning in the spec. If the caller passes a value, it is appended to the URL as is. The only argument that gives the correct URL is the literal ":search".
Expected
searchItems: (params: RequestParams = {}) =>
this.request<void, any>({ path: `/v1/items:search`, method: "POST", ...params }),
archiveItem: (itemId: string, params: RequestParams = {}) =>
this.request<void, any>({ path: `/v1/items/${itemId}:archive`, method: "POST", ...params }),
Cause and a possible fix
In src/schema-routes/schema-routes.ts, parseRouteName matches a colon parameter at any position:
/({[\w[\\\]^`][-_.\w]*})|(:[\w[\\\]^`][-_.\w]*:?)/g
An Express-style parameter always starts a path segment. Requiring a preceding / keeps /users/:userId working and stops matching custom-method suffixes:
/({[\w[\\\]^`][-_.\w]*})|((?<=\/):[\w[\\\]^`][-_.\w]*:?)/g
| route |
current |
with lookbehind |
/v1/items:search |
[":search"] |
none |
/v1/items/{itemId}:archive |
["{itemId}", ":archive"] |
["{itemId}"] |
/users/:userId |
[":userId"] |
[":userId"] |
/users/:userId/posts/:postId |
[":userId", ":postId"] |
[":userId", ":postId"] |
Workaround
We use an onCreateRoute hook. It finds path params whose $match starts with :, puts the literal back into request.path, and removes the param from request.parameters and from requestParams.typeData.content.
Description
A colon inside a path segment is treated as an Express-style path parameter. Routes that use AIP-136 custom methods, such as
/v1/items:searchor/v1/items/{itemId}:archive, come out with a requiredstringargument that the spec does not declare. The colon suffix is also removed from the URL template.This looks like the same problem as #532, which was filed as a question and is still open.
Version: 13.12.6.
parseRouteNameonmainis unchanged.Reproduction
{ "openapi": "3.0.3", "info": { "title": "repro", "version": "1.0.0" }, "paths": { "/v1/items:search": { "post": { "operationId": "searchItems", "responses": { "200": { "description": "ok" } } } }, "/v1/items/{itemId}:archive": { "post": { "operationId": "archiveItem", "parameters": [ { "name": "itemId", "in": "path", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "ok" } } } } } }Actual
searchItems()requires an argument that has no meaning in the spec. If the caller passes a value, it is appended to the URL as is. The only argument that gives the correct URL is the literal":search".Expected
Cause and a possible fix
In
src/schema-routes/schema-routes.ts,parseRouteNamematches a colon parameter at any position:/({[\w[\\\]^`][-_.\w]*})|(:[\w[\\\]^`][-_.\w]*:?)/gAn Express-style parameter always starts a path segment. Requiring a preceding
/keeps/users/:userIdworking and stops matching custom-method suffixes:/({[\w[\\\]^`][-_.\w]*})|((?<=\/):[\w[\\\]^`][-_.\w]*:?)/g/v1/items:search[":search"]/v1/items/{itemId}:archive["{itemId}", ":archive"]["{itemId}"]/users/:userId[":userId"][":userId"]/users/:userId/posts/:postId[":userId", ":postId"][":userId", ":postId"]Workaround
We use an
onCreateRoutehook. It finds path params whose$matchstarts with:, puts the literal back intorequest.path, and removes the param fromrequest.parametersand fromrequestParams.typeData.content.