Skip to content

Commit ba75e25

Browse files
authored
feat: validate presence of optional @requires dependencies (#1297)
In GraphQL Federation, when a field has @requires dependencies on external nullable field and those dependencies fail to resolve, the entire entity resolution would lead to malformed data being sent to later fetches. For example, 2 subgraphs: # Subgraph 1 type Entity @key(fields: "id") { id: ID info: String } # Subgraph 2, where fullInfo requires # the "info" field to be resolved first. type Query { entity: [Entity] } type Entity @key(fields: "id") { id: ID info: String @external fullInfo: String @requires(fields: "info") } If the fetch from Subgraph1 returned the null value for the "info" field along with error pointing to that field, then this bit of information should not be used in the fetch from Subgraph 2 that provides info to get the value of the "fullInfo" field. This change implements validation of optional @requires dependencies in GraphQL Federation. The core feature allows the system to gracefully handle scenarios where entities fail to resolve their @requires dependencies, particularly for nullable fields. This change introduces selective handling where only nullable @requires fields are treated as "optional" - if they fail, the entity is marked as "tainted" but processing continues for other entities.
1 parent bca665a commit ba75e25

14 files changed

Lines changed: 2114 additions & 92 deletions

execution/engine/execution_engine_helpers_test.go

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,54 @@ func createTestRoundTripper(t *testing.T, testCase roundTripperTestCase) testRou
4444
receivedBodyBytes, err = io.ReadAll(req.Body)
4545
require.NoError(t, err)
4646
}
47-
require.Equal(t, testCase.expectedBody, string(receivedBodyBytes), "roundTripperTestCase body do not match")
47+
require.Equal(t, testCase.expectedBody, string(receivedBodyBytes), "roundTripperTestCase received unexpected body")
4848
}
4949

5050
body := bytes.NewBuffer([]byte(testCase.sendResponseBody))
5151
return &http.Response{StatusCode: testCase.sendStatusCode, Body: io.NopCloser(body)}
5252
}
5353
}
5454

55+
type conditionalTestCase struct {
56+
expectedHost string
57+
expectedPath string
58+
expectedMethod string
59+
60+
// responses map an expected body to the output that should be sent
61+
responses map[string]sendResponse
62+
}
63+
64+
type sendResponse struct {
65+
statusCode int
66+
body string
67+
}
68+
69+
func createConditionalTestRoundTripper(t *testing.T, testCase conditionalTestCase) testRoundTripper {
70+
t.Helper()
71+
72+
require.True(t, len(testCase.responses) > 0, "no responses defined")
73+
74+
return func(req *http.Request) *http.Response {
75+
t.Helper()
76+
77+
assert.Equal(t, testCase.expectedHost, req.URL.Host)
78+
assert.Equal(t, testCase.expectedPath, req.URL.Path)
79+
80+
require.NotNil(t, req.Body, "roundTripperTestCase received nil body")
81+
82+
gotBody, err := io.ReadAll(req.Body)
83+
require.NoError(t, err)
84+
defer req.Body.Close()
85+
86+
require.Containsf(t, testCase.responses, string(gotBody), "received unexpected body: %v", string(gotBody))
87+
response := testCase.responses[string(gotBody)]
88+
return &http.Response{
89+
StatusCode: response.statusCode,
90+
Body: io.NopCloser(bytes.NewBuffer([]byte(response.body))),
91+
}
92+
}
93+
}
94+
5595
func stringify(any interface{}) []byte {
5696
out, _ := json.Marshal(any)
5797
return out

0 commit comments

Comments
 (0)