Skip to content

fix(router): renew APQ TTL without corrupting stored query#3072

Open
Lawal-create wants to merge 1 commit into
wundergraph:mainfrom
Lawal-create:fix/apq-ttl-renewal-corrupts-stored-query
Open

fix(router): renew APQ TTL without corrupting stored query#3072
Lawal-create wants to merge 1 commit into
wundergraph:mainfrom
Lawal-create:fix/apq-ttl-renewal-corrupts-stored-query

Conversation

@Lawal-create

@Lawal-create Lawal-create commented Jul 12, 2026

Copy link
Copy Markdown

Summary by CodeRabbit

  • Bug Fixes

    • Improved automatic persisted query TTL renewal without altering the original query content.
    • Fixed issues affecting queries that use conditional directives such as @skip.
    • Ensured cached queries continue to resolve conditional fields correctly after TTL renewal.
  • Tests

    • Added regression coverage for TTL renewal across distributed and in-memory persistence scenarios.

Description

Fixes #3062.

Automatic Persisted Queries (APQ) corrupted the stored query on TTL renewal.
When a persisted operation whose store entry has a TTL is re-executed and
resolved from the in-memory normalization cache, the router re-saved it to renew
the expiration but wrote back the normalized representation instead of the
original raw query. Normalization evaluates and strips @skip/@include using
the current request's variables, so the stored query was overwritten with a
variable-specific, directive-stripped version. Subsequent requests resolving the
same hash with different variables then silently lost the conditionally-included
fields (HTTP 200, fields absent). Only TTL-renewal on later cache hits was
affected; initial registration correctly stored the raw query.

Root cause

In FetchPersistedOperation (router/core/operation_processor.go), the APQ
cache-hit-with-TTL branch called SaveOperation(..., NormalizedRepresentation).
On the hash-only replay path the raw query is not in memory, so the normalized
(stripped) form was written to the APQ store (Redis or in-memory).

Fix

Renew the TTL at the storage layer without rewriting the body:

  • apq.Client.RenewTTL — re-writes the raw body already present in the store via
    the existing Get+Set (APQ is content-addressed by sha256(rawQuery), so
    the stored body is invariant). No-op when the entry is absent. Covers both the
    Redis and in-memory stores.
  • persistedoperation.Client.RenewOperationTTL — wraps it and preserves the
    existing PQL-manifest ownership guard used by SaveOperation.
  • FetchPersistedOperation now calls RenewOperationTTL instead of
    SaveOperation(NormalizedRepresentation).

No change to the apq.KVClient interface, APQ registration, or
persisted-operation lookup semantics. No generated code changes.

Tests

  • Unit (router/core/operation_processor_apq_ttl_test.go): regression tests for
    the KV-backed (Redis-style mock) and in-memory APQ stores — asserting the
    stored body stays the raw query and that a skip:false replay still resolves
    the conditional field after a skip:true renewal. Based on the regression test
    from Failing regression test for APQ TTL renewal corrupting stored query #3061.
  • Integration (router-tests/operations/automatic_persisted_queries_test.go): a
    Redis-backed end-to-end regression in the existing redis cache group,
    asserting the Redis entry remains the raw query after renewal and that a
    skip:false request returns the conditional field.

Both were verified to fail on the unpatched code and pass with the fix.

How to test

  1. Configure APQ with a Redis store and automatic_persisted_queries.cache.ttl > 0.
  2. Register query Q($skip: Boolean!) { a: __typename b: __typename @skip(if: $skip) }
    with {"skip": true} (query + sha in the request).
  3. Replay hash-only with {"skip": true} (triggers TTL renewal).
  4. Send hash-only with {"skip": false} → the response must include b.
    On the unpatched router, b is missing and Redis holds the stripped
    query Q { a: __typename }.

Automated:

cd router && make test
cd router-tests && go test -race -run '^TestAutomaticPersistedQueries$' ./operations/

Notes for upgraders

Entries already corrupted in Redis by an affected router cannot be reconstructed
from hash-only requests. After upgrading, flush the APQ key prefix or wait for
the TTL to expire; affected clients re-register on the next miss.

Checklist

Open Source AI Manifesto

This contribution follows the principles of the Open Source AI Manifesto.

@Lawal-create
Lawal-create requested a review from a team as a code owner July 12, 2026 06:07
@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e1767d4b-b1ae-4f2a-bd68-6edca956c38e

📥 Commits

Reviewing files that changed from the base of the PR and between 803a4bc and 2212ec3.

📒 Files selected for processing (5)
  • router-tests/operations/automatic_persisted_queries_test.go
  • router/core/operation_processor.go
  • router/core/operation_processor_apq_ttl_test.go
  • router/internal/persistedoperation/apq/client.go
  • router/internal/persistedoperation/client.go

Walkthrough

Changes

APQ TTL renewal

Layer / File(s) Summary
APQ renewal client contract and storage behavior
router/internal/persistedoperation/apq/client.go, router/internal/persistedoperation/client.go
Adds TTL renewal methods that refresh existing APQ entries without replacing their stored raw queries.
Persisted operation cache-hit renewal
router/core/operation_processor.go
Replaces normalized-operation re-saving with persisted-operation TTL renewal on cache hits.
Raw-query preservation regression coverage
router/core/operation_processor_apq_ttl_test.go, router-tests/operations/automatic_persisted_queries_test.go
Tests KV-backed, in-memory, and Redis APQ renewal with conditional directives and variable-dependent replay.

Estimated code review effort: 3 (Moderate) | ~20 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the APQ TTL renewal fix without corrupting the stored query.
Linked Issues check ✅ Passed The PR addresses #3062 by renewing APQ TTL without rewriting the stored raw query, preserving conditional-field behavior and leaving registration/lookup intact.
Out of Scope Changes check ✅ Passed The code and tests stay focused on APQ TTL renewal and raw-query preservation, with no obvious unrelated changes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies"


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

APQ corrupts stored queries with @skip and @includeIf directives on TTL renewal with normalized versions

1 participant