graphql-go / graphql

An implementation of GraphQL for Go / Golang
MIT License
9.82k stars 836 forks source link

When using custom scalar types, it's crucial to provide error feedback to users when issues arise with their submitted data. However, triggering exceptions within the ParseValue and ParseLiteral methods can lead to program crashes when using the graphql.Do method. This prevents the necessary error messages from being delivered to users. #679

Open vyks520 opened 10 months ago

vyks520 commented 10 months ago

When using custom scalar types, it's crucial to provide error feedback to users when issues arise with their submitted data. However, triggering exceptions within the ParseValue and ParseLiteral methods can lead to program crashes when using the graphql.Do method. This prevents the necessary error messages from being delivered to users.

The proposed improvement is to handle exceptions by checking the error object's type when they occur. If the error is created using gqlerrors.Error{}, it signifies that the error object's Message field contains information intended for user feedback, allowing us to capture and return that Message to users. In cases where the error is not created using gqlerrors.Error{}, it can be rethrown as an exception. This approach enhances error handling and ensures that valuable information is communicated to users while avoiding the potential exposure of sensitive information by preventing all exceptions from being exposed to the frontend.

package main

import (
    "encoding/json"
    "fmt"
    "github.com/graphql-go/graphql"
    "github.com/graphql-go/graphql/gqlerrors"
    "github.com/graphql-go/graphql/language/ast"
    "github.com/graphql-go/graphql/language/kinds"
    "log"
    "strconv"
)

func PtrSliceToSlice[T any](s []*T) []T {
    if s == nil {
        return nil
    }
    ret := make([]T, 0, len(s))
    for _, v := range s {
        ret = append(ret, *v)
    }
    return ret
}

func parseObject(valueAST interface{}) interface{} {
    var value = make(map[string]interface{})
    var fieldList []ast.ObjectField
    switch obj := valueAST.(type) {
    case []*ast.ObjectField:
        fieldList = PtrSliceToSlice(obj)
    case []ast.ObjectField:
        fieldList = obj
    default:
        err := gqlerrors.NewError(
            fmt.Sprintf("JSON cannot represent value: %v", valueAST),
            nil, "", nil, nil, nil)
        panic(err)
    }
    for _, field := range fieldList {
        value[field.Name.Value] = parseLiteral(field.Value)
    }
    return value
}

func parseList(valueAST interface{}) interface{} {
    var valueList []ast.Value
    switch vs := valueAST.(type) {
    case []ast.Value:
        valueList = vs
    case []*ast.Value:
        valueList = PtrSliceToSlice(vs)
    default:
        err := gqlerrors.NewError(
            fmt.Sprintf("JSON cannot represent value: %v", valueAST),
            nil, "", nil, nil, nil)
        panic(err)
    }
    value := make([]interface{}, len(valueList))
    for i, item := range valueList {
        value[i] = parseLiteral(item)
    }
    return value
}

func parseLiteral(valueAST ast.Value) interface{} {
    switch valueAST.GetKind() {
    case kinds.StringValue, kinds.BooleanValue:
        return valueAST.GetValue()
    case kinds.IntValue:
        intValue := (valueAST.GetValue()).(string)
        v, _ := strconv.ParseFloat(intValue, 64)
        return v
    case kinds.FloatValue:
        floatValue := (valueAST.GetValue()).(string)
        v, _ := strconv.ParseFloat(floatValue, 64)
        return v
    case kinds.ObjectValue:
        return parseObject(valueAST.GetValue())
    case kinds.ListValue:
        return parseList(valueAST.GetValue())
    case kinds.NonNull:
        return nil
    case kinds.Variable:
        var name ast.Name
        switch v := valueAST.GetValue().(type) {
        case *ast.Name:
            name = *v
        case ast.Name:
            name = v
        }

        err := gqlerrors.NewError(
            fmt.Sprintf("JSON does not support the use of variable: $%s", name.Value),
            nil, "", nil, nil, nil)
        panic(err)
    }
    err := gqlerrors.NewError(
        fmt.Sprintf("JSON cannot represent value: %v", valueAST),
        nil, "", nil, nil, nil)
    panic(err)
}

var JSONScalarType = graphql.NewScalar(graphql.ScalarConfig{
    Name:        "JSON",
    Description: "The `JSON` scalar",
    Serialize: func(value interface{}) interface{} {
        err := gqlerrors.NewError(
            "This method is successfully caught by graphql.Do, and any errors that occur will be added to Result.Errors.",
            nil, "", nil, nil, nil)
        panic(err)
        return value
    },
    ParseValue: func(value interface{}) interface{} {
        /*err := gqlerrors.NewError(
            "graphql.Do does not catch exceptions, and the program crashes directly when an error occurs.",
            nil, "", nil, nil, nil)
        panic(err)*/
        return value
    },
    // ParseLiteral does not catch exceptions, and the program crashes directly when an error occurs.
    ParseLiteral: parseLiteral,
})

func main() {
    schema, err := graphql.NewSchema(graphql.SchemaConfig{
        Query: graphql.NewObject(graphql.ObjectConfig{
            Name: "Query",
            Fields: graphql.Fields{
                "getData": &graphql.Field{
                    Type: JSONScalarType,
                    Args: graphql.FieldConfigArgument{
                        "input": &graphql.ArgumentConfig{
                            Type: JSONScalarType,
                        },
                    },
                    Resolve: func(p graphql.ResolveParams) (interface{}, error) {
                        return map[string]interface{}{
                            "test": "testValue",
                        }, nil
                    },
                },
            },
        }),
    })
    if err != nil {
        log.Fatal(err)
    }
    query := `
        query ($input: JSON) {
            getData(input: {test: $input})
        }
    `
    result := graphql.Do(graphql.Params{
        Schema:        schema,
        RequestString: query,
        VariableValues: map[string]interface{}{
            "input": 89897886,
        },
    })
    b, err := json.Marshal(result)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(string(b))
}