Repository navigation
Add per-operation hooks to GraphQLHandler - #20
Merged
Merged
Conversation
Coverage Report for CI Build 35236863921Coverage increased (+0.04%) to 91.288%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
wutsch0
reviewed
Sep 9, 2026
Comment on lines
+38
to
+39
| preOperationHook PreOperationHook | ||
| postOperationHook PostOperationHook |
There was a problem hiding this comment.
@OlfaKaroui you once implemented pre- and post execution hooks into the gateway as well, didn't you? Was that in the same area or somewhere else?
greenmato
approved these changes
Sep 15, 2026
greenmato
left a comment
There was a problem hiding this comment.
LGTM (see comment on https://github.com/amboss-mededu/graphql-gateway/pull/695#pullrequestreview-5207108619)
The handler already exposes pre/post hooks per plan step, but nothing runs once per operation, and the response payload is assembled entirely inside executeRequest. Users who need to touch the payload, e.g. to add response extensions gathered during execution, had to reimplement the handler. WithPreOperationHook runs after the RequestContext is built and before planning; it may replace rc.Context, which is how per-operation state reaches the executor and the queryers even in batch mode, where every operation otherwise shares the incoming request context. WithPostOperationHook runs as the last step before the operation's payload is written, on success and on failure. It sees the payload exactly as the gateway would have written it, including the gateway's own extensions, and may add or modify keys. The gateway itself makes no assumptions about what the hook does with them. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
cideM
force-pushed
the
feat/operation-hooks
branch
from
September 17, 2026 14:55
e46be37 to
d11662a
Compare
Author
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Context
The Content API team has a feature in its GraphQL API that requires both custom directives to work (already fixed) and
extensionssupport in GQL responses (this PR + one in thegraphqllibrary fork + one in our GQL gateway). Specifically, we want to forward theextensions.relationskey that Content API returns for queries carrying the@capiRelationMapdirective.Problem
Extensions are currently dropped by the combination of the
gatewaylibrary and our own GQL gateway.As for the library,
GraphQLHandlerbuilds the response payload for each operation insideexecuteRequest:But
Executeonly returns(data, err), notextensions, and nothing runs between the payload being assembled and it being stored.Solution
First, the main alternative I considered was implementing this entirely in the GQL gateway. That would require copying the entire handler (
GraphQLHandlerand its helpers inhttp.go) fromnautilus/gatewayinto our GQL gateway, in order to own the one step where the payload is assembled fromExecute's return value and add the collectedextensionsthere. Everything else in the copy (request parsing, batch handling, status codes, error formatting) would exist only to reach that step and would have to track upstream changes tohttp.goby hand. The advantage would be that the fork stays closer to upstream, the downside is unexpected code that needs additional maintenance.I therefore decided to add a minimal configuration option, similar to the per-step execution hooks we already added to the fork previously (c89d5e6):
Those run once per plan step. The new hooks run once per operation, one level up:
WithPreOperationHook(func(rc *RequestContext))runs once per operation, after theRequestContextis built and before planning. It may replacerc.Context. This matters in batch mode, where every operation is given the samer.Context(); a hook that needs per-operation state derives a child context here, and the executor and queryers see it during execution.WithPostOperationHook(func(rc *RequestContext, payload map[string]interface{}))runs as the last step before an operation's payload is stored, on success and on failure. It sees the payload exactly as the gateway would have written it,persistedQueryincluded, and may add or modify keys.The library makes no assumptions about what the hooks do. Batch responses are unaffected: hooks run per operation, and each entry in the batch keeps its own payload.
The idea is that we can use these hooks to attach a collector (pre-op; combines extensions based on a merge policy) to each operation and make sure that the
extensionskeys from downstream hooks aren't dropped (post-op).