Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,47 @@ config.add_route("foo_route", pattern="/foo")

The `pyramid_openapi3_register_routes()` method supports setting a factory and route prefix as well. See the source for details.

### Deployment: the `servers` URL is a mount point, not a route prefix

A common point of confusion is how a `servers` entry with a path interacts with
route registration:

```yaml
servers:
- url: /api
paths:
/v2/users/:
x-pyramid-route-name: users
```

`pyramid_openapi3` treats the `servers` URL path (`/api`) as **where the whole
application is mounted**, not as a prefix to prepend to your route patterns. Your
spec paths (`/v2/users/`) are written *relative to that mount point*. This gives
you two supported deployment styles — pick **one**:

- **Mounted (recommended when a reverse proxy / WSGI compositor mounts the app):**
deploy the app under `/api` so the WSGI server sets `SCRIPT_NAME=/api`, and
register routes verbatim. Requests to `/api/v2/users/` are routed and validated
correctly.

```python
config.pyramid_openapi3_register_routes() # routes at /v2/users/
```

- **Prefixed (when the app runs at the URL root):** bake the prefix into the routes
with `route_prefix`, and do **not** mount the app.

```python
config.pyramid_openapi3_register_routes(route_prefix="/api") # routes at /api/v2/users/
```

Do **not** combine the two: mounting the app at `/api` *and* passing
`route_prefix="/api"` double-prefixes the routes (they end up expecting
`/api/api/v2/users/`) and requests will 404. Likewise, using `servers: /api` while
running at the root *without* either mounting or `route_prefix="/api"` means
`/api/v2/users/` has no route (404) and the un-prefixed `/v2/users/` fails
server-matching validation — pick one of the two styles above instead.

### Specify protocol and port for getting the OpenAPI 3 spec file

Sometimes, it is necessary to specify the protocol and port to access the openapi3 spec file. This can be configured using the `proto_port` optional parameter to the the `pyramid_openapi3_add_explorer` function:
Expand Down
80 changes: 80 additions & 0 deletions pyramid_openapi3/tests/test_routes.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
"""Tests routes."""

from pyramid.httpexceptions import HTTPNotFound
from pyramid.request import Request
from pyramid.testing import testConfig
from webtest.app import TestApp

import pytest
import tempfile


Expand Down Expand Up @@ -133,3 +136,80 @@ def test_register_routes_with_prefix() -> None:
("pyramid_openapi3.spec_json", "/openapi.json"),
("foo", "/api/v1/foo"),
]


# GH #194 -- the `servers` URL is a mount point, not a route prefix.
GH194_SPEC = b"""\
openapi: "3.1.0"
info:
version: "1.0.0"
title: Foo API
servers:
- url: /api
paths:
/v2/users/:
x-pyramid-route-name: users
get:
responses:
"200":
description: A list of users
content:
application/json:
schema:
type: array
items:
type: string
"""


def _users_view(*args: object) -> list:
"""Return a response that validates against GH194_SPEC's 200."""
return []


@pytest.mark.parametrize(
("route_prefix", "script_name", "path_info", "expected_status"),
[
# style A -- mounted at /api (SCRIPT_NAME), routes registered verbatim.
# The `servers` prefix is the mount point, so verbatim is correct.
pytest.param(None, "/api", "/v2/users/", 200, id="mounted-verbatim"),
# style B -- app at the root, prefix baked into the routes explicitly.
pytest.param("/api", "", "/api/v2/users/", 200, id="root-route_prefix"),
# style C (the trap) -- mounting *and* route_prefix double-prefixes, so the
# route expects /api/api/v2/users/ and the request 404s. This is why the
# `servers` prefix must NOT be auto-derived: it would push every mounted app
# (style A) into this broken state.
pytest.param("/api", "/api", "/v2/users/", 404, id="mounted-and-route_prefix"),
],
)
def test_servers_url_is_mount_point_not_route_prefix(
route_prefix: str | None, script_name: str, path_info: str, expected_status: int
) -> None:
"""GH #194: `servers` is a mount point, so verbatim registration is correct.

Drive a real request through each supported deployment style (and the trap of
combining them). The external URL is http://localhost/api/v2/users/ in every
case; ``SCRIPT_NAME`` is the mount point, ``PATH_INFO`` is the remainder.
"""
with testConfig() as config:
config.include("pyramid_openapi3")
# Render an unmatched route as a 404 response (like a real app) instead of
# letting the raw HTTPNotFound propagate, so the style-C miss is observable.
config.add_notfound_view(lambda request: HTTPNotFound())
with tempfile.NamedTemporaryFile() as tempdoc:
tempdoc.write(GH194_SPEC)
tempdoc.seek(0)
config.pyramid_openapi3_spec(tempdoc.name)
# route_prefix=None is the directive's default (verbatim registration).
config.pyramid_openapi3_register_routes(route_prefix=route_prefix)
config.add_view(
_users_view, route_name="users", renderer="json", openapi=True
)
test_app = TestApp(config.make_wsgi_app())
response = test_app.get(
path_info,
extra_environ={"SCRIPT_NAME": script_name},
expect_errors=True,
)

assert response.status_int == expected_status