AlinsRan opened a new pull request, #13840:
URL: https://github.com/apache/apisix/pull/13840

   ### Description
   
   `graphql-limit-count` charges the quota by the query's maximum nesting 
depth. Depth ignores both fan-out and width — `users(first: 10000)` and 
`users(first: 1)` have the same depth, so they cost the same — which makes it 
hard to protect an upstream from the queries that actually hurt it.
   
   This adds two cost strategies next to the existing `depth`:
   
   | `cost_strategy` | cost of a field |
   |---|---|
   | `depth` | unchanged: the query's maximum nesting depth |
   | `default` | `( Σ the fields selected under it ) × mul_value + add_value` |
   | `node_quantifier` | only fields carrying a quantifier argument are 
charged, multiplied by how many times the field is resolved |
   
   **`depth` stays the default**, so an existing configuration keeps its 
current cost after the upgrade.
   
   ### Configuring the weights
   
   The per-field weights are configured inline on the plugin as `field_costs`:
   
   ```json
   {
     "cost_strategy": "node_quantifier",
     "max_cost": 5000,
     "field_costs": [
       {"field_path": "Query.products", "mul_arguments": ["first"], 
"add_value": 1},
       {"field_path": "Product.reviews", "mul_arguments": ["first"], 
"add_value": 1}
     ]
   }
   ```
   
   `add_value` / `mul_value` are the literals; `add_arguments` / 
`mul_arguments` name query arguments whose values fold into them, which is how 
a paginating argument turns into fan-out. `*_value` and `*_arguments` are the 
same quantity from two sources.
   
   A `field_path` is a chain of GraphQL name tokens, matched by walking it 
token by token:
   
   - `Product.reviews` weights `reviews` **wherever** it is selected on a 
`Product`.
   - `Query.products.nodes.reviews` pins the weight to **one chain** of 
selections and leaves a `reviews` reached any other way alone.
   
   Where a shallow and a deep path name the same field they merge key by key, 
longer path last, so the result is a function of the configuration rather than 
of the order the entries are listed in.
   
   A field with no matching entry weighs 1 and multiplies by 1, so a route with 
no `field_costs` is charged its node count.
   
   ### Schema introspection
   
   `field_costs` addresses a field by its GraphQL type, while the query AST 
carries only field names — `Person.name` cannot be told from `Vehicle.name` 
without the schema. The plugin issues an introspection query to the upstream on 
the first request that needs one, single-flights it with `resty.lock`, and 
caches it per worker; a schema change on a live upstream takes effect after a 
reload. **No introspection request is made** when `field_costs` is unset or 
`cost_strategy` is `depth`, so a route that does not use the cost model is 
unaffected.
   
   `introspection_endpoint` is there for deployments where the endpoint cannot 
be derived from the route's upstream. A failed introspection is remembered for 
10s, so an unresponsive endpoint does not draw one upstream request per client 
request, and the request path derived for the call is escaped before it reaches 
the introspection request line.
   
   ### Also added
   
   - `max_cost` — 403 once a query costs more than this, whatever quota is 
left. The quota is charged first, and 403 wins over the rate-limit rejection.
   - `score_factor` — scales the raw cost before it is charged, so a cost model 
with large numbers still fits a sane quota.
   - `resolve_variables` (default `false`) — read a quantifier passed as a 
GraphQL variable, or defaulted by the schema, instead of treating it as absent.
   - `pass_all_downstream_headers` — forward the downstream headers on the 
introspection request; only `Authorization` is forwarded by default.
   - `X-Graphql-Query-Cost` on the response, behind the existing 
`show_limit_quota_header`.
   
   ### Deliberate handling worth a look
   
   - **A quantifier that is not a number is treated as absent.** `first: "ten"` 
and `first: true` are client-controlled; letting them reach the arithmetic 
would let any caller fail their own request. A numeric string still multiplies.
   - **A field the introspected schema does not declare counts as a plain 
unweighted node.** `__typename` is in no schema's `fields` and clients add it 
automatically; a schema can also drift from the running upstream. Neither 
should fail a request.
   - **`operationName` decides what is charged.** A document with several 
operations only executes the one `operationName` selects, so that is the one 
costed. Without it the whole document is costed and the dearest operation 
charged, which cannot under-charge whichever one the upstream runs.
   
   ### Checklist
   
   - [x] I have explained the need for this PR and the problem it solves
   - [x] I have explained the changes or the new features added to this PR
   - [x] I have added tests corresponding to this change
   - [x] I have updated the documentation to reflect this change
   - [x] I have verified that this change is backward compatible (`depth` 
remains the default; no introspection request is made unless the cost model is 
configured)
   
   ### Tests
   
   `t/plugin/graphql-limit-count2.t`, 16 cases:
   
   - schema validation of `cost_strategy`, `score_factor` and `field_path`, 
including a four-segment path
   - the engine on a paginating query under both strategies, and with no 
weights at all
   - fragment spreads counted per spread site, inline fragments not moving the 
type cursor, variables with `resolve_variables` on and off, and a deep 
`field_path`
   - end to end: introspection from the upstream and the cost applied; 
quantifiers multiplying across nesting; `resolve_variables` counting a variable 
and a schema argument default; `max_cost` rejecting with 403; `score_factor`; 
`operationName` selecting the charged operation; and `depth` still being the 
default strategy
   
   `t/lib/server.lua` gains a mock GraphQL upstream that answers both the 
introspection query and normal queries from the same endpoint, which is how the 
plugin derives the introspection endpoint when `introspection_endpoint` is 
unset.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to