Add non-cloning ctx.GetOASDefinitionRaw accessor for read-only hot paths
Is your feature request related to a problem? Please describe.
ctx.GetOASDefinition() returns a deep clone of the OAS definition stored in the request context, built through reflect.Clone. It clones on every call, even where the caller only reads the result.
In production, at around 100 requests per second, one auth plugin spends about 80% of its CPU time in that clone. It calls GetOASDefinition() once per request. That cost is a scaling bottleneck for a gateway instance under load.
flowchart LR
A[Request context] --> B{Caller needs to mutate?}
B -->|No, read only| C[GetOASDefinitionRaw]
B -->|Yes| D[GetOASDefinition]
C --> E[Live *oas.OAS pointer<br/>0 allocations]
D --> F[reflect.Clone<br/>about 75.5 KB, 857 allocations]A benchmark against a realistic definition shows the cost. Timing varies by machine. The allocation counts do not:
| Call | Time | Allocations | Memory |
|---|---|---|---|
GetOASDefinition (clone) |
215 µs | 857 | 75.5 KB |
| Direct pointer read | 5 ns | 0 | 0 B |
The deep copy exists so a caller can safely mutate a request-private copy, without racing other concurrent requests. That safety matters for the general case, but a caller that only reads the definition pays that cost for no benefit.
Describe the solution you'd like
Add ctx.GetOASDefinitionRaw(r *http.Request) *oas.OAS. It returns the shared *oas.OAS pointer stored in the context, with no clone. This follows the "Raw" naming already used in the codebase for an unprocessed, underlying value, such as log.GetRaw() and certificateManager.GetRaw().
GetOASDefinitionRaw returns the live pointer. A caller must not mutate it. GetOASDefinition is unchanged and stays the safe default for a caller that needs to mutate the result.
The new function is additive. Existing code keeps calling GetOASDefinition and keeps the same guarantees.
Describe alternatives you've considered
- Change
GetOASDefinitionto return the raw pointer directly. Rejected: it breaks any caller that mutates the result today. - Add a boolean parameter, such as
GetOASDefinition(r, clone=true). Rejected in favor of two distinct functions, which reads more clearly at the call site and matches the existing "Raw" naming pattern. - Clone lazily, only once the caller actually mutates the result. Rejected: detecting a mutation is itself expensive, and the two-function approach is simpler.
- Cache the clone per request. Rejected: cloning is already cheap to repeat, so a cache only helps a caller that calls
GetOASDefinitionmore than once per request. That does not justify the added context-map state.
Additional context
The OAS definition is a 200 to 300 KB object. A full reflect.Clone walks and reallocates the whole structure, which becomes hot for any plugin or middleware that reads it once per request.
The benchmark ran as a standard Go benchmark (go test -bench=. -benchmem), with the iteration count chosen by the testing framework.
The classic-definition equivalent, ctx.GetDefinition(), has the same clone cost. That is a separate piece of work, not covered here. This issue and its PR stay scoped to the profiled OAS auth path.
AI assistance disclosure
I used an AI coding assistant (Claude) to draft this issue and the linked PR's code, tests, and description. I read and reviewed the diff and the reasoning behind it myself before opening either.
Source: TykTechnologies/tyk