@@ -5,6 +5,7 @@ package main
55
66import (
77 "bytes"
8+ "encoding/base64"
89 "encoding/json"
910 "errors"
1011 "fmt"
@@ -104,6 +105,47 @@ func (l *LocalStackAdapter) SendResult(invokeId string, body []byte, isError boo
104105 return l .post (endpoint , body )
105106}
106107
108+ func decodeInvokeRequest (body []byte ) (* interop.Invoke , error ) {
109+ var request * lsapi.InvokeRequest
110+ if err := json .Unmarshal (body , & request ); err != nil {
111+ return nil , err
112+ }
113+ if request == nil {
114+ return nil , errors .New ("invocation request must be an object" )
115+ }
116+ clientContext , err := base64 .StdEncoding .DecodeString (request .ClientContext )
117+ if err != nil {
118+ return nil , err
119+ }
120+ return & interop.Invoke {
121+ ID : request .InvokeId ,
122+ InvokedFunctionArn : request .InvokedFunctionArn ,
123+ Payload : strings .NewReader (request .Payload ),
124+ NeedDebugLogs : true ,
125+ TraceID : request .TraceId ,
126+ ClientContext : string (clientContext ),
127+ }, nil
128+ }
129+
130+ // newInvokeHandler validates requests before acknowledging and dispatching them asynchronously
131+ func newInvokeHandler (dispatch func (* interop.Invoke )) http.HandlerFunc {
132+ return func (w http.ResponseWriter , r * http.Request ) {
133+ body , err := io .ReadAll (r .Body )
134+ if err != nil {
135+ http .Error (w , "Failed to read invocation request" , http .StatusInternalServerError )
136+ return
137+ }
138+ invoke , err := decodeInvokeRequest (body )
139+ if err != nil {
140+ http .Error (w , "Invalid invocation request" , http .StatusBadRequest )
141+ return
142+ }
143+ go dispatch (invoke )
144+ w .WriteHeader (http .StatusOK )
145+ _ , _ = w .Write ([]byte ("OK" ))
146+ }
147+ }
148+
107149func NewCustomInteropServer (lsOpts * LsOpts , delegate interop.Server , logCollector * LogCollector , eventsAPI * LocalStackEventsAPI ) (server * CustomInteropServer ) {
108150 server = & CustomInteropServer {
109151 delegate : delegate .(* rapidcore.Server ),
@@ -117,114 +159,80 @@ func NewCustomInteropServer(lsOpts *LsOpts, delegate interop.Server, logCollecto
117159 resetDone : make (chan struct {}, 1 ),
118160 }
119161
120- // TODO: extract this
121162 go func () {
122163 r := chi .NewRouter ()
123- r .Post ("/invoke" , func (w http.ResponseWriter , r * http.Request ) {
124- invokeR := lsapi.InvokeRequest {}
125- bytess , err := io .ReadAll (r .Body )
164+ r .Post ("/invoke" , newInvokeHandler (func (invoke * interop.Invoke ) {
165+ invokeResp := & standalone.ResponseWriterProxy {}
166+ // The synthetic START and INIT_REPORT lines are emitted via LocalStackEventsAPI
167+ // from rapid's lifecycle events, so they land at the AWS-faithful points (e.g.
168+ // after an inline suppressed init's own logs) — see events.go.
169+
170+ // First invocation into a successfully initialized on-demand environment: REPORT
171+ // carries the Init phase duration as measured by rapid (take-once; absent on warm
172+ // starts, failed/timed-out inits, and non-on-demand environments).
173+ initDurationMS , hasInitDuration := server .eventsAPI .TakeColdStartInitDuration ()
174+
175+ invokeStart := time .Now ()
176+ err := server .Invoke (invokeResp , invoke )
177+ timeout := int (server .delegate .GetInvokeTimeout ().Seconds ())
178+ isErr := false
179+ status := ""
180+ errorType := ""
126181 if err != nil {
127- log .Error (err )
128- }
129-
130- go func () {
131- err = json .Unmarshal (bytess , & invokeR )
132- if err != nil {
133- log .Error (err )
134- }
135-
136- invokeResp := & standalone.ResponseWriterProxy {}
137- // The synthetic START and INIT_REPORT lines are emitted via LocalStackEventsAPI
138- // from rapid's lifecycle events, so they land at the AWS-faithful points (e.g.
139- // after an inline suppressed init's own logs) — see events.go.
140-
141- // First invocation into a successfully initialized on-demand environment: REPORT
142- // carries the Init phase duration as measured by rapid (take-once; absent on warm
143- // starts, failed/timed-out inits, and non-on-demand environments).
144- initDurationMS , hasInitDuration := server .eventsAPI .TakeColdStartInitDuration ()
145-
146- invokeStart := time .Now ()
147- err = server .Invoke (invokeResp , & interop.Invoke {
148- ID : invokeR .InvokeId ,
149- InvokedFunctionArn : invokeR .InvokedFunctionArn ,
150- Payload : strings .NewReader (invokeR .Payload ), // r.Body,
151- NeedDebugLogs : true ,
152- TraceID : invokeR .TraceId ,
153- // TODO: set correct segment ID from request
154- //LambdaSegmentID: "LambdaSegmentID", // r.Header.Get("X-Amzn-Segment-Id"),
155- //CognitoIdentityID: "",
156- //CognitoIdentityPoolID: "",
157- //DeadlineNs: "",
158- //ClientContext: "",
159- //ContentType: "",
160- //ReservationToken: "",
161- //VersionID: "",
162- //InvokeReceivedTime: 0,
163- //ResyncState: interop.Resync{},
164- })
165- timeout := int (server .delegate .GetInvokeTimeout ().Seconds ())
166- isErr := false
167- status := ""
168- errorType := ""
169- if err != nil {
170- switch {
171- case errors .Is (err , rapidcore .ErrInvokeTimeout ):
172- log .Debugf ("Got invoke timeout" )
173- isErr = true
174- status = "timeout"
175- errorResponse := lsapi.ErrorResponse {
176- ErrorType : "Sandbox.Timedout" ,
177- ErrorMessage : fmt .Sprintf (
178- "RequestId: %s Error: Task timed out after %d.00 seconds" ,
179- invokeR .InvokeId ,
180- timeout ,
181- ),
182- }
183- jsonErrorResponse , err := json .Marshal (errorResponse )
184- if err != nil {
185- log .Fatalln ("unable to marshall json timeout response" )
186- }
187- _ , err = invokeResp .Write (jsonErrorResponse )
188- if err != nil {
189- log .Fatalln ("unable to write to response" )
190- }
191- case errors .Is (err , rapidcore .ErrInvokeDoneFailed ):
192- // The error response body was already written by rapid and is sent below.
193- // When an init failure was folded into this invocation (AWS suppressed
194- // init), the REPORT additionally carries the failure status and the
195- // scrubbed fatal error type (e.g. Runtime.Unknown).
196- if errType := server .eventsAPI .InitErrorType (); errType != "" {
197- isErr = true
198- status = "error"
199- errorType = errType
200- }
201- default :
202- log .Fatalln (err )
182+ switch {
183+ case errors .Is (err , rapidcore .ErrInvokeTimeout ):
184+ log .Debugf ("Got invoke timeout" )
185+ isErr = true
186+ status = "timeout"
187+ errorResponse := lsapi.ErrorResponse {
188+ ErrorType : "Sandbox.Timedout" ,
189+ ErrorMessage : fmt .Sprintf (
190+ "RequestId: %s Error: Task timed out after %d.00 seconds" ,
191+ invoke .ID ,
192+ timeout ,
193+ ),
203194 }
204- }
205- // optional sleep. can be used for debugging purposes
206- if lsOpts .PostInvokeWaitMS != "" {
207- waitMS , err := strconv .Atoi (lsOpts .PostInvokeWaitMS )
195+ jsonErrorResponse , err := json .Marshal (errorResponse )
208196 if err != nil {
209- log .Fatalln (err )
197+ log .Fatalln ("unable to marshall json timeout response" )
210198 }
211- time .Sleep (time .Duration (waitMS ) * time .Millisecond )
212- }
213- timeoutDuration := time .Duration (timeout ) * time .Second
214- memorySize := GetEnvOrDie ("AWS_LAMBDA_FUNCTION_MEMORY_SIZE" )
215- PrintEndReports (invokeR .InvokeId , initDurationMS , hasInitDuration , status , errorType , memorySize , invokeStart , timeoutDuration , logCollector )
216-
217- if err2 := server .localStackAdapter .SendLogs (invokeR .InvokeId , logCollector .getLogs ()); err2 != nil {
218- log .Error ("failed to send logs to LocalStack: " , err2 )
199+ _ , err = invokeResp .Write (jsonErrorResponse )
200+ if err != nil {
201+ log .Fatalln ("unable to write to response" )
202+ }
203+ case errors .Is (err , rapidcore .ErrInvokeDoneFailed ):
204+ // The error response body was already written by rapid and is sent below.
205+ // When an init failure was folded into this invocation (AWS suppressed
206+ // init), the REPORT additionally carries the failure status and the
207+ // scrubbed fatal error type (e.g. Runtime.Unknown).
208+ if errType := server .eventsAPI .InitErrorType (); errType != "" {
209+ isErr = true
210+ status = "error"
211+ errorType = errType
212+ }
213+ default :
214+ log .Fatalln (err )
219215 }
220- if err2 := server .localStackAdapter .SendResult (invokeR .InvokeId , invokeResp .Body , isErr ); err2 != nil {
221- log .Error ("failed to send result to LocalStack: " , err2 )
216+ }
217+ // optional sleep. can be used for debugging purposes
218+ if lsOpts .PostInvokeWaitMS != "" {
219+ waitMS , err := strconv .Atoi (lsOpts .PostInvokeWaitMS )
220+ if err != nil {
221+ log .Fatalln (err )
222222 }
223- }()
223+ time .Sleep (time .Duration (waitMS ) * time .Millisecond )
224+ }
225+ timeoutDuration := time .Duration (timeout ) * time .Second
226+ memorySize := GetEnvOrDie ("AWS_LAMBDA_FUNCTION_MEMORY_SIZE" )
227+ PrintEndReports (invoke .ID , initDurationMS , hasInitDuration , status , errorType , memorySize , invokeStart , timeoutDuration , logCollector )
224228
225- w .WriteHeader (200 )
226- _ , _ = w .Write ([]byte ("OK" ))
227- })
229+ if err2 := server .localStackAdapter .SendLogs (invoke .ID , logCollector .getLogs ()); err2 != nil {
230+ log .Error ("failed to send logs to LocalStack: " , err2 )
231+ }
232+ if err2 := server .localStackAdapter .SendResult (invoke .ID , invokeResp .Body , isErr ); err2 != nil {
233+ log .Error ("failed to send result to LocalStack: " , err2 )
234+ }
235+ }))
228236 err := http .ListenAndServe (":" + server .port , r )
229237 if err != nil {
230238 log .Error (err )
0 commit comments