appsync-resolvers icon indicating copy to clipboard operation
appsync-resolvers copied to clipboard

How do we handle errors?

Open jonsaw opened this issue 6 years ago • 12 comments

How do we properly pass errors? Right now, all errors are returning as "Lambda:Unhandled".

Any way to customize errorType, errorInfo?

{
  "data": null,
  "errors": [
    {
      "path": [
        "updateProject"
      ],
      "data": null,
      "errorType": "Lambda:Unhandled",
      "errorInfo": null,
      "locations": [
        {
          "line": 15,
          "column": 3,
          "sourceName": null
        }
      ],
      "message": "Name required"
    }
  ]
}

Thanks!

jonsaw avatar Jul 20 '18 11:07 jonsaw

Good question 👍

Do you know if AppSync supports different error types? Is there any documentation available?

It might be even possible, that the error occurred before the Lambda function was involved at all. Seems like you missed a required attribute Name in your updateProject mutation? Required fields are validated/checked using the GraphQL schema, the request might be rejected before invoking the Lambda function …

sbstjn avatar Jul 21 '18 20:07 sbstjn

Got it to work based on the example found on AWS.

First, by having a custom error:

type ErrorHandler struct {
	Type    string `json:"error_type"`
	Message string `json:"error_message"`
}

And then returning an interface{} in the handler:

// CreateProject handler
func CreateProject(project models.Project) (interface{}, error) {
	now := time.Now()
	project.CreatedAt = &now
	project.UpdatedAt = &now

	err := project.Validate()
	if err != nil {
		handledError := &ErrorHandler{
			Type:    "VALIDATION_ERROR",
			Message: err.Error(),
		}
		return handledError, nil
	}

	err = project.Upsert()
	if err != nil {
		// Unhandled error
		return nil, err
	}

	return &project, nil
}

With a condition in the resolver's mapping-template:

#if( $context.result && $context.result.error_message )
    $utils.error($context.result.error_message, $context.result.error_type, $context.result)
#else
    $util.toJson($context.result)
#end

Not sure if this is the best way because we lose type checking in the handler.

jonsaw avatar Jul 22 '18 09:07 jonsaw

Another option would be to include ErrorType and ErrorMessage in the model:

// Project model structure
type Project struct {
	ProjectID        string     `json:"project_id"`
	Featured         bool       `json:"featured,omitempty"`
	ImageSrc050      string     `json:"image_src_050,omitempty"`
	ImageSrc100      string     `json:"image_src_100,omitempty"`
	Location         string     `json:"location,omitempty"`
	ShortName        string     `json:"short_name,omitempty"`
	ShortDescription string     `json:"short_description,omitempty"`
	Name             string     `json:"name"`
	Description      string     `json:"description"`
	CreatedBy        string     `json:"created_by"`
	CreatedAt        *time.Time `json:"created_at"`
	UpdatedAt        *time.Time `json:"updated_at"`

	ErrorType    utils.ErrorType `json:"error_type,omitempty"`
	ErrorMessage string          `json:"error_message,omitempty"`
}

And return *models.Project in the handler as per usual without losing the type.

We just need to add the condition in the resolver's mapping-template.

jonsaw avatar Jul 22 '18 10:07 jonsaw

Updating the data model with an optional error is a common practice, but somehow feels wrong. I have seen this a few times …

I'd rather update to general error handling and check if the returned error has a "Type" and "Message", and format the error response as needed.

Let's stick to best practices in Go for the resolvers, and use the package/lib for mapping to AWS magic 😂

sbstjn avatar Jul 22 '18 16:07 sbstjn

Based on your feedback, something like this?

Handler:

package handlers

// imports removed

func CreateProject(project models.Project) (interface{}, error) {
	now := time.Now()
	project.CreatedAt = &now
	project.UpdatedAt = &now

	err := project.Validate()
	if err != nil {
		return errortypes.ErrorHandler(err)
	}

	err = project.Upsert()
	if err != nil {
		return errortypes.ErrorHandler(err)
	}

	return &project, nil
}

Resolver:

#if( $context.result && $context.result.error_message )
    $util.error($context.result.error_message, $context.result.error_type, $context.result.error_data)
#else
    $util.toJson($context.result)
#end

Example model:

package models

// imports removed

type Project struct {
	Name string `json:"name"`
}

func (project *Project) Validate() error {
	if project.Name == "" {
		return errortypes.New(errortypes.BadRequest, "Name required", project)
	}
	return nil
}

// other methods...

Error utility:

package errortypes

// imports removed

type ErrorType int

const (
	BadRequest ErrorType = iota
)

func (e ErrorType) String() string {
	return errorsID[e]
}

var errorsID = map[ErrorType]string{
	BadRequest: "BAD_REQUEST",
}

var errorsName = map[string]ErrorType{
	"BAD_REQUEST": BadRequest,
}

