使用gqlgen编写中间件,仅获取指定operationName的GraphQL查询
问题
我有一个私有GraphQL API,想要编写一个小型公共GraphQL API来调用其中部分查询/变更操作。在公共解析器向私有API发起请求时,需要获取指定operationName对应的查询字符串(包含字段、参数和变量),方便在github.com/machinebox/graphql这类客户端中使用。
目前通过HTTP中间件获取请求体时,会拿到Playground里的所有查询和变更内容,比如:
query listTodos { todos { id text } } mutation createTodo { createTodo(input:{text: "new todo", userId: "123"}) { id text } }
我需要实现当operationName为listTodos时,仅提取对应的查询内容:
query listTodos { todos { id text } }
初始化示例代码如下:
// server.go package main import ( "bytes" "encoding/json" "io" "log" "net/http" "os" "squirrel/lab/stack-gql/graph" "squirrel/lab/stack-gql/graph/generated" "github.com/99designs/gqlgen/graphql/handler" "github.com/99designs/gqlgen/graphql/playground" "github.com/go-chi/chi" ) const defaultPort = "8081" func main() { port := os.Getenv("PORT") if port == "" { port = defaultPort } router := chi.NewRouter() router.Use(middlewareQuery()) srv := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: &graph.Resolver{}})) router.Handle("/", playground.Handler("GraphQL playground", "/query")) router.Handle("/query", srv) log.Printf("connect to http://localhost:%s/ for GraphQL playground", port) log.Fatal(http.ListenAndServe(":"+port, router)) } func middlewareQuery() func(http.Handler) http.Handler { type bodyGraphQL struct { OperationName string `json:"operationName"` Variables struct{} `json:"variables"` Query string `json:"query"` } return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { buf, err := io.ReadAll(r.Body) if err != nil { next.ServeHTTP(w, r) return } rdr := io.NopCloser(bytes.NewBuffer(buf)) r.Body = io.NopCloser(bytes.NewBuffer(buf)) var body bodyGraphQL if err := json.Unmarshal(func(reader io.Reader) []byte { buf := new(bytes.Buffer) buf.ReadFrom(reader) return buf.Bytes() }(rdr), &body); err != nil { next.ServeHTTP(w, r) return } log.Println(body.Query) // <--- HERE WE WANT TO GET THE QUERY, BUT ONLY THE QUERY NOT ALL PLAYGROUND next.ServeHTTP(w, r) }) } }
解决方案
要精准提取指定operationName对应的查询内容,不能依赖简单的字符串截取,需要通过GraphQL解析库处理查询的抽象语法树(AST),定位目标操作后再重新生成标准的GraphQL字符串。
步骤1:引入GraphQL解析依赖
使用github.com/graphql-go/graphql库来解析和生成GraphQL文档,先安装依赖:
go get github.com/graphql-go/graphql
步骤2:修改中间件逻辑
在中间件中完成以下操作:解析查询字符串为AST、匹配目标操作、重新序列化目标操作为字符串。修改后的middlewareQuery函数如下:
func middlewareQuery() func(http.Handler) http.Handler { type bodyGraphQL struct { OperationName string `json:"operationName"` Variables map[string]interface{} `json:"variables"` Query string `json:"query"` } return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { buf, err := io.ReadAll(r.Body) if err != nil { next.ServeHTTP(w, r) return } // 重置请求体,保证后续流程能正常读取 r.Body = io.NopCloser(bytes.NewBuffer(buf)) var body bodyGraphQL if err := json.Unmarshal(buf, &body); err != nil { next.ServeHTTP(w, r) return } // 未指定operationName时直接返回原查询 if body.OperationName == "" { log.Println(body.Query) next.ServeHTTP(w, r) return } // 解析查询字符串为AST doc, err := graphql.Parse(body.Query) if err != nil { log.Printf("解析查询失败: %v", err) next.ServeHTTP(w, r) return } // 遍历AST找到匹配的操作 var targetOp *ast.OperationDefinition for _, def := range doc.Definitions { op, ok := def.(*ast.OperationDefinition) if !ok { continue } if op.Name != nil && op.Name.Value == body.OperationName { targetOp = op break } } if targetOp == nil { log.Printf("未找到操作: %s", body.OperationName) next.ServeHTTP(w, r) return } // 将目标操作重新生成为GraphQL字符串 var opBuf bytes.Buffer printer := ast.NewPrinter() if err := printer.Print(&opBuf, targetOp); err != nil { log.Printf("生成操作字符串失败: %v", err) next.ServeHTTP(w, r) return } extractedQuery := opBuf.String() log.Println(extractedQuery) // 这里就是提取后的目标查询内容 next.ServeHTTP(w, r) }) } }
关键说明
- 利用
graphql.Parse将查询字符串转为AST,避免手动字符串处理导致的格式错误。 - 遍历AST中的定义节点,精准筛选出与
operationName匹配的操作类型节点。 - 使用
ast.NewPrinter将目标操作节点重新打印为标准格式的GraphQL字符串,保证输出内容的正确性。 - 简化请求体重置逻辑,直接复用读取到的
buf生成新的可读流。
内容的提问来源于stack exchange,提问作者eclaude
相关产品推荐
相关产品推荐

