Add a native route#

This guide shows you how to serve an ASGI endpoint from the same process as Plone. Register it in ZCML when it belongs to an add-on, or pass it to create_app when it belongs to the project.

Write the endpoint#

An HTTP endpoint registered in ZCML is a sync function. It takes a PloneContext and a Starlette Request, and returns a Starlette Response:

from starlette.responses import JSONResponse


def status(ctx, request):
    return JSONResponse({"ok": True, "site": ctx.portal.getId()})

It runs in a worker thread, after the permission check and in the same context. To read Plone content from it, see Read content from a route.

A websocket endpoint is async, and takes a WebSocket:

async def echo(websocket):
    await websocket.accept()
    async for message in websocket.iter_text():
        await websocket.send_text(message)

Register it in ZCML#

Declare the route in a ZCML file that Zope loads, for example your add-on's configure.zcml:

<configure
    xmlns="http://namespaces.zope.org/zope"
    xmlns:ploneasgi="http://namespaces.plone.org/ploneasgi"
    >

  <include package="ploneasgi" file="meta.zcml" />

  <ploneasgi:route
      path="/status"
      handler=".endpoints.status"
      permission="zope2.View"
      />

  <ploneasgi:route
      path="/echo"
      handler=".endpoints.echo"
      type="websocket"
      permission="cmf.ManagePortal"
      />

</configure>

Give every route a permission id. For an endpoint meant to be open to anyone, write unguarded="true" instead.

Then make sure the project's server.py collects ZCML routes after Zope starts:

wsgi_app = build_wsgi_app(settings)
app = create_app(
    settings,
    routes=routes_from_registry(prefix=settings.route_prefix),
    wsgi_app=wsgi_app,
)

ZCML changes take effect when you restart the server.

Pass it to create_app#

A project can pass Starlette routes directly. These are plain Starlette endpoints, async, with no context of their own:

async def ping(request):
    return JSONResponse({"ok": True})

Mount them under the reserved prefix, so the proxy rule serves them:

from starlette.routing import Route
from starlette.routing import WebSocketRoute

prefix = settings.route_prefix
app = create_app(
    settings,
    routes=[
        Route(f"{prefix}/ping", ping),
        WebSocketRoute(f"{prefix}/echo", echo),
    ],
)

A route passed this way has no permission guard. Check authorization in the endpoint, with run_authenticated(). Each call is a thread hop and a ZODB connection, so do the check and the work in one call.

Check it#

Without a proxy, request the route under the prefix:

curl http://127.0.0.1:8080/++asgi++/status

A ZCML route that answers 403 is working: the request did not have the permission. Send credentials that do, for example with curl -u admin:<password>.