Skip to content

HttpApiClient treats a literal :action suffix as a path parameter #8251

Description

@robinbraemer

What version of Effect is running?

4.0.0-rc.112 (also reproduces on 4.0.0-rc.115, and on 4.0.0-beta.106)

What steps can reproduce the bug?

REST APIs that follow the Google AIP custom-method convention express an action
on a resource as POST /resources/{id}:action. Written as an Effect path that
is /resources/:id:action, where :id is a placeholder and :action is
literal text.

import { Effect, Schema } from 'effect';
import { HttpApi, HttpApiClient, HttpApiEndpoint, HttpApiGroup } from 'effect/unstable/httpapi';

class Operations extends HttpApiGroup.make('operations').add(
	HttpApiEndpoint.post('wait', '/operations/:id:wait', {
		params: Schema.Struct({ id: Schema.String }),
		success: Schema.Struct({ done: Schema.Boolean })
	})
) {}

class Api extends HttpApi.make('api').add(Operations) {}

const urls = HttpApiClient.urlBuilder(Api, { baseUrl: 'https://api.example.com' });

urls.operations.wait({ params: { id: 'op_1' } });

What is the expected behavior?

https://api.example.com/operations/op_1:wait

What do you see instead?

Error: Missing path parameter: wait

The same throw happens through HttpApiClient.make, before the request is
built, so no request is ever sent. Every endpoint in an AIP-shaped API that
combines a path parameter with a custom method is unreachable from a generated
client. A collection-level custom method such as /offers:resolve is not
affected only because a client call with no params never invokes the path
compiler at all.

Additional information

compilePath in src/unstable/httpapi/HttpApiClient.ts derives placeholders by
scanning the path string:

const paramsRegExp = /(\/?):(\w+)(\?)?/g;

A path string alone cannot decide this: :action after a placeholder, and
:refresh after a static segment as in /clusters/:id/capabilities:refresh,
are both indistinguishable in shape from a placeholder. Tightening the regex
(for example requiring a preceding /) would still get
/clusters/:id/capabilities:refresh wrong, and would drop support for
multi-parameter segments.

The endpoint already carries the authoritative answer. params is declared
explicitly on the endpoint and is never inferred from the path string, so the
names in that schema are exactly the placeholders. Suggested change: pass
endpoint.params into compilePath at both call sites (makeClient and
urlBuilder), read the declared names off the parameter schema, and leave any
:name that is not among them as literal text. An endpoint with no params
substitutes nothing; a parameter schema whose names cannot be read falls back to
today's behavior, so nothing that works now regresses.

The server side is unaffected, so the two ends currently disagree about what a
path means.

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