Skip to content

refactor: make default interceptor ordering explicit and tested#47

Merged
ankurs merged 4 commits intomainfrom
claude/fix-interceptor-ordering-g4egC
Apr 20, 2026
Merged

refactor: make default interceptor ordering explicit and tested#47
ankurs merged 4 commits intomainfrom
claude/fix-interceptor-ordering-g4egC

Conversation

@ankurs
Copy link
Copy Markdown
Member

@ankurs ankurs commented Apr 18, 2026

Assign each default server interceptor to a named position constant
(unaryPos*, streamPos*) instead of relying on the physical order of
append calls, and document the ordering contract above the constants.
Add tests in interceptors_test.go that guard the layering: the position
ordering itself, slot wiring, user interceptors being outermost, and
that panic recovery is innermost so metrics/error-reporting/tracing see
the synthesized error. No behavior change.

https://claude.ai/code/session_01EWgytCisKXLiVp5BNhp2xE

Summary by CodeRabbit

  • Tests

    • Added comprehensive interceptor ordering contract tests validating correct execution sequence across timeout, logging, tracing, validation, metrics, error handling, and panic recovery layers.
  • Chores

    • Refactored interceptor construction using explicit slot-based ordering for unary and stream interceptors.

Assign each default server interceptor to a named position constant
(unaryPos*, streamPos*) instead of relying on the physical order of
append calls, and document the ordering contract above the constants.
Add tests in interceptors_test.go that guard the layering: the position
ordering itself, slot wiring, user interceptors being outermost, and
that panic recovery is innermost so metrics/error-reporting/tracing see
the synthesized error. No behavior change.

https://claude.ai/code/session_01EWgytCisKXLiVp5BNhp2xE
Copilot AI review requested due to automatic review settings April 18, 2026 01:28
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 18, 2026

Warning

Rate limit exceeded

@ankurs has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 50 minutes and 32 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 50 minutes and 32 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: dce8a9a2-48a3-4aa8-8ee5-b5a11fb0c201

📥 Commits

Reviewing files that changed from the base of the PR and between 641bdf6 and 729cf29.

📒 Files selected for processing (3)
  • README.md
  • interceptors_test.go
  • server.go
📝 Walkthrough

Walkthrough

Refactored interceptor chain construction in server.go to use explicit slot-based positioning for unary and stream interceptors, establishing fixed ordering guarantees. Added comprehensive test coverage in interceptors_test.go validating slot positions, wiring correctness, and semantic invariants around panic recovery and interceptor layering.

Changes

