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.
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 thatis
/resources/:id:action, where:idis a placeholder and:actionisliteral text.
What is the expected behavior?
https://api.example.com/operations/op_1:waitWhat do you see instead?
The same throw happens through
HttpApiClient.make, before the request isbuilt, 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:resolveis notaffected only because a client call with no
paramsnever invokes the pathcompiler at all.
Additional information
compilePathinsrc/unstable/httpapi/HttpApiClient.tsderives placeholders byscanning the path string:
A path string alone cannot decide this:
:actionafter a placeholder, and:refreshafter 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:refreshwrong, and would drop support formulti-parameter segments.
The endpoint already carries the authoritative answer.
paramsis declaredexplicitly 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.paramsintocompilePathat both call sites (makeClientandurlBuilder), read the declared names off the parameter schema, and leave any:namethat is not among them as literal text. An endpoint with noparamssubstitutes 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.