• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

localstack / localstack / 20565403496

29 Dec 2025 05:11AM UTC coverage: 84.103% (-2.8%) from 86.921%
20565403496

Pull #13567

github

web-flow
Merge 4816837a5 into 2417384aa
Pull Request #13567: Update ASF APIs

67166 of 79862 relevant lines covered (84.1%)

0.84 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.83
/localstack-core/localstack/services/apigateway/next_gen/execute_api/handlers/parse.py
1
import datetime
1✔
2
import logging
1✔
3
import re
1✔
4
from collections import defaultdict
1✔
5
from urllib.parse import urlparse
1✔
6

7
from rolo.request import restore_payload
1✔
8
from werkzeug.datastructures import Headers, MultiDict
1✔
9

10
from localstack.http import Response
1✔
11
from localstack.services.apigateway.helpers import REQUEST_TIME_DATE_FORMAT
1✔
12
from localstack.utils.strings import long_uid, short_uid
1✔
13
from localstack.utils.time import timestamp
1✔
14

15
from ..api import RestApiGatewayHandler, RestApiGatewayHandlerChain
1✔
16
from ..context import InvocationRequest, RestApiInvocationContext
1✔
17
from ..header_utils import should_drop_header_from_invocation
1✔
18
from ..helpers import generate_trace_id, generate_trace_parent, parse_trace_id
1✔
19
from ..variables import (
1✔
20
    ContextVariableOverrides,
21
    ContextVariables,
22
    ContextVarsIdentity,
23
    ContextVarsRequestOverride,
24
    ContextVarsResponseOverride,
25
)
26

27
LOG = logging.getLogger(__name__)
1✔
28

29

30
class InvocationRequestParser(RestApiGatewayHandler):
1✔
31
    def __call__(
1✔
32
        self,
33
        chain: RestApiGatewayHandlerChain,
34
        context: RestApiInvocationContext,
35
        response: Response,
36
    ):
37
        context.account_id = context.deployment.account_id
1✔
38
        context.region = context.deployment.region
1✔
39
        self.parse_and_enrich(context)
1✔
40

41
    def parse_and_enrich(self, context: RestApiInvocationContext):
1✔
42
        # first, create the InvocationRequest with the incoming request
43
        context.invocation_request = self.create_invocation_request(context)
1✔
44
        # then we can create the ContextVariables, used throughout the invocation as payload and to render authorizer
45
        # payload, mapping templates and such.
46
        context.context_variables = self.create_context_variables(context)
1✔
47
        context.context_variable_overrides = ContextVariableOverrides(
1✔
48
            requestOverride=ContextVarsRequestOverride(header={}, querystring={}, path={}),
49
            responseOverride=ContextVarsResponseOverride(header={}, status=0),
50
        )
51
        # TODO: maybe adjust the logging
52
        LOG.debug("Initializing $context='%s'", context.context_variables)
1✔
53
        # then populate the stage variables
54
        context.stage_variables = self.get_stage_variables(context)
1✔
55
        LOG.debug("Initializing $stageVariables='%s'", context.stage_variables)
1✔
56

57
        context.trace_id = self.populate_trace_id(context.request.headers)
1✔
58

59
    def create_invocation_request(self, context: RestApiInvocationContext) -> InvocationRequest:
1✔
60
        request = context.request
1✔
61
        params, multi_value_params = self._get_single_and_multi_values_from_multidict(request.args)
1✔
62
        headers = self._get_invocation_headers(request.headers)
1✔
63
        invocation_request = InvocationRequest(
1✔
64
            http_method=request.method,
65
            query_string_parameters=params,
66
            multi_value_query_string_parameters=multi_value_params,
67
            headers=headers,
68
            body=restore_payload(request),
69
        )
70
        self._enrich_with_raw_path(context, invocation_request)
1✔
71

72
        return invocation_request
1✔
73

74
    @staticmethod
1✔
75
    def _enrich_with_raw_path(
1✔
76
        context: RestApiInvocationContext, invocation_request: InvocationRequest
77
    ):
78
        # Base path is not URL-decoded, so we need to get the `RAW_URI` from the request
79
        request = context.request
1✔
80
        raw_uri = request.environ.get("RAW_URI") or request.path
1✔
81

82
        # if the request comes from the LocalStack only `_user_request_` route, we need to remove this prefix from the
83
        # path, in order to properly route the request
84
        if "_user_request_" in raw_uri:
1✔
85
            # in this format, the stage is before `_user_request_`, so we don't need to remove it
86
            raw_uri = raw_uri.partition("_user_request_")[2]
1✔
87
        else:
88
            if raw_uri.startswith("/_aws/execute-api"):
1✔
89
                # the API can be cased in the path, so we need to ignore it to remove it
90
                raw_uri = re.sub(
1✔
91
                    f"^/_aws/execute-api/{context.api_id}",
92
                    "",
93
                    raw_uri,
94
                    flags=re.IGNORECASE,
95
                )
96

97
            # remove the stage from the path, only replace the first occurrence
98
            raw_uri = raw_uri.replace(f"/{context.stage}", "", 1)
1✔
99

100
        if raw_uri.startswith("//"):
1✔
101
            # TODO: AWS validate this assumption
102
            # if the RAW_URI starts with double slashes, `urlparse` will fail to decode it as path only
103
            # it also means that we already only have the path, so we just need to remove the query string
104
            raw_uri = raw_uri.split("?")[0]
1✔
105
            raw_path = "/" + raw_uri.lstrip("/")
1✔
106

107
        else:
108
            # we need to make sure we have a path here, sometimes RAW_URI can be a full URI (when proxied)
109
            raw_path = raw_uri = urlparse(raw_uri).path
1✔
110

111
        invocation_request["path"] = raw_path
1✔
112
        invocation_request["raw_path"] = raw_uri
1✔
113

114
    @staticmethod
1✔
115
    def _get_single_and_multi_values_from_multidict(
1✔
116
        multi_dict: MultiDict,
117
    ) -> tuple[dict[str, str], dict[str, list[str]]]:
118
        single_values = {}
1✔
119
        multi_values = defaultdict(list)
1✔
120

121
        for key, value in multi_dict.items(multi=True):
1✔
122
            multi_values[key].append(value)
1✔
123
            # for the single value parameters, AWS only keeps the last value of the list
124
            single_values[key] = value
1✔
125

126
        return single_values, dict(multi_values)
1✔
127

128
    @staticmethod
1✔
129
    def _get_invocation_headers(headers: Headers) -> Headers:
1✔
130
        invocation_headers = Headers()
1✔
131
        for key, value in headers:
1✔
132
            if should_drop_header_from_invocation(key):
1✔
133
                LOG.debug("Dropping header from invocation request: '%s'", key)
1✔
134
                continue
1✔
135
            invocation_headers.add(key, value)
1✔
136
        return invocation_headers
1✔
137

138
    @staticmethod
1✔
139
    def create_context_variables(context: RestApiInvocationContext) -> ContextVariables:
1✔
140
        invocation_request: InvocationRequest = context.invocation_request
1✔
141
        domain_name = invocation_request["headers"].get("Host", "")
1✔
142
        domain_prefix = domain_name.split(".")[0]
1✔
143
        now = datetime.datetime.now()
1✔
144

145
        context_variables = ContextVariables(
1✔
146
            accountId=context.account_id,
147
            apiId=context.api_id,
148
            deploymentId=context.deployment_id,
149
            domainName=domain_name,
150
            domainPrefix=domain_prefix,
151
            extendedRequestId=short_uid(),  # TODO: use snapshot tests to verify format
152
            httpMethod=invocation_request["http_method"],
153
            identity=ContextVarsIdentity(
154
                accountId=None,
155
                accessKey=None,
156
                caller=None,
157
                cognitoAuthenticationProvider=None,
158
                cognitoAuthenticationType=None,
159
                cognitoIdentityId=None,
160
                cognitoIdentityPoolId=None,
161
                principalOrgId=None,
162
                sourceIp="127.0.0.1",  # TODO: get the sourceIp from the Request
163
                user=None,
164
                userAgent=invocation_request["headers"].get("User-Agent"),
165
                userArn=None,
166
            ),
167
            path=f"/{context.stage}{invocation_request['raw_path']}",
168
            protocol="HTTP/1.1",
169
            requestId=long_uid(),
170
            requestTime=timestamp(time=now, format=REQUEST_TIME_DATE_FORMAT),
171
            requestTimeEpoch=int(now.timestamp() * 1000),
172
            stage=context.stage,
173
        )
174
        if context.is_canary is not None:
1✔
175
            context_variables["isCanaryRequest"] = context.is_canary
×
176

177
        return context_variables
1✔
178

179
    @staticmethod
1✔
180
    def get_stage_variables(context: RestApiInvocationContext) -> dict[str, str] | None:
1✔
181
        stage_variables = context.stage_configuration.get("variables")
1✔
182
        if context.is_canary:
1✔
183
            overrides = (
×
184
                context.stage_configuration["canarySettings"].get("stageVariableOverrides") or {}
185
            )
186
            stage_variables = (stage_variables or {}) | overrides
×
187

188
        if not stage_variables:
1✔
189
            return None
×
190

191
        return stage_variables
1✔
192

193
    @staticmethod
1✔
194
    def populate_trace_id(headers: Headers) -> str:
1✔
195
        incoming_trace = parse_trace_id(headers.get("x-amzn-trace-id", ""))
1✔
196
        # parse_trace_id always return capitalized keys
197

198
        trace = incoming_trace.get("Root", generate_trace_id())
1✔
199
        incoming_parent = incoming_trace.get("Parent")
1✔
200
        parent = incoming_parent or generate_trace_parent()
1✔
201
        sampled = incoming_trace.get("Sampled", "1" if incoming_parent else "0")
1✔
202
        # TODO: lineage? not sure what it related to
203
        return f"Root={trace};Parent={parent};Sampled={sampled}"
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc