Skip to content

Commit e50558e

Browse files
authored
Make the restate app mountable (#80)
This commit makes the restate asgi app mountable at a different path than root (`/`) this is helpful when mixing with an FastAPI server that mounts restate related routes elsewhere
1 parent 7473787 commit e50558e

File tree

1 file changed

+36
-7
lines changed

1 file changed

+36
-7
lines changed

python/restate/server.py

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"""This module contains the ASGI server for the restate framework."""
1212

1313
import asyncio
14-
from typing import Dict, Literal
14+
from typing import Dict, TypedDict, Literal
1515
import traceback
1616
from restate.discovery import compute_discovery_json
1717
from restate.endpoint import Endpoint
@@ -135,6 +135,33 @@ async def process_invocation_to_completion(vm: VMWrapper,
135135
class LifeSpanNotImplemented(ValueError):
136136
"""Signal to the asgi server that we didn't implement lifespans"""
137137

138+
139+
class ParsedPath(TypedDict):
140+
"""Parsed path from the request."""
141+
type: Literal["invocation", "health", "discover", "unknown"]
142+
service: str | None
143+
handler: str | None
144+
145+
def parse_path(request: str) -> ParsedPath:
146+
"""Parse the path from the request."""
147+
# The following routes are possible
148+
# $mountpoint/health
149+
# $mountpoint/discover
150+
# $mountpoint/invoke/:service/:handler
151+
# as we don't know the mountpoint, we need to check the path carefully
152+
fragments = request.rsplit('/', 4)
153+
# /invoke/:service/:handler
154+
if len(fragments) >= 3 and fragments[-3] == 'invoke':
155+
return { "type": "invocation" , "handler" : fragments[-1], "service" : fragments[-2] }
156+
# /health
157+
if fragments[-1] == 'health':
158+
return { "type": "health", "service": None, "handler": None }
159+
# /discover
160+
if fragments[-1] == 'discover':
161+
return { "type": "discover" , "service": None, "handler": None }
162+
# anything other than invoke is 404
163+
return { "type": "unknown" , "service": None, "handler": None }
164+
138165
def asgi_app(endpoint: Endpoint):
139166
"""Create an ASGI-3 app for the given endpoint."""
140167

@@ -150,9 +177,10 @@ async def app(scope: Scope, receive: Receive, send: Send):
150177

151178
request_path = scope['path']
152179
assert isinstance(request_path, str)
180+
request: ParsedPath = parse_path(request_path)
153181

154182
# Health check
155-
if request_path == '/health':
183+
if request['type'] == 'health':
156184
await send_health_check(send)
157185
return
158186

@@ -168,16 +196,17 @@ async def app(scope: Scope, receive: Receive, send: Send):
168196
return
169197

170198
# might be a discovery request
171-
if request_path == '/discover':
199+
if request['type'] == 'discover':
172200
await send_discovery(scope, send, endpoint)
173201
return
174202
# anything other than invoke is 404
175-
if not request_path.startswith('/invoke/'):
203+
if request['type'] == 'unknown':
176204
await send404(send, receive)
177205
return
178-
# path is of the form: /invoke/:service/:handler
179-
# strip "/invoke/" (= strlen 8) and split the service and handler
180-
service_name, handler_name = request_path[8:].split('/')
206+
assert request['type'] == 'invocation'
207+
assert request['service'] is not None
208+
assert request['handler'] is not None
209+
service_name, handler_name = request['service'], request['handler']
181210
service = endpoint.services.get(service_name)
182211
if not service:
183212
await send404(send, receive)

0 commit comments

Comments
 (0)