@@ -48,6 +48,16 @@ type CustomInteropServer struct {
4848 // LocalStack via SendInitErrorResponse, so the crash-path fallback (SendInitError) does
4949 // not send a duplicate error status for the same failed initialization.
5050 initErrorForwarded atomic.Bool
51+ // initErrorType holds rapidcore's scrubbed fatal error type (e.g. Runtime.Unknown) when init
52+ // failed, used to render the INIT_REPORT(phase=invoke) and REPORT Status/Error Type lines for
53+ // the on-demand folded-into-invoke path. Stores a string; empty/unset means init did not fail.
54+ initErrorType atomic.Value
55+ // onDemand is true for on-demand functions, where AWS folds a failed cold-start init into
56+ // the first invocation (suppressed init). For these we do NOT report init failures via
57+ // /status/error; instead we signal ready and let the first invoke surface the error with
58+ // the full INIT_REPORT/START/END/REPORT envelope. Provisioned concurrency, SnapStart, and
59+ // Managed Instances keep the provisioning-time /status/error model.
60+ onDemand bool
5161}
5262
5363type LocalStackAdapter struct {
@@ -118,6 +128,7 @@ func NewCustomInteropServer(lsOpts *LsOpts, adapter *LocalStackAdapter, delegate
118128 upstreamEndpoint : lsOpts .RuntimeEndpoint ,
119129 localStackAdapter : adapter ,
120130 logCollector : logCollector ,
131+ onDemand : GetenvWithDefault ("AWS_LAMBDA_INITIALIZATION_TYPE" , "on-demand" ) == "on-demand" ,
121132 }
122133
123134 // TODO: extract this
@@ -140,13 +151,24 @@ func NewCustomInteropServer(lsOpts *LsOpts, adapter *LocalStackAdapter, delegate
140151 // The synthetic START line is emitted via LocalStackEventsAPI.SendInvokeStart so it
141152 // lands after any inline (suppressed) init, matching AWS — see events.go.
142153
154+ initErrType , _ := server .initErrorType .Load ().(string )
155+
143156 initDuration := ""
144- if ! server .warmStart && ! server .initTimedOut .Load () {
157+ if ! server .warmStart && ! server .initTimedOut .Load () && initErrType == "" {
145158 initTimeMS := float64 (time .Since (server .initStart ).Nanoseconds ()) / float64 (time .Millisecond )
146159 initDuration = fmt .Sprintf ("Init Duration: %.2f ms\t " , initTimeMS )
147160 }
148161 server .warmStart = true
149162
163+ // On-demand init failure folded into this invocation (AWS suppressed init): emit
164+ // the INIT_REPORT(phase=invoke) line before START (emitted during Invoke below).
165+ if initErrType != "" {
166+ initTimeMS := float64 (time .Since (server .initStart ).Nanoseconds ()) / float64 (time .Millisecond )
167+ _ , _ = fmt .Fprintf (logCollector ,
168+ "INIT_REPORT Init Duration: %.2f ms\t Phase: invoke\t Status: error\t Error Type: %s\n " ,
169+ initTimeMS , initErrType )
170+ }
171+
150172 invokeStart := time .Now ()
151173 err = server .Invoke (invokeResp , & interop.Invoke {
152174 ID : invokeR .InvokeId ,
@@ -197,6 +219,12 @@ func NewCustomInteropServer(lsOpts *LsOpts, adapter *LocalStackAdapter, delegate
197219 log .Fatalln (err )
198220 }
199221 }
222+ // On-demand init failure folded into this invocation: the REPORT carries the
223+ // failure status and rapidcore's scrubbed fatal error type (e.g. Runtime.Unknown).
224+ if initErrType != "" {
225+ isErr = true
226+ status = "Status: error\t Error Type: " + initErrType
227+ }
200228 // optional sleep. can be used for debugging purposes
201229 if lsOpts .PostInvokeWaitMS != "" {
202230 waitMS , err := strconv .Atoi (lsOpts .PostInvokeWaitMS )
@@ -249,6 +277,19 @@ func (c *CustomInteropServer) SendInitErrorResponse(resp *interop.ErrorInvokeRes
249277 // Mark synchronously, before sending: this runs in the init flow before
250278 // AwaitInitializedWithDetails unblocks in main.go, so the fallback observes the flag.
251279 c .initErrorForwarded .Store (true )
280+ // Record rapidcore's scrubbed fatal error type so the folded-into-invoke path can render the
281+ // INIT_REPORT(phase=invoke) and REPORT Status/Error Type lines (on-demand).
282+ c .initErrorType .Store (string (resp .FunctionError .Type ))
283+
284+ // Always cache the structured error in the delegate so the first invoke can surface it.
285+ defer c .delegate .SendInitErrorResponse (resp )
286+
287+ // On-demand folds the failed init into the first invocation, which carries the error and
288+ // logs; reporting it here via /status/error too would race the invoke and fail the env
289+ // startup before the invoke runs. PC/SnapStart/MI report at provisioning time below.
290+ if c .onDemand {
291+ return nil
292+ }
252293
253294 // Forward the runtime's structured payload as-is and only inject the requestId. Decoding
254295 // into a map rather than a typed struct preserves fields exactly as the runtime emitted
@@ -261,7 +302,7 @@ func (c *CustomInteropServer) SendInitErrorResponse(resp *interop.ErrorInvokeRes
261302 log .WithError (err ).WithField ("runtime-id" , c .localStackAdapter .RuntimeId ).
262303 Error ("Failed to send init error to LocalStack" )
263304 }
264- return c . delegate . SendInitErrorResponse ( resp )
305+ return nil
265306 }
266307
267308 // No invocation is active during the init phase, so this is typically blank; AWS still
@@ -274,14 +315,11 @@ func (c *CustomInteropServer) SendInitErrorResponse(resp *interop.ErrorInvokeRes
274315 body = resp .Payload
275316 }
276317
277- go func () {
278- if err := c .localStackAdapter .SendStatus (Error , body ); err != nil {
279- log .WithError (err ).WithField ("runtime-id" , c .localStackAdapter .RuntimeId ).
280- Error ("Failed to send init error to LocalStack" )
281- }
282- }()
283-
284- return c .delegate .SendInitErrorResponse (resp )
318+ if err := c .localStackAdapter .SendStatus (Error , body ); err != nil {
319+ log .WithError (err ).WithField ("runtime-id" , c .localStackAdapter .RuntimeId ).
320+ Error ("Failed to send init error to LocalStack" )
321+ }
322+ return nil
285323}
286324
287325// SendInitError reports a structured init failure to LocalStack when the runtime failed to
0 commit comments