func (e *ErrorType) MarshalJSON() ([]byte, error) {
	buffer := bytes.NewBufferString(`"`)
	buffer.WriteString(errorsID[*e])
	buffer.WriteString(`"`)
	return buffer.Bytes(), nil
}

func (e *ErrorType) UnmarshalJSON(b []byte) error {
	var s string
	err := json.Unmarshal(b, &s)
	if err != nil {
		return err
	}
	*e = errorsName[s]
	return nil
}

type GraphQLError struct {
	Type    ErrorType   `json:"error_type"`
	Message string      `json:"error_message"`
	Data    interface{} `json:"error_data"`
}

func (e *GraphQLError) Error() string {
	return e.Message
}

func New(t ErrorType, m string, d interface{}) *GraphQLError {
	return &GraphQLError{
		Type:    t,
		Message: m,
		Data:    d,
	}
}

func ErrorHandler(err error) (interface{}, error) {
	if errData, ok := err.(*GraphQLError); ok {
		return errData, nil
	}
	return nil, err
}

jonsaw avatar Jul 23 '18 03:07 jonsaw

How about introducing the concept of middleware? Similar to how it's done with Goji or Gorilla.

package main

import (
	// removed
)

var (
	r = resolvers.New()
)

func errorMiddleware(h resolvers.Handler) resolvers.Handler {
	fn := func(in interface{}, err error) {
		if errData, ok := err.(*GraphQLError); ok {
			// hijack return if error is present
			h.Serve(errData, nil)
			return
		}

		// continue as per usual...
		h.Serve(in, err)
	}
	return resolvers.HandlerFunc(fn)
}

func init() {
	r.Use(errorMiddleware)
	// or chain other middleware ...
	// r.Use(loggerMiddleware)

	r.Add("create.project", handlers.CreateProject)
}

func main() {
	lambda.Start(r.Handle)
}

jonsaw avatar Jul 25 '18 01:07 jonsaw

Dabbled at the idea of having a middleware. This would allow users to further extend this awesome package. Still prelim, but you can find some example applications in this fork.

jonsaw avatar Jul 27 '18 05:07 jonsaw

I always see "errorInfo" to be null in the error response. Has anyone figured a way for custom error?

tanduong avatar Nov 27 '18 05:11 tanduong

I found the answer: https://stackoverflow.com/a/53495843/2724342

tanduong avatar Nov 27 '18 09:11 tanduong

the above link doesn't help in having the actual custom error message set at the AWS lambda java handler function, instead I get "message": "java.lang.RuntimeException", example response :{ "data": { "smalltournaments": null }, "errors": [ { "path": [ "smalltournaments" ], "data": null, "errorType": "Lambda:Unhandled", "errorInfo": null, "locations": [ { "line": 30, "column": 6, "sourceName": null } ], "message": "java.lang.RuntimeException" } ] } handler response json : {"cause":{"stackTrace":[....],"message":"Error Message for A101", "localizedMessage":"Error Message for A101"}

alkleela avatar Jan 27 '20 15:01 alkleela

Could you please confirm if the documented solution works out for you?

https://docs.aws.amazon.com/appsync/latest/devguide/resolver-mapping-template-reference-lambda.html#lambda-mapping-template-bypass-errors

KoldBrewEd avatar Apr 15 '22 20:04 KoldBrewEd

hi I ran into the same issue as the one described above. I'm using a js lambda resolver that simply throws an exception.

Lambda output:

{
  "errorType": "SampleError",
  "errorMessage": "an error message",
  "trace": [
    "SampleError: an error message",
    "    at LambdaHandler.handleEvent [as cb] (/var/task/dist/app/sample/handlers/get.js:10:11)",
    "    at Runtime.handler (/var/task/dist/app/handlers/types.js:18:25)",
    "    at Runtime.handleOnceNonStreaming (file:///var/runtime/index.mjs:1085:29)"
  ]
}

Response mapping template:

#if($ctx.error)
     $util.error($ctx.error.message, $ctx.error.type, $ctx, $ctx)
#end
$util.toJson($ctx.result)

AppSync output:

{
  ...
  "errors": [
    {
      "path": [
        "getSample"
      ],
      "data": {
        "id": null
      },
      "errorType": "Lambda:Unhandled",
      "message": "an error message"
   ...
}

Request mapping template (tried with both 2018-05-29 and 2017-02-28 template versions, same result)

{
  "version" : "2018-05-29",
  "operation": "Invoke",
  "payload": $util.toJson($context)
}

Now the weird thing is that if I remove the response mapping template, I get the correct error name:

{
   ...
  "errors": [
     ...
      "data": null,
      "errorType": "SampleError",
      "errorInfo": null,
      "locations": [
        {
          "line": 2,
          "column": 3,
          "sourceName": null
        }
      ],
      "message": "an error message"
    }
  ]
}

am I missing something?

sescotti avatar Mar 19 '23 13:03 sescotti