Problem
Some HTTP servers cannot route a path parameter that contains a literal . unless the client sends it percent-encoded as %2E. The common case is Rails: its default dynamic-segment pattern is [^/.?]+, so a . ends the segment and the remainder is parsed as a format suffix. A route like
resources :customers, param: :external_id
matches GET /customers/acme.example.com-42 as external_id = "acme" with format example.com-42 and returns 404, while GET /customers/acme%2Eexample%2Ecom-42 matches correctly, because Rails routes on the raw path before decoding. Identifiers built from hostnames, emails or dotted tenant names hit this routinely, and the API owner is often not in a position to change the server's routing.
Current behaviour in 1.x
There is no supported way to send %2E for a path parameter:
_escapeuri follows RFC 3986 and leaves the unreserved set A-Z a-z 0-9 - _ . ~ untouched, so . always goes on the wire as ..
- Pre-encoding the value (
"acme%2Eexample%2Ecom-42") is double-encoded to %252E.
allowReserved: true on the parameter would pass a pre-encoded %2E through, but it is the wrong tool twice over: a 3.1 document that declares it on a path parameter fails document validation at load time (3.1 scopes the field to in: query), and it also stops escaping every reserved character, which is far broader than wanted.
- 0.2 exposed
pre_request_hook, which is how this was typically handled there. 1.x deliberately has no request hook.
Minimal example
openapi: 3.1.0
info: { title: Example, version: 1.0.0 }
paths:
/customers/{external_id}:
get:
operationId: getCustomer
parameters:
- name: external_id
in: path
required: true
schema: { type: string }
responses:
"200": { description: ok }
client = Example.Client("https://api.example.com")
Example.getcustomer("acme.example.com-42"; client)
# sends GET /customers/acme.example.com-42 -> 404 on a Rails server
# needed: GET /customers/acme%2Eexample%2Ecom-42
Proposal
A client-level option naming extra characters to percent-encode in path parameters, applied after the standard escaping:
client = Example.Client("https://api.example.com"; escape_path_chars = ".")
Example.getcustomer("acme.example.com-42"; client)
# sends GET /customers/acme%2Eexample%2Ecom-42
- Default is empty, so existing behaviour is unchanged.
- Applies to every path parameter of every operation, so callers cannot forget it for one route.
- Percent-encoding an unreserved character is permitted by RFC 3986 §2.3; the encoded and unencoded forms are equivalent identifiers, so a server that decodes before routing is unaffected.
- Declarative and narrow: no hook, no spec edit, works with 3.1 documents, and orthogonal to
allowReserved.
Implementation looks small: _path_scalar (and the array/object variants that call it) would run the listed characters through %XX after _escape, reading the set from the Client.
Alternatives considered
allowReserved on the path parameter. Rejected by the 3.1 loader, and semantically the opposite of what is needed (less escaping, not more).
- Caller pre-encodes the value. Double-encoded today; making the escaper pass through existing
%XX triplets would silently change the meaning of legitimate values that contain a literal %.
- A general pre-request hook. Much larger surface than the problem, and reintroduces the 0.2 pattern that 1.x removed on purpose.
Problem
Some HTTP servers cannot route a path parameter that contains a literal
.unless the client sends it percent-encoded as%2E. The common case is Rails: its default dynamic-segment pattern is[^/.?]+, so a.ends the segment and the remainder is parsed as a format suffix. A route likematches
GET /customers/acme.example.com-42asexternal_id = "acme"with formatexample.com-42and returns 404, whileGET /customers/acme%2Eexample%2Ecom-42matches correctly, because Rails routes on the raw path before decoding. Identifiers built from hostnames, emails or dotted tenant names hit this routinely, and the API owner is often not in a position to change the server's routing.Current behaviour in 1.x
There is no supported way to send
%2Efor a path parameter:_escapeurifollows RFC 3986 and leaves the unreserved setA-Z a-z 0-9 - _ . ~untouched, so.always goes on the wire as.."acme%2Eexample%2Ecom-42") is double-encoded to%252E.allowReserved: trueon the parameter would pass a pre-encoded%2Ethrough, but it is the wrong tool twice over: a 3.1 document that declares it on a path parameter fails document validation at load time (3.1 scopes the field toin: query), and it also stops escaping every reserved character, which is far broader than wanted.pre_request_hook, which is how this was typically handled there. 1.x deliberately has no request hook.Minimal example
Proposal
A client-level option naming extra characters to percent-encode in path parameters, applied after the standard escaping:
allowReserved.Implementation looks small:
_path_scalar(and the array/object variants that call it) would run the listed characters through%XXafter_escape, reading the set from theClient.Alternatives considered
allowReservedon the path parameter. Rejected by the 3.1 loader, and semantically the opposite of what is needed (less escaping, not more).%XXtriplets would silently change the meaning of legitimate values that contain a literal%.