Select Git revision
OrderCreateWebhook.php
lambda.go 9.55 KiB
package api
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"math/rand"
"net/http"
"reflect"
"time"
"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambdacontext"
"gitlab.com/uafrica/go-utils/errors"
"gitlab.com/uafrica/go-utils/logger"
)
func (api Api) NewContext(baseCtx context.Context, requestID string, request events.APIGatewayProxyRequest) (Context, error) {
serviceContext, err := api.Service.NewContext(baseCtx, requestID, nil)
if err != nil {
return nil, err
}
return &apiContext{
Context: serviceContext,
request: request,
}, nil
}
//this is native handler for lambda passed into lambda.Start()
//to run locally, this is called from app.ServeHTTP()
func (api Api) Handler(baseCtx context.Context, apiGatewayProxyReq events.APIGatewayProxyRequest) (res events.APIGatewayProxyResponse, err error) {
res = events.APIGatewayProxyResponse{
StatusCode: http.StatusInternalServerError,
Body: "undefined response",
Headers: map[string]string{},
}
// Replace the proxy resource with the path, has some edge cases but works for our current API implementation
// Edge case being that if have path params then specify those routes explicitly
if apiGatewayProxyReq.Resource == "/{proxy+}" {
apiGatewayProxyReq.Resource = apiGatewayProxyReq.Path
}
//get request-id from HTTP headers (used when making internal service calls)
//if not defined in header, get the AWS request id from the AWS context
requestID, ok := apiGatewayProxyReq.Headers[api.requestIDHeaderKey]
if !ok || requestID == "" {
if lambdaContext, ok := lambdacontext.FromContext(baseCtx); ok && lambdaContext != nil {
requestID = lambdaContext.AwsRequestID
}
}
//service context invoke the starters and could fail, e.g. if cannot connect to db
ctx, err := api.NewContext(baseCtx, requestID, apiGatewayProxyReq)
if err != nil {
return res, err
}
//report handler crashes
if api.crashReporter != nil {
defer api.crashReporter.Catch(ctx)
}
defer func() {
//set CORS headers on every response
if api.cors != nil {
for n, v := range api.cors.CORS() {
res.Headers[n] = v
}
}
}()
defer func() {
ctx.LogAPIRequestAndResponse(res, err)
if err != nil {
ctx.Errorf("failed: %+v", err)
//try to retrieve HTTP code from error
if withCause, ok := err.(errors.ErrorWithCause); ok && withCause.Code() != 0 {
res.StatusCode = withCause.Code()
err = withCause.Cause() //drop the http layers + up
} else {
//no HTTP code indicate in err,
//see if there are SQL errors that could indicate code
if code, ok := StatusCodeFromSQLError(err); ok {
res.StatusCode = code
}
}
errorMessage := fmt.Sprintf("%c", err)
jsonError, _ := json.Marshal(map[string]interface{}{"message": errorMessage})
res.Headers["Content-Type"] = "application/json"
res.Body = string(jsonError)
err = nil //never pass error back to lambda or http server
}
if api.requestIDHeaderKey != "" {
res.Headers[api.requestIDHeaderKey] = ctx.RequestID()
}
if err := api.Service.WriteValues(ctx.StartTime(), time.Now(), ctx.RequestID(), map[string]interface{}{
"direction": "incoming",
"type": "api",
"request_id": ctx.RequestID(),
"request": ctx.Request(),
"response": res},
); err != nil {
ctx.Errorf("failed to audit: %+v", err)
}
}()
//Early return OPTIONS call
if apiGatewayProxyReq.HTTPMethod == "OPTIONS" {
res.StatusCode = http.StatusNoContent
err = nil
return
}
rand.Seed(time.Now().Unix())
for checkName, check := range api.checks {
var checkData interface{}
checkData, err = check.Check(ctx)
if err != nil {
err = errors.Wrapf(err, "%s", checkName)
return
}
if err = ctx.Set(checkName, checkData); err != nil {
err = errors.Wrapf(err, "failed to set check(%s) data=(%T)%+v", checkName, checkData, checkData)
return
}
}
//LEGACY: delete this as soon as all handlers accepts context
//this does not support concurrent execution!
CurrentRequestID = &apiGatewayProxyReq.RequestContext.RequestID
ctx.Debugf("HTTP %s %s ...\n", apiGatewayProxyReq.HTTPMethod, apiGatewayProxyReq.Resource)
ctx.WithFields(map[string]interface{}{
"http_method": ctx.Request().HTTPMethod,
"path": ctx.Request().Path,
"api_gateway_request_id": ctx.Request().RequestContext.RequestID,
"user_cognito_auth_provider": ctx.Request().RequestContext.Identity.CognitoAuthenticationProvider,
"user_arn": ctx.Request().RequestContext.Identity.UserArn,
}).Infof("Start API Handler")
//TODO:
// // Get claims and check the status of the user
// ctx.Claims, err = api.RetrieveClaims(&apiGatewayProxyReq)
// if err != nil {
// return events.APIGatewayProxyResponse{
// StatusCode: http.StatusBadRequest,
// Body: fmt.Sprintf("%v\n", err),
// Headers: utils.CorsHeaders(),
// }, nil
// }
// if ctx.Claims.UserID != nil {
// userStatusResponse := checkUserStatus(ctx.Claims)
// if userStatusResponse != nil {
// return *userStatusResponse, nil
// }
// }
// permissionString := fmt.Sprintf("API_%s%s:%s", os.Getenv("MICRO_SERVICE_API_BASE_PATH"), apiGatewayProxyReq.Resource, apiGatewayProxyReq.HTTPMethod)
// if !permissions.HasPermission(ctx.Claims.Role, permissionString) {
// response, _ := apierr.ClientError(http.StatusUnauthorized, fmt.Sprintf("You do not have access to the requested resource: %s", permissionString))
// if ctx.Claims.Role == nil {
// ctx.Errorf("%d :: %s: %v", *ctx.Claims.RoleID, permissionString, fmt.Errorf("you have no role"))
// } else if ctx.Claims.RoleID == nil {
// ctx.Errorf("%s: you have no role ID", permissionString)
// }
// return response, nil
// }
//route on method and path
resourceHandler, err := api.router.Route(apiGatewayProxyReq.Resource, apiGatewayProxyReq.HTTPMethod)
if err != nil {
err = errors.Wrapf(err, "invalid route")
return
}
if legacyHandlerFunc, ok := resourceHandler.(func(req events.APIGatewayProxyRequest) (response events.APIGatewayProxyResponse, err error)); ok {
ctx.Debugf("Calling legacy handler...")
return legacyHandlerFunc(apiGatewayProxyReq)
}
handler, ok := resourceHandler.(handler)
if !ok {
//should not get here if validateAPIEndpoints() is properly checking!
err = errors.HTTP(http.StatusInternalServerError, errors.Errorf("invalid handler function %T", resourceHandler), "invalid routing")
}
//new type of handler function
//allocate, populate and validate params struct
paramsStruct, paramsErr := ctx.GetRequestParams(handler.RequestParamsType)
if paramsErr != nil {
err = errors.HTTP(http.StatusBadRequest, paramsErr, "invalid parameters")
return
}
//apply claims to params struct - TODO: Removed - see if cannot force to get claims from context always
// if err = ctx.Claims.FillOnObject(ctx.request, ¶msStruct); err != nil {
// err = errors.HTTP(http.StatusInternalServerError, err, "claims failed on parameters")
// return
// }
ctx.Debugf("Params: (%T) %+v", paramsStruct, paramsStruct)
args := []reflect.Value{
reflect.ValueOf(ctx),
reflect.ValueOf(paramsStruct),
}
var bodyStruct interface{}
if handler.RequestBodyType != nil {
//allocate, populate and validate request struct
bodyStruct, err = ctx.GetRequestBody(handler.RequestBodyType)
if err != nil {
err = errors.HTTP(http.StatusBadRequest, err, "invalid body")
return
}
//apply claims to request struct - TODO: Removed - see if cannot force to get claims from context always
// if err = ctx.Claims.FillOnObject(ctx.request, &bodyStruct); err != nil {
// err = errors.HTTP(http.StatusInternalServerError, err, "claims failed on body")
// return
// }
ctx.Debugf("Body: (%T) %+v", bodyStruct, bodyStruct)
args = append(args, reflect.ValueOf(bodyStruct))
}
//call handler in a func with defer to catch potential crash
ctx.Infof("Calling handle %s %s ...", apiGatewayProxyReq.HTTPMethod, apiGatewayProxyReq.Resource)
var results []reflect.Value
results, err = func() (results []reflect.Value, err error) {
defer func() {
if crashErr := recover(); crashErr != nil {
stack := logger.CallStack()
err = errors.Errorf("handler function crashed: %v, with stack: %+v", crashErr, stack)
return
}
}()
results = handler.FuncValue.Call(args)
return results, nil
}()
if err != nil {
err = errors.Wrapf(err, "handler failed")
return
}
//ctx.Debugf("handler -> results: %v", results)
//see if handler failed using last result of type error
lastResultValue := results[len(results)-1]
if !lastResultValue.IsNil() {
err = lastResultValue.Interface().(error)
if err != nil {
return
}
}
//handler succeeded, some handler does not have a response data (typically post/put/patch/delete)
err = nil
switch apiGatewayProxyReq.HTTPMethod {
case http.MethodDelete:
res.StatusCode = http.StatusNoContent
default:
res.StatusCode = http.StatusOK
}
if len(results) > 1 {
responseStruct := results[0].Interface()
ctx.Debugf("Response type: %T", responseStruct)
var bodyBytes []byte
bodyBytes, err = json.Marshal(responseStruct)
if err != nil {
err = errors.Wrapf(err, "failed to encode response content")
return
}
res.Headers["Content-Type"] = "application/json"
res.Body = string(bodyBytes)
} else {
//no content
delete(res.Headers, "Content-Type")
res.Body = ""
}
return
}
//look for SQL errors in the error stack
func StatusCodeFromSQLError(err error) (int, bool) {
if err == sql.ErrNoRows {
return http.StatusNotFound, true
}
if errWithCause, ok := err.(errors.ErrorWithCause); ok {
return StatusCodeFromSQLError(errWithCause.Cause())
}
//could not determine known SQL error
return 0, false
}