33import io
44import json
55import logging
6+ from concurrent .futures import ThreadPoolExecutor
67from datetime import datetime
78from urllib .parse import unquote
89
1112
1213from samcli .commands .local .cli_common .durable_context import DurableContext
1314from samcli .commands .local .lib .exceptions import TenantIdValidationError , UnsupportedInlineCodeError
15+ from samcli .lib .utils .invocation_type import EVENT
1416from samcli .lib .utils .name_utils import InvalidFunctionNameException , normalize_sam_function_identifier
1517from samcli .lib .utils .stream_writer import StreamWriter
1618from samcli .local .docker .exceptions import DockerContainerCreationFailedException
@@ -67,6 +69,7 @@ def __init__(self, lambda_runner, port, host, stderr=None, ssl_context=None):
6769 super ().__init__ (lambda_runner .is_debugging (), port = port , host = host , ssl_context = ssl_context )
6870 self .lambda_runner = lambda_runner
6971 self .stderr = stderr
72+ self .executor = ThreadPoolExecutor ()
7073
7174 def create (self ):
7275 """
@@ -240,32 +243,36 @@ def _invoke_request_handler(self, function_name):
240243 # Extract durable execution name from headers
241244 durable_execution_name = flask_request .headers .get ("X-Amz-Durable-Execution-Name" )
242245
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 )
246+ arguments = {
247+ "function_name" : function_name ,
248+ "request_data" : request_data ,
249+ "invocation_type" : invocation_type ,
250+ "durable_execution_name" : durable_execution_name ,
251+ "tenant_id" : tenant_id ,
252+ }
253+
254+ headers = {"Content-Type" : "application/json" }
255+
256+ if invocation_type == EVENT :
257+ # Validate function exists before submitting async task
258+ if validation_error := self ._validate_function_for_invocation (function_name ):
259+ return validation_error
260+
261+ self .executor .submit (self ._invoke_async_lambda , ** arguments )
262+ return self .service_response ("" , headers , 202 )
246263
247264 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- )
265+ invoke_headers , stdout_stream_string , stdout_stream_bytes = self ._invoke_lambda (** arguments )
260266 except (InvalidFunctionNameException , TenantIdValidationError ) as e :
261267 LOG .error ("Validation error: %s" , str (e ))
262268 return LambdaErrorResponses .validation_exception (str (e ))
263269 except UnsupportedInvocationType as e :
264- LOG .warning ("invocation-type: %s is not supported. RequestResponse is only supported." , invocation_type )
270+ LOG .warning (
271+ "invocation-type: %s is not supported. Event and RequestResponse are only supported." , invocation_type
272+ )
265273 return LambdaErrorResponses .not_implemented_locally (str (e ))
266274 except FunctionNotFound :
267- LOG .debug ("%s was not found to invoke." , normalized_function_name )
268- return LambdaErrorResponses .resource_not_found (normalized_function_name )
275+ return self ._handle_function_not_found (function_name )
269276 except UnsupportedInlineCodeError :
270277 return LambdaErrorResponses .not_implemented_locally (
271278 "Inline code is not supported for sam local commands. Please write your code in a separate file."
@@ -278,20 +285,87 @@ def _invoke_request_handler(self, function_name):
278285 )
279286
280287 # Prepare headers
281- headers = {"Content-Type" : "application/json" }
282288 if invoke_headers and isinstance (invoke_headers , dict ):
283289 headers .update (invoke_headers )
284290
285291 if is_lambda_user_error_response :
286292 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 )
292293
293294 return self .service_response (lambda_response , headers , 200 )
294295
296+ def _validate_function_for_invocation (self , function_name ):
297+ """
298+ Validates that a function exists and can be invoked.
299+
300+ Parameters
301+ ----------
302+ function_name : str
303+ Name or ARN of the function to validate
304+
305+ Returns
306+ -------
307+ Flask.Response or None
308+ Error response if validation fails, None if validation succeeds
309+ """
310+ try :
311+ self .lambda_runner .get_function (function_name )
312+ return None
313+ except FunctionNotFound :
314+ return self ._handle_function_not_found (function_name )
315+ except InvalidFunctionNameException as e :
316+ LOG .error ("Validation error: %s" , str (e ))
317+ return LambdaErrorResponses .validation_exception (str (e ))
318+
319+ def _handle_function_not_found (self , function_name ):
320+ """
321+ Handles FunctionNotFound exception by returning appropriate error response.
322+
323+ Parameters
324+ ----------
325+ function_name : str
326+ Name or ARN of the function that was not found
327+
328+ Returns
329+ -------
330+ Flask.Response
331+ Error response for function not found
332+ """
333+ normalized_function_name = normalize_sam_function_identifier (function_name )
334+ LOG .debug ("%s was not found to invoke." , normalized_function_name )
335+ return LambdaErrorResponses .resource_not_found (normalized_function_name )
336+
337+ def _invoke_async_lambda (self , function_name , ** kwargs ):
338+ """
339+ Wrapper for _invoke_lambda that runs in an async context (Event invocation type)
340+ """
341+ try :
342+ self ._invoke_lambda (function_name = function_name , ** kwargs )
343+ except Exception as e :
344+ LOG .error ("Async invocation failed for function %s: %s" , function_name , str (e ), exc_info = True )
345+
346+ def _invoke_lambda (self , function_name , request_data , invocation_type , durable_execution_name , tenant_id ):
347+ """
348+ Invokes a Lambda function and returns the result
349+ """
350+
351+ stdout_stream_string = io .StringIO ()
352+ stdout_stream_bytes = io .BytesIO ()
353+ stdout_stream_writer = StreamWriter (stdout_stream_string , stdout_stream_bytes , auto_flush = True )
354+
355+ normalized_function_name = normalize_sam_function_identifier (function_name )
356+
357+ invoke_headers = self .lambda_runner .invoke (
358+ normalized_function_name ,
359+ request_data ,
360+ invocation_type = invocation_type ,
361+ durable_execution_name = durable_execution_name ,
362+ tenant_id = tenant_id ,
363+ stdout = stdout_stream_writer ,
364+ stderr = self .stderr ,
365+ )
366+
367+ return invoke_headers , stdout_stream_string , stdout_stream_bytes
368+
295369 def _get_durable_execution_handler (self , durable_execution_arn ):
296370 """
297371 Handler for GET /2025-12-01/durable-executions/{DurableExecutionArn}
0 commit comments