Open jonsaw opened 6 years ago
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 …
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.
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.
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 😂
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
}
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)
}
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.
I always see "errorInfo" to be null in the error response. Has anyone figured a way for custom error?
I found the answer: https://stackoverflow.com/a/53495843/2724342
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"}
Could you please confirm if the documented solution works out for you?
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?
How do we properly pass errors? Right now, all errors are returning as
"Lambda:Unhandled"
.Any way to customize
errorType
,errorInfo
?Thanks!