Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions router-tests/modules/add_operation_name_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package module_test

import (
"encoding/json"
"io"
"net/http"
"sync"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

custom_add_operation_name "github.com/wundergraph/cosmo/router-tests/modules/custom-add-operation-name"
"github.com/wundergraph/cosmo/router-tests/testenv"
"github.com/wundergraph/cosmo/router/core"
"github.com/wundergraph/cosmo/router/pkg/config"
)

func TestAddOperationNameModule(t *testing.T) {
t.Parallel()

t.Run("adds the client operation name to the origin request body", func(t *testing.T) {
t.Parallel()

cfg := config.Config{
Graph: config.Graph{},
Modules: map[string]any{
"addOperationNameModule": custom_add_operation_name.AddOperationNameModule{},
},
}

var (
mu sync.Mutex
capturedBodies [][]byte
)

testenv.Run(t, &testenv.Config{
Subgraphs: testenv.SubgraphsConfig{
GlobalMiddleware: func(handler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
require.NoError(t, err)

mu.Lock()
capturedBodies = append(capturedBodies, body)
mu.Unlock()

handler.ServeHTTP(w, r)
})
},
Comment on lines +39 to +50

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

file='router-tests/modules/add_operation_name_test.go'

echo '--- file outline ---'
ast-grep outline "$file" --view expanded || true

echo '--- relevant lines ---'
nl -ba "$file" | sed -n '1,140p'

echo '--- search for body restoration or similar patterns in this test file ---'
rg -n "io\.NopCloser|bytes\.NewReader|ReadAll\(r\.Body\)|GlobalMiddleware|ServeHTTP\(w, r\)" "$file"

Repository: wundergraph/cosmo

Length of output: 340


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file='router-tests/modules/add_operation_name_test.go'

echo '--- relevant lines (1-160) ---'
sed -n '1,160p' "$file"

echo '--- search for response assertions in this test file ---'
rg -n "errors|data|status|require\.Equal|assert\." "$file"

Repository: wundergraph/cosmo

Length of output: 2748


Restore r.Body before calling the wrapped handler.

io.ReadAll(r.Body) drains the request, so the downstream subgraph handler receives an empty body and the test no longer exercises real GraphQL execution. Rewind it with io.NopCloser(bytes.NewReader(body)), and consider asserting the response payload too.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@router-tests/modules/add_operation_name_test.go` around lines 39 - 50, The
GlobalMiddleware wrapper in add_operation_name_test is draining r.Body with
io.ReadAll before passing the request to handler.ServeHTTP, so restore the body
afterward by resetting r.Body to a new io.NopCloser over the captured bytes
before calling the wrapped handler. Keep the existing capture logic for
capturedBodies, and use the middleware/test helper identifiers GlobalMiddleware
and handler.ServeHTTP to locate the fix; if useful, also add an assertion on the
response payload so the test still validates GraphQL execution.

},
RouterOptions: []core.Option{
core.WithModulesConfig(cfg.Modules),
core.WithCustomModules(&custom_add_operation_name.AddOperationNameModule{}),
},
ModifyEngineExecutionConfiguration: func(conf *config.EngineExecutionConfiguration) {
conf.EnableSubgraphFetchOperationName = true
},
}, func(t *testing.T, xEnv *testenv.Environment) {
res, err := xEnv.MakeGraphQLRequest(testenv.GraphQLRequest{
Query: `query MyQuery { employees { id } }`,
OperationName: json.RawMessage(`"MyQuery"`),
})
require.NoError(t, err)
require.Equal(t, 200, res.Response.StatusCode)

mu.Lock()
defer mu.Unlock()
require.Len(t, capturedBodies, 1)

var payload map[string]json.RawMessage
require.NoError(t, json.Unmarshal(capturedBodies[0], &payload))

// The operationName property must sit at the same level as the query property
require.Contains(t, payload, "query")
require.Contains(t, payload, "operationName")

var operationName string
require.NoError(t, json.Unmarshal(payload["operationName"], &operationName))
assert.Equal(t, "MyQuery__employees__0", operationName)
})
})
}
73 changes: 73 additions & 0 deletions router-tests/modules/custom-add-operation-name/module.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package module

import (
"bytes"
"io"
"net/http"

"github.com/wundergraph/astjson"
"github.com/wundergraph/cosmo/router/core"
"github.com/wundergraph/graphql-go-tools/v2/pkg/astparser"
"go.uber.org/zap"
)

const moduleID = "addOperationNameModule"

type AddOperationNameModule struct {
Logger *zap.Logger
}

func (m *AddOperationNameModule) Provision(ctx *core.ModuleContext) error {

// Assign the logger to the module for non-request related logging
m.Logger = ctx.Logger

return nil
}

func (m *AddOperationNameModule) OnOriginRequest(request *http.Request, ctx core.RequestContext) (*http.Request, *http.Response) {
if request.Body == nil {
return request, nil
}
body, err := io.ReadAll(request.Body)
_ = request.Body.Close()
if err != nil {
m.Logger.Error("addOperationNameModule: failed to read origin request body", zap.Error(err))
return request, nil
}

// Best-effort: on any parse failure, forward the original body unchanged.
if payload, err := astjson.ParseBytes(body); err == nil {
doc, report := astparser.ParseGraphqlDocumentBytes(payload.GetStringBytes("query"))
if !report.HasErrors() && len(doc.OperationDefinitions) == 1 {
if name := doc.Input.ByteSlice(doc.OperationDefinitions[0].Name); len(name) > 0 {
payload.Set(nil, "operationName", astjson.StringValueBytes(nil, name))
body = payload.MarshalTo(nil)
}
}
}
request.Body = io.NopCloser(bytes.NewReader(body))
request.ContentLength = int64(len(body))
request.GetBody = func() (io.ReadCloser, error) {
return io.NopCloser(bytes.NewReader(body)), nil
}
return request, nil
}

func (m *AddOperationNameModule) Module() core.ModuleInfo {
return core.ModuleInfo{
// This is the ID of your module, it must be unique
ID: moduleID,
// The priority of your module, lower numbers are executed first
Priority: 1,
New: func() core.Module {
return &AddOperationNameModule{}
},
}
}

// Interface guard
var (
_ core.EnginePreOriginHandler = (*AddOperationNameModule)(nil)
_ core.Provisioner = (*AddOperationNameModule)(nil)
)
78 changes: 78 additions & 0 deletions router/cmd/custom-add-operation-name/module.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package module

import (
"bytes"
"io"
"net/http"

"github.com/wundergraph/astjson"
"github.com/wundergraph/cosmo/router/core"
"github.com/wundergraph/graphql-go-tools/v2/pkg/astparser"
"go.uber.org/zap"
)

func init() {
// Register your module here
core.RegisterModule(&AddOperationNameModule{})
}

const moduleID = "addOperationNameModule"

type AddOperationNameModule struct {
Logger *zap.Logger
}

func (m *AddOperationNameModule) Provision(ctx *core.ModuleContext) error {

// Assign the logger to the module for non-request related logging
m.Logger = ctx.Logger

return nil
}

func (m *AddOperationNameModule) OnOriginRequest(request *http.Request, ctx core.RequestContext) (*http.Request, *http.Response) {
if request.Body == nil {
return request, nil
}
body, err := io.ReadAll(request.Body)
_ = request.Body.Close()
if err != nil {
m.Logger.Error("addOperationNameModule: failed to read origin request body", zap.Error(err))
return request, nil
}

// Best-effort: on any parse failure, forward the original body unchanged.
if payload, err := astjson.ParseBytes(body); err == nil {
doc, report := astparser.ParseGraphqlDocumentBytes(payload.GetStringBytes("query"))
if !report.HasErrors() && len(doc.OperationDefinitions) == 1 {
if name := doc.Input.ByteSlice(doc.OperationDefinitions[0].Name); len(name) > 0 {
payload.Set(nil, "operationName", astjson.StringValueBytes(nil, name))
body = payload.MarshalTo(nil)
}
}
}
request.Body = io.NopCloser(bytes.NewReader(body))
request.ContentLength = int64(len(body))
request.GetBody = func() (io.ReadCloser, error) {
return io.NopCloser(bytes.NewReader(body)), nil
}
return request, nil
}

func (m *AddOperationNameModule) Module() core.ModuleInfo {
return core.ModuleInfo{
// This is the ID of your module, it must be unique
ID: moduleID,
// The priority of your module, lower numbers are executed first
Priority: 1,
New: func() core.Module {
return &AddOperationNameModule{}
},
}
}

// Interface guard
var (
_ core.EnginePreOriginHandler = (*AddOperationNameModule)(nil)
_ core.Provisioner = (*AddOperationNameModule)(nil)
)
2 changes: 2 additions & 0 deletions router/cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import (
"github.com/wundergraph/cosmo/router/pkg/profile"
"github.com/wundergraph/cosmo/router/pkg/watcher"

_ "github.com/wundergraph/cosmo/router/cmd/custom-add-operation-name"

"go.uber.org/zap"
)

Expand Down
Loading