You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.11 16:50:30