Skip to content

Commit 18574b8

Browse files
Extract ASGI scope creation into function (#162)
* 🏗 simplify magnum call function extract scope and request body creation into a dedicated functions * 📝 add documentation to create scope function
1 parent 70f0750 commit 18574b8

File tree

1 file changed

+89
-76
lines changed

1 file changed

+89
-76
lines changed

mangum/adapter.py

Lines changed: 89 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from dataclasses import dataclass, InitVar
77
from contextlib import ExitStack
88

9-
from mangum.types import ASGIApp
9+
from mangum.types import ASGIApp, Scope
1010
from mangum.protocols.lifespan import LifespanCycle
1111
from mangum.protocols.http import HTTPCycle
1212
from mangum.exceptions import ConfigurationError
@@ -91,89 +91,102 @@ def __call__(self, event: dict, context: "LambdaContext") -> dict:
9191
)
9292
stack.enter_context(lifespan_cycle)
9393

94-
request_context = event["requestContext"]
95-
96-
if event.get("multiValueHeaders"):
97-
headers = {
98-
k.lower(): ", ".join(v) if isinstance(v, list) else ""
99-
for k, v in event.get("multiValueHeaders", {}).items()
100-
}
101-
elif event.get("headers"):
102-
headers = {k.lower(): v for k, v in event.get("headers", {}).items()}
103-
else:
104-
headers = {}
105-
106-
# API Gateway v2
107-
if event.get("version") == "2.0":
108-
source_ip = request_context["http"]["sourceIp"]
109-
path = request_context["http"]["path"]
110-
http_method = request_context["http"]["method"]
111-
query_string = event.get("rawQueryString", "").encode()
112-
113-
if event.get("cookies"):
114-
headers["cookie"] = "; ".join(event.get("cookies", []))
115-
116-
# API Gateway v1 / ELB
117-
else:
118-
if "elb" in request_context:
119-
# NOTE: trust only the most right side value
120-
source_ip = headers.get("x-forwarded-for", "").split(", ")[-1]
121-
else:
122-
source_ip = request_context.get("identity", {}).get("sourceIp")
123-
124-
path = event["path"]
125-
http_method = event["httpMethod"]
126-
127-
if event.get("multiValueQueryStringParameters"):
128-
query_string = urllib.parse.urlencode(
129-
event.get("multiValueQueryStringParameters", {}), doseq=True
130-
).encode()
131-
elif event.get("queryStringParameters"):
132-
query_string = urllib.parse.urlencode(
133-
event.get("queryStringParameters", {})
134-
).encode()
135-
else:
136-
query_string = b""
137-
138-
server_name = headers.get("host", "mangum")
139-
if ":" not in server_name:
140-
server_port = headers.get("x-forwarded-port", 80)
141-
else:
142-
server_name, server_port = server_name.split(":") # pragma: no cover
143-
server = (server_name, int(server_port))
144-
client = (source_ip, 0)
145-
146-
if not path: # pragma: no cover
147-
path = "/"
148-
elif self.api_gateway_base_path:
149-
if path.startswith(self.api_gateway_base_path):
150-
path = path[len(self.api_gateway_base_path) :]
151-
152-
scope = {
153-
"type": "http",
154-
"http_version": "1.1",
155-
"method": http_method,
156-
"headers": [[k.encode(), v.encode()] for k, v in headers.items()],
157-
"path": urllib.parse.unquote(path),
158-
"raw_path": None,
159-
"root_path": "",
160-
"scheme": headers.get("x-forwarded-proto", "https"),
161-
"query_string": query_string,
162-
"server": server,
163-
"client": client,
164-
"asgi": {"version": "3.0"},
165-
"aws.event": event,
166-
"aws.context": context,
167-
}
168-
16994
is_binary = event.get("isBase64Encoded", False)
17095
initial_body = event.get("body") or b""
17196
if is_binary:
17297
initial_body = base64.b64decode(initial_body)
17398
elif not isinstance(initial_body, bytes):
17499
initial_body = initial_body.encode()
175100

101+
scope = self._create_scope(event, context)
176102
http_cycle = HTTPCycle(scope, text_mime_types=self.text_mime_types)
177103
response = http_cycle(self.app, initial_body)
178104

179105
return response
106+
107+
def _create_scope(self, event: dict, context: "LambdaContext") -> Scope:
108+
"""Creates a scope object according to ASGI specification from a Lambda Event.
109+
110+
https://asgi.readthedocs.io/en/latest/specs/www.html#http-connection-scope
111+
112+
The event comes from various sources: AWS ALB, AWS API Gateway of different
113+
versions and configurations(multivalue header, etc).
114+
Thus, some heuristics is applied to guess an event type.
115+
116+
"""
117+
request_context = event["requestContext"]
118+
119+
if event.get("multiValueHeaders"):
120+
headers = {
121+
k.lower(): ", ".join(v) if isinstance(v, list) else ""
122+
for k, v in event.get("multiValueHeaders", {}).items()
123+
}
124+
elif event.get("headers"):
125+
headers = {k.lower(): v for k, v in event.get("headers", {}).items()}
126+
else:
127+
headers = {}
128+
129+
# API Gateway v2
130+
if event.get("version") == "2.0":
131+
source_ip = request_context["http"]["sourceIp"]
132+
path = request_context["http"]["path"]
133+
http_method = request_context["http"]["method"]
134+
query_string = event.get("rawQueryString", "").encode()
135+
136+
if event.get("cookies"):
137+
headers["cookie"] = "; ".join(event.get("cookies", []))
138+
139+
# API Gateway v1 / ELB
140+
else:
141+
if "elb" in request_context:
142+
# NOTE: trust only the most right side value
143+
source_ip = headers.get("x-forwarded-for", "").split(", ")[-1]
144+
else:
145+
source_ip = request_context.get("identity", {}).get("sourceIp")
146+
147+
path = event["path"]
148+
http_method = event["httpMethod"]
149+
150+
if event.get("multiValueQueryStringParameters"):
151+
query_string = urllib.parse.urlencode(
152+
event.get("multiValueQueryStringParameters", {}), doseq=True
153+
).encode()
154+
elif event.get("queryStringParameters"):
155+
query_string = urllib.parse.urlencode(
156+
event.get("queryStringParameters", {})
157+
).encode()
158+
else:
159+
query_string = b""
160+
161+
server_name = headers.get("host", "mangum")
162+
if ":" not in server_name:
163+
server_port = headers.get("x-forwarded-port", 80)
164+
else:
165+
server_name, server_port = server_name.split(":") # pragma: no cover
166+
server = (server_name, int(server_port))
167+
client = (source_ip, 0)
168+
169+
if not path: # pragma: no cover
170+
path = "/"
171+
elif self.api_gateway_base_path:
172+
if path.startswith(self.api_gateway_base_path):
173+
path = path[len(self.api_gateway_base_path) :]
174+
175+
scope = {
176+
"type": "http",
177+
"http_version": "1.1",
178+
"method": http_method,
179+
"headers": [[k.encode(), v.encode()] for k, v in headers.items()],
180+
"path": urllib.parse.unquote(path),
181+
"raw_path": None,
182+
"root_path": "",
183+
"scheme": headers.get("x-forwarded-proto", "https"),
184+
"query_string": query_string,
185+
"server": server,
186+
"client": client,
187+
"asgi": {"version": "3.0"},
188+
"aws.event": event,
189+
"aws.context": context,
190+
}
191+
192+
return scope

0 commit comments

Comments
 (0)