Skip to content

Colon suffix in path segment (AIP-136 custom method, e.g. /v1/items:search) becomes a phantom required path param #1833

Description

@denis-markushin

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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