33import io
44import json
55import logging
6+ from concurrent .futures import ThreadPoolExecutor
67from datetime import datetime
8+ from typing import Dict , Optional , Tuple
79from urllib .parse import unquote
810
9- from flask import Flask , request
11+ from flask import Flask , Response , request
1012from werkzeug .routing import BaseConverter
1113
1214from samcli .commands .local .cli_common .durable_context import DurableContext
1315from samcli .commands .local .lib .exceptions import TenantIdValidationError , UnsupportedInlineCodeError
16+ from samcli .lib .utils .invocation_type import EVENT
1417from samcli .lib .utils .name_utils import InvalidFunctionNameException , normalize_sam_function_identifier
1518from samcli .lib .utils .stream_writer import StreamWriter
1619from samcli .local .docker .exceptions import DockerContainerCreationFailedException
@@ -67,6 +70,7 @@ def __init__(self, lambda_runner, port, host, stderr=None, ssl_context=None):
6770 super ().__init__ (lambda_runner .is_debugging (), port = port , host = host , ssl_context = ssl_context )
6871 self .lambda_runner = lambda_runner
6972 self .stderr = stderr
73+ self .executor = ThreadPoolExecutor ()
7074
7175 def create (self ):
7276 """
@@ -240,32 +244,36 @@ def _invoke_request_handler(self, function_name):
240244 # Extract durable execution name from headers
241245 durable_execution_name = flask_request .headers .get ("X-Amz-Durable-Execution-Name" )
242246
243- stdout_stream_string = io .StringIO ()
244- stdout_stream_bytes = io .BytesIO ()
245- stdout_stream_writer = StreamWriter (stdout_stream_string , stdout_stream_bytes , auto_flush = True )
247+ arguments = {
248+ "function_name" : function_name ,
249+ "request_data" : request_data ,
250+ "invocation_type" : invocation_type ,
251+ "durable_execution_name" : durable_execution_name ,
252+ "tenant_id" : tenant_id ,
253+ }
254+
255+ headers = {"Content-Type" : "application/json" }
256+
257+ if invocation_type == EVENT :
258+ # Validate function exists before submitting async task
259+ if validation_error := self ._validate_function_for_invocation (function_name ):
260+ return validation_error
261+
262+ self .executor .submit (self ._invoke_async_lambda , ** arguments )
263+ return self .service_response ("" , headers , 202 )
246264
247265 try :
248- # Normalize function name from ARN if provided
249- normalized_function_name = normalize_sam_function_identifier (function_name )
250-
251- invoke_headers = self .lambda_runner .invoke (
252- normalized_function_name ,
253- request_data ,
254- invocation_type = invocation_type ,
255- durable_execution_name = durable_execution_name ,
256- tenant_id = tenant_id ,
257- stdout = stdout_stream_writer ,
258- stderr = self .stderr ,
259- )
266+ invoke_headers , stdout_stream_string , stdout_stream_bytes = self ._invoke_lambda (** arguments )
260267 except (InvalidFunctionNameException , TenantIdValidationError ) as e :
261268 LOG .error ("Validation error: %s" , str (e ))
262269 return LambdaErrorResponses .validation_exception (str (e ))
263270 except UnsupportedInvocationType as e :
264- LOG .warning ("invocation-type: %s is not supported. RequestResponse is only supported." , invocation_type )
271+ LOG .warning (
272+ "invocation-type: %s is not supported. Event and RequestResponse are only supported." , invocation_type
273+ )
265274 return LambdaErrorResponses .not_implemented_locally (str (e ))
266275 except FunctionNotFound :
267- LOG .debug ("%s was not found to invoke." , normalized_function_name )
268- return LambdaErrorResponses .resource_not_found (normalized_function_name )
276+ return self ._handle_function_not_found (function_name )
269277 except UnsupportedInlineCodeError :
270278 return LambdaErrorResponses .not_implemented_locally (
271279 "Inline code is not supported for sam local commands. Please write your code in a separate file."
@@ -278,20 +286,94 @@ def _invoke_request_handler(self, function_name):
278286 )
279287
280288 # Prepare headers
281- headers = {"Content-Type" : "application/json" }
282289 if invoke_headers and isinstance (invoke_headers , dict ):
283290 headers .update (invoke_headers )
284291
285292 if is_lambda_user_error_response :
286293 headers ["x-amz-function-error" ] = "Unhandled"
287- return self .service_response (lambda_response , headers , 200 )
288-
289- # For async invocations (Event type), return 202
290- if invocation_type == "Event" :
291- return self .service_response ("" , headers , 202 )
292294
293295 return self .service_response (lambda_response , headers , 200 )
294296
297+ def _validate_function_for_invocation (self , function_name : str ) -> Optional [Response ]:
298+ """
299+ Validates that a function exists and can be invoked.
300+
301+ Parameters
302+ ----------
303+ function_name : str
304+ Name or ARN of the function to validate
305+
306+ Returns
307+ -------
308+ Flask.Response or None
309+ Error response if validation fails, None if validation succeeds
310+ """
311+ try :
312+ self .lambda_runner .get_function (function_name )
313+ return None
314+ except FunctionNotFound :
315+ return self ._handle_function_not_found (function_name )
316+ except InvalidFunctionNameException as e :
317+ LOG .error ("Validation error: %s" , str (e ))
318+ return LambdaErrorResponses .validation_exception (str (e ))
319+
320+ def _handle_function_not_found (self , function_name : str ) -> Response :
321+ """
322+ Handles FunctionNotFound exception by returning appropriate error response.
323+
324+ Parameters
325+ ----------
326+ function_name : str
327+ Name or ARN of the function that was not found
328+
329+ Returns
330+ -------
331+ Flask.Response
332+ Error response for function not found
333+ """
334+ normalized_function_name = normalize_sam_function_identifier (function_name )
335+ LOG .debug ("%s was not found to invoke." , normalized_function_name )
336+ return LambdaErrorResponses .resource_not_found (normalized_function_name )
337+
338+ def _invoke_async_lambda (self , function_name : str , ** kwargs ) -> None :
339+ """
340+ Wrapper for _invoke_lambda that runs in an async context (Event invocation type)
341+ """
342+ try :
343+ self ._invoke_lambda (function_name = function_name , ** kwargs )
344+ except Exception as e :
345+ LOG .error ("Async invocation failed for function %s: %s" , function_name , str (e ), exc_info = True )
346+
347+ def _invoke_lambda (
348+ self ,
349+ function_name : str ,
350+ request_data : str ,
351+ invocation_type : str ,
352+ durable_execution_name : Optional [str ],
353+ tenant_id : Optional [str ],
354+ ) -> Tuple [Optional [Dict [str , str ]], io .StringIO , io .BytesIO ]:
355+ """
356+ Invokes a Lambda function and returns the result
357+ """
358+
359+ stdout_stream_string = io .StringIO ()
360+ stdout_stream_bytes = io .BytesIO ()
361+ stdout_stream_writer = StreamWriter (stdout_stream_string , stdout_stream_bytes , auto_flush = True )
362+
363+ normalized_function_name = normalize_sam_function_identifier (function_name )
364+
365+ invoke_headers = self .lambda_runner .invoke (
366+ normalized_function_name ,
367+ request_data ,
368+ invocation_type = invocation_type ,
369+ durable_execution_name = durable_execution_name ,
370+ tenant_id = tenant_id ,
371+ stdout = stdout_stream_writer ,
372+ stderr = self .stderr ,
373+ )
374+
375+ return invoke_headers , stdout_stream_string , stdout_stream_bytes
376+
295377 def _get_durable_execution_handler (self , durable_execution_arn ):
296378 """
297379 Handler for GET /2025-12-01/durable-executions/{DurableExecutionArn}
0 commit comments