Cohort / File(s) Summary
Interceptor Slot Positioning
server.go
Introduced unaryPos* and streamPos* slot constants for fixed-position interceptor placement. Refactored DefaultInterceptors() and DefaultStreamInterceptors() to allocate fixed-size slices and conditionally assign interceptors (timeouts, logging, trace-id extraction, debug-log override, proto-validate, metrics, error notifier, New Relic, panic recovery) to their designated slots, with final chain built by iterating slot slice.
Interceptor Ordering Contract Tests
interceptors_test.go
Added 372 lines of "Interceptor ordering contract tests" validating slot constants, interceptor count, wiring correctness (deadline, log enrichment, trace-id extraction, debug override, panic recovery), and semantic invariants (panic recovery innermost relative to error/metrics/tracing, proto-validation outer to metrics/error layers). Includes end-to-end chain tests for unary and stream interceptors with user-registered interceptor execution ordering.
Test Dependencies
go.mod (implied)
Added dependency on github.com/go-coldbrew/errors/notifier for trace-id propagation validation in TraceID slot wiring tests.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 Slots aligned in perfect rows,
Each interceptor finds its pose,
From panic's catch to trace's glow,
The ordered chain now clearly flows!
No more append chaos—hooray!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 58.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: refactoring interceptor ordering to be explicit with named position constants and adding comprehensive tests to validate the ordering contract.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/fix-interceptor-ordering-g4egC

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 and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@interceptors_test.go`:
- Around line 2065-2098: The test TestDefaultInterceptors_PanicThroughFullChain
can false-pass because ProtoValidateInterceptor (at unaryPosProtoValidate) may
short-circuit a nil request before the panic-recovery interceptor
(unaryPosPanicRecovery) runs; update the test to either disable protovalidate
for this case or assert that the returned error is specifically the recovered
panic error. Concretely, modify TestDefaultInterceptors_PanicThroughFullChain to
call DefaultInterceptors() with proto validation disabled (or remove
ProtoValidateInterceptor from the chain before calling chainUnaryServer) so the
handler panic executes, and/or tighten the assertion on err to confirm it
contains the panic payload (e.g., the "boom" message) rather than any validation
error.

In `@server.go`:
- Around line 93-97: Update the godoc comment that currently references
non-existent tests to list the actual test names that enforce the interceptor
contract: replace `TestDefaultInterceptors_Order`,
`TestDefaultInterceptors_PanicRecoveryInnermost`, and
`TestDefaultInterceptors_ProtoValidateBeforeHandler` with the real test names
`TestInterceptorPositionConstants`, `TestDefaultInterceptors_SlotWiring`, and
`TestDefaultInterceptors_PanicThroughFullChain` (keeping
`TestDefaultInterceptors_UserInterceptorsOutermost` and the stream variants) so
the comment accurately points to the guardrail tests in `interceptors_test.go`.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7986e273-af7c-4ac1-b049-fdd3cb985389

📥 Commits

Reviewing files that changed from the base of the PR and between 48b1e38 and 641bdf6.

📒 Files selected for processing (2)
  • interceptors_test.go
  • server.go

Comment thread interceptors_test.go
Comment thread server.go Outdated
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR makes the default gRPC server interceptor ordering explicit (via named position constants) and adds tests to lock in the intended layering/ordering semantics for both unary and stream chains.

Changes:

  • Introduces unaryPos* / streamPos* constants and refactors DefaultInterceptors / DefaultStreamInterceptors to populate interceptors by named slots rather than relying on append order.
  • Adds a documented interceptor ordering contract in server.go.
  • Adds contract/ordering tests in interceptors_test.go to guard position ordering, slot wiring, user interceptor placement, and panic recovery placement.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
server.go Adds ordering contract + position constants; refactors default interceptor chain construction to be slot-based.
interceptors_test.go Adds tests intended to enforce the ordering/slot wiring contract and key layering invariants.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread server.go Outdated
Comment on lines +93 to +97
// Tests in interceptors_test.go (TestDefaultInterceptors_Order,
// TestDefaultInterceptors_PanicRecoveryInnermost,
// TestDefaultInterceptors_ProtoValidateBeforeHandler,
// TestDefaultInterceptors_UserInterceptorsOutermost, and their stream
// variants) guard this contract.
Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

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

The comment listing the specific contract-guarding tests references test names that don’t exist in interceptors_test.go (e.g., TestDefaultInterceptors_Order, TestDefaultInterceptors_PanicRecoveryInnermost, TestDefaultInterceptors_ProtoValidateBeforeHandler). Please update this list to the actual test names added in this PR so the documentation remains accurate.

Suggested change
// Tests in interceptors_test.go (TestDefaultInterceptors_Order,
// TestDefaultInterceptors_PanicRecoveryInnermost,
// TestDefaultInterceptors_ProtoValidateBeforeHandler,
// TestDefaultInterceptors_UserInterceptorsOutermost, and their stream
// variants) guard this contract.
// Contract tests in interceptors_test.go guard this ordering and layering
// behavior for both unary and stream interceptor chains.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Addressed in 9dccd96: the godoc now lists the real tests — TestInterceptorPositionConstants, TestDefaultInterceptors_SlotWiring, TestDefaultInterceptors_PanicThroughFullChain, TestDefaultInterceptors_UserInterceptorsOutermost (and stream variants).


Generated by Claude Code

Comment thread server.go Outdated
Comment on lines +79 to +83
// - Protovalidate runs BEFORE metrics / error reporting / tracing so that
// validation failures are visible to them as InvalidArgument errors.
// - Metrics, ServerErrorInterceptor, and New Relic wrap the handler from
// the OUTSIDE. They observe the final error/response, including errors
// synthesized by the panic-recovery layer.
Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

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

The ordering contract claims protovalidate runs before metrics/error reporting/tracing “so that validation failures are visible” to those layers, but with the current ordering (protovalidate placed outer to metrics/ServerError/NewRelic) a validation failure will typically short-circuit and prevent inner interceptors (metrics/error reporting/tracing) from running/observing the error. Either (a) move metrics/error-reporting/tracing to be outer to protovalidate if the intent is to record validation failures, or (b) adjust the contract wording to reflect that inner layers won’t observe validation rejections when protovalidate short-circuits.

Suggested change
// - Protovalidate runs BEFORE metrics / error reporting / tracing so that
// validation failures are visible to them as InvalidArgument errors.
// - Metrics, ServerErrorInterceptor, and New Relic wrap the handler from
// the OUTSIDE. They observe the final error/response, including errors
// synthesized by the panic-recovery layer.
// - Protovalidate runs BEFORE metrics / error reporting / tracing. Because
// it is outer to those layers, a validation failure usually short-circuits
// the call and returns InvalidArgument before the inner metrics / error
// reporting / tracing interceptors run.
// - Metrics, ServerErrorInterceptor, and New Relic wrap the handler from
// the OUTSIDE of the inner handler stack. They observe downstream
// responses/errors that propagate back outward, including errors
// synthesized by the panic-recovery layer, but not validation rejections
// short-circuited by outer protovalidate.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Addressed in e8bb8b8 — went with option (b). The godoc now states that protovalidate sits outer to metrics/error-reporting/tracing, so a validation failure short-circuits with InvalidArgument before the inner layers run (intentional: don't burn metrics/reporting work on obviously bad requests).


Generated by Claude Code

Comment thread interceptors_test.go Outdated
Comment on lines +1885 to +1892
// Protovalidate must be INNER to metrics/error-reporting so validation
// errors are visible to them.
if unaryPosProtoValidate >= unaryPosMetrics {
t.Error("protovalidate must be OUTER to metrics (so InvalidArgument is recorded)")
}
if unaryPosProtoValidate >= unaryPosServerError {
t.Error("protovalidate must be OUTER to ServerErrorInterceptor")
}
Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

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

In TestInterceptorPositionConstants, the protovalidate invariants/commentary are internally inconsistent with the chaining semantics documented in server.go/chain.go: the comment says protovalidate must be INNER to metrics/error-reporting, but the assertions enforce protovalidate being OUTER (pos < metrics/serverError). Also, the failure messages implying this ordering makes validation errors “recorded” by metrics are misleading if protovalidate short-circuits before calling inner interceptors. Please align the comment/assertions/messages with the actual intended layering.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Addressed in e8bb8b8: updated the comment and failure messages in TestInterceptorPositionConstants to match the actual semantics — protovalidate is OUTER to metrics/ServerError and short-circuits before they run, rather than "so InvalidArgument is recorded".


Generated by Claude Code

Comment thread interceptors_test.go Outdated
}
})

t.Run("ServerError_PassThroughNoError", func(t *testing.T) {
Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

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

The subtest name "ServerError_PassThroughNoError" is misleading: this case intentionally uses a panicking handler and asserts that the panic propagates past ServerErrorInterceptor (i.e., it’s not a no-error pass-through). Renaming the subtest to reflect that it’s validating non-recovery/panic propagation would make the intent clearer.

Suggested change
t.Run("ServerError_PassThroughNoError", func(t *testing.T) {
t.Run("ServerError_PanicPropagates", func(t *testing.T) {

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Addressed in e8bb8b8: renamed to ServerError_PanicPropagates (and the stream variant from ServerError_NoRecover to ServerError_PanicPropagates).


Generated by Claude Code

claude added 3 commits April 18, 2026 06:03
- Correct stale test names referenced in the ordering-contract godoc.
- Drop redundant per-constant comments; the godoc already explains the
  rationale and the names are self-documenting.
- Preallocate the returned interceptor slice to its known maximum size
  to avoid reallocation on append.
- Drop the redundant userCalled flag in TestDefaultInterceptors_PanicThroughFullChain
  (a non-nil userSawErr already implies the user interceptor ran).

https://claude.ai/code/session_01EWgytCisKXLiVp5BNhp2xE
…ordering docs

- TestDefaultInterceptors_PanicThroughFullChain: disable protovalidate and
  assert the returned error contains the panic payload. Otherwise the test
  could false-pass on the InvalidArgument returned by protovalidate rejecting
  a nil request, without ever invoking the panicking handler.
- Correct the ordering contract godoc: protovalidate sits OUTER to metrics /
  error-reporting / tracing, so a validation failure short-circuits and
  inner layers do NOT observe it. The previous wording claimed the opposite.
- Align the TestInterceptorPositionConstants failure messages with the
  corrected semantics.
- Rename the ServerError subtests from *_PassThroughNoError / *_NoRecover to
  *_PanicPropagates — they test panic propagation past ServerErrorInterceptor.

https://claude.ai/code/session_01EWgytCisKXLiVp5BNhp2xE
Pick up the refreshed DefaultInterceptors / DefaultStreamInterceptors
descriptions and the line-number shifts caused by the new ordering
contract godoc and position constants.

https://claude.ai/code/session_01EWgytCisKXLiVp5BNhp2xE
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@ankurs ankurs merged commit b52853b into main Apr 20, 2026
11 checks passed
@ankurs ankurs deleted the claude/fix-interceptor-ordering-g4egC branch April 20, 2026 15:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants