Skip to content

Generated servers cannot route a path parameter declared allowReserved: true #113

Description

@tanmaykm

Summary

Since the runtime honours allowReserved on path parameters, a generated
client sends a slash-delimited path value as multiple path segments. A
generated server built from the same document still registers the raw
template (/documents/{path}), and HTTP.Router matches {name} against
exactly one segment — so the request 404s before it ever reaches
_decode_path_parameter.

One document therefore produces a client and a server that cannot talk to each
other, and the failure is a silent 404 rather than a diagnostic.

Reproduction

Document (3.0 or 3.2 — see the version note below):

"/documents/{path}": {
  "get": {
    "operationId": "getDocument",
    "parameters": [{
      "name": "path", "in": "path", "required": true,
      "allowReserved": true,
      "schema": { "type": "string" }
    }],
    "responses": { "200": { "description": "ok",
      "content": { "text/plain": { "schema": { "type": "string" } } } } }
  }
}

Generate the server, register! it on an HTTP.Router, and dispatch both wire
forms of the value opa/examples/public servers:

/documents/opa%2Fexamples%2Fpublic%20servers  -> 200 got:opa/examples/public servers
/documents/opa/examples/public%20servers      -> 404 <empty>   <-- what the client now sends

Cause

ext/OpenAPIHTTPExt.jl registers the template verbatim:

HTTP.register!(router, entry.method, string(path_prefix, entry.path), handler)

HTTP.Handlers.gethandler splits the request target on / and matches
segment by segment, so {path} is inherently single-segment.

_decode_path_parameter is not at fault — given the full multi-segment raw
text it _percent_decodes to the correct value. Only the route shape is wrong.

Constraints on a fix

  • HTTP.jl's only multi-segment construct is **, and insert! hard-errors
    with "/** double wildcard must be last segment in path". A greedy route is
    therefore only possible when the allowReserved parameter is the final
    path segment. /documents/{path}/versions is unservable this way and should
    raise a generation-time diagnostic rather than emit a silently broken route.
  • ** is not parsed as a Variable, so it contributes nothing to
    HTTP.getparams. The generated handler has to slice the tail out of
    request.target itself.
  • OpenAPI.server_source is a public seam and the HTTP extension is only one
    consumer. The "this operation has a greedy tail parameter" fact belongs in
    _SERVER_OPS (e.g. a greedy_path_arg field) rather than inside
    GENERATED_HTTP_SERVER_GLUE, so other framework extensions do not each
    re-derive it.
  • Not confined to style: simple. label and matrix values span segments
    too (.a/b, ;path=a/b), so those branches of _decode_path_parameter are
    in scope for whatever routing fix lands.
  • allowReserved preserves existing percent-escapes (_escape("%2F"; allow_reserved = true) == "%2F"), so a value whose literal text contains
    %2F is indistinguishable from a separator on the wire. That ambiguity is
    inherent to the feature and should be documented, not worked around.

Tests

SERVER_ROUNDTRIP_DOCUMENT in test/servergen.jl has no allowReserved path
parameter, which is why nothing caught this. A round-trip case there — generated
client target in, generated server handler argument out — is the right place for
the regression test.

Version note

The bundled spec schemas disagree about whether allowReserved is even legal
on a path parameter:

Document version allowReserved on a path parameter
3.0 accepted (generic Parameter property; PathParameter does not forbid it)
3.1 rejected — scoped to styles-for-query under unevaluatedProperties: false; the document fails to load even with strict = false
3.2 accepted, explicitly (styles-for-path)

So this affects 3.0 and 3.2 documents only. The client-side caveat is already
documented in MIGRATION.md and docs/src/clients.md.

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