zuplo 6.70.69 → 6.70.71

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/docs/ai-gateway/getting-started.mdx +14 -9
  2. package/docs/ai-gateway/integrations/ai-sdk.mdx +17 -0
  3. package/docs/ai-gateway/introduction.mdx +12 -10
  4. package/docs/ai-gateway/providers.mdx +2 -0
  5. package/docs/analytics/access-and-entitlements.md +71 -0
  6. package/docs/analytics/overview.md +63 -0
  7. package/docs/analytics/reference/metrics-glossary.md +105 -0
  8. package/docs/analytics/reference/url-parameters.md +66 -0
  9. package/docs/analytics/shared-controls.md +121 -0
  10. package/docs/analytics/tabs/agents.md +88 -0
  11. package/docs/analytics/tabs/consumers.md +73 -0
  12. package/docs/analytics/tabs/graphql.md +77 -0
  13. package/docs/analytics/tabs/mcp.md +80 -0
  14. package/docs/analytics/tabs/origins.md +82 -0
  15. package/docs/analytics/tabs/requests.md +96 -0
  16. package/docs/articles/api-key-buckets.mdx +4 -2
  17. package/docs/articles/archiving-requests-to-storage.mdx +4 -4
  18. package/docs/articles/branch-based-deployments.mdx +10 -8
  19. package/docs/articles/ci-cd-github/basic-deployment.mdx +10 -1
  20. package/docs/articles/ci-cd-github/cleanup-on-branch-delete.mdx +52 -31
  21. package/docs/articles/ci-cd-github/deploy-and-test.mdx +14 -1
  22. package/docs/articles/ci-cd-github/local-testing.mdx +3 -1
  23. package/docs/articles/ci-cd-github/pr-preview-environments.mdx +53 -10
  24. package/docs/articles/custom-ci-cd-azure.mdx +1 -1
  25. package/docs/articles/custom-ci-cd-bitbucket.mdx +1 -1
  26. package/docs/articles/custom-ci-cd-circleci.mdx +1 -1
  27. package/docs/articles/custom-ci-cd-github.mdx +12 -3
  28. package/docs/articles/custom-ci-cd-gitlab.mdx +1 -1
  29. package/docs/articles/graphql.mdx +276 -0
  30. package/docs/articles/monetization/api-access.mdx +184 -0
  31. package/docs/articles/monetization/meters.mdx +4 -4
  32. package/docs/articles/monetization/monetization-policy.md +4 -1
  33. package/docs/articles/monetization/private-plans.md +3 -4
  34. package/docs/articles/monetization/stripe-integration.md +9 -0
  35. package/docs/articles/monetization/subscription-lifecycle.md +12 -11
  36. package/docs/articles/monorepo-deployment.mdx +37 -5
  37. package/docs/articles/opentelemetry.mdx +5 -2
  38. package/docs/articles/securing-the-gateway-with-client-mtls.mdx +68 -43
  39. package/docs/articles/step-1-setup-basic-gateway.mdx +1 -3
  40. package/docs/articles/step-2-add-rate-limiting.mdx +1 -1
  41. package/docs/articles/testing.mdx +1 -1
  42. package/docs/articles/troubleshooting.md +7 -3
  43. package/docs/articles/waf-ddos-akamai.md +35 -16
  44. package/docs/articles/waf-ddos-aws-waf-shield.mdx +35 -16
  45. package/docs/articles/waf-ddos-fastly.mdx +10 -7
  46. package/docs/cli/deploy.mdx +44 -9
  47. package/docs/cli/deploy.partial.mdx +44 -9
  48. package/docs/concepts/api-keys.md +2 -2
  49. package/docs/dev-portal/zudoku/components/callout.mdx +11 -18
  50. package/docs/dev-portal/zudoku/components/sidecar-box.mdx +131 -0
  51. package/docs/dev-portal/zudoku/configuration/api-catalog.md +62 -42
  52. package/docs/dev-portal/zudoku/configuration/api-reference.md +5 -4
  53. package/docs/dev-portal/zudoku/configuration/navigation.mdx +70 -7
  54. package/docs/dev-portal/zudoku/configuration/search.md +36 -0
  55. package/docs/dev-portal/zudoku/configuration/site.md +38 -0
  56. package/docs/dev-portal/zudoku/customization/colors-theme.mdx +51 -40
  57. package/docs/errors/rate-limit-exceeded.mdx +30 -3
  58. package/docs/guides/canary-routing-for-employees.mdx +103 -39
  59. package/docs/guides/modify-openapi-paths.mdx +3 -3
  60. package/docs/handlers/legacy-dev-portal-handler.mdx +1 -1
  61. package/docs/handlers/mcp-server.mdx +13 -11
  62. package/docs/handlers/url-forward.mdx +5 -1
  63. package/docs/handlers/url-rewrite.mdx +7 -2
  64. package/docs/handlers/websocket-handler.mdx +5 -1
  65. package/docs/mcp-gateway/observability/logging.mdx +19 -12
  66. package/docs/mcp-server/resources.mdx +27 -15
  67. package/docs/mcp-server/testing.mdx +0 -2
  68. package/docs/policies/_index.md +2 -0
  69. package/docs/policies/archive-request-azure-storage-inbound/doc.md +1 -1
  70. package/docs/policies/archive-response-azure-storage-outbound/doc.md +1 -1
  71. package/docs/policies/data-loss-prevention-inbound/doc.md +116 -0
  72. package/docs/policies/data-loss-prevention-inbound/intro.md +15 -0
  73. package/docs/policies/data-loss-prevention-inbound/schema.json +220 -0
  74. package/docs/policies/data-loss-prevention-outbound/doc.md +116 -0
  75. package/docs/policies/data-loss-prevention-outbound/intro.md +18 -0
  76. package/docs/policies/data-loss-prevention-outbound/schema.json +220 -0
  77. package/docs/policies/ip-restriction-inbound/policy.ts +1 -1
  78. package/docs/programmable-api/background-dispatcher.mdx +6 -8
  79. package/docs/programmable-api/http-problems.mdx +0 -18
  80. package/docs/programmable-api/jwt-service-plugin.mdx +131 -109
  81. package/docs/programmable-api/runtime-behaviors.mdx +4 -2
  82. package/docs/programmable-api/streaming-zone-cache.mdx +4 -6
  83. package/docs/programmable-api/web-crypto-apis.mdx +10 -6
  84. package/docs/programmable-api/zone-cache.mdx +1 -1
  85. package/docs/rate-limiting/combining-policies.mdx +293 -0
  86. package/docs/rate-limiting/dynamic-rate-limiting.mdx +240 -0
  87. package/docs/rate-limiting/getting-started.mdx +339 -0
  88. package/docs/rate-limiting/how-it-works.md +225 -0
  89. package/docs/rate-limiting/monitoring-and-troubleshooting.mdx +243 -0
  90. package/docs/{articles → rate-limiting}/per-user-rate-limits-using-db.mdx +39 -28
  91. package/package.json +4 -4
  92. package/docs/concepts/rate-limiting.md +0 -246
  93. package/docs/errors/get-head-body-error.mdx +0 -41
@@ -0,0 +1,243 @@
1
+ ---
2
+ title: Monitoring and troubleshooting rate limits
3
+ sidebar_label: Monitoring & troubleshooting
4
+ description:
5
+ Monitor rate limit events, debug unexpected 429 responses, and understand
6
+ failure modes.
7
+ ---
8
+
9
+ Rate limiting only delivers value when you can observe it in action. Without
10
+ visibility into which consumers hit limits, how often requests are rejected, and
11
+ whether the rate limit service itself is healthy, you are operating blind. This
12
+ guide covers how to monitor rate limit activity, understand failure modes,
13
+ choose the right enforcement mode, and diagnose common issues.
14
+
15
+ ## Monitoring rate limit events
16
+
17
+ Zuplo produces structured logs for every request, including those rejected with
18
+ a `429 Too Many Requests` status code. Ship these logs to an external provider
19
+ to build dashboards and alerts around rate limit activity.
20
+
21
+ ### Setting up log shipping
22
+
23
+ Configure a [logging plugin](../articles/logging.mdx) in your `zuplo.runtime.ts`
24
+ file to send logs to your observability platform. Zuplo supports AWS CloudWatch,
25
+ Datadog, Dynatrace, Google Cloud Logging, Loki, New Relic, Splunk, Sumo Logic,
26
+ and VMware Log Insight. You can also build a
27
+ [custom logging plugin](../articles/custom-logging-example.mdx) for unsupported
28
+ providers.
29
+
30
+ ### Filtering for rate-limited requests
31
+
32
+ Every log entry includes default fields you can filter on:
33
+
34
+ - **`requestId`** -- Correlate a specific rejected request end-to-end using the
35
+ `zp-rid` response header.
36
+ - **`environment`** and **`environmentStage`** -- Distinguish between
37
+ `production`, `preview`, and `working-copy` environments.
38
+
39
+ To break down rate-limited requests by consumer or IP, add custom log properties
40
+ in a policy that runs before or alongside the rate limit check:
41
+
42
+ ```ts
43
+ import { ZuploContext, ZuploRequest } from "@zuplo/runtime";
44
+
45
+ export default async function policy(
46
+ request: ZuploRequest,
47
+ context: ZuploContext,
48
+ ) {
49
+ // Tag every log entry with the consumer identity for filtering
50
+ context.log.setLogProperties!({
51
+ rateLimitIdentity:
52
+ request.user?.sub ?? request.headers.get("true-client-ip") ?? "unknown",
53
+ });
54
+ return request;
55
+ }
56
+ ```
57
+
58
+ This adds a `rateLimitIdentity` field to all log entries for the request, making
59
+ it straightforward to group 429 responses by consumer in your logging dashboard.
60
+
61
+ ### Setting up alerts
62
+
63
+ Configure alerts in your logging provider for the following conditions:
64
+
65
+ - **Spike in 429 responses** -- A sudden increase may indicate a
66
+ misconfiguration, an attack, or a legitimate traffic surge.
67
+ - **429 rate exceeding a threshold** -- If more than a small percentage of
68
+ requests return 429, the rate limit may be set too low for normal traffic.
69
+ - **Zero 429 responses over an extended period** -- If you expect rate limiting
70
+ to be active but see no rejections, the policy may not be attached to the
71
+ correct routes.
72
+
73
+ ### Metrics plugins
74
+
75
+ For quantitative monitoring, Zuplo supports
76
+ [metrics plugins](../articles/metrics-plugins.mdx) that send request latency,
77
+ request size, and response size data to Datadog, Dynatrace, New Relic, or any
78
+ OpenTelemetry-compatible collector. While these metrics do not track rate limit
79
+ counters directly, the `statusCode` dimension (when enabled) allows you to chart
80
+ 429 response rates alongside overall request volume.
81
+
82
+ ## Understanding failure modes
83
+
84
+ The rate limiting policies depend on a globally distributed rate limit service
85
+ to track request counters. Understanding what happens when that service is
86
+ unreachable helps you make the right availability tradeoff.
87
+
88
+ ### Fail-open (default)
89
+
90
+ By default, `throwOnFailure` is set to `false`. If the rate limit service is
91
+ unreachable, the policy allows the request through. This fail-open behavior
92
+ prevents a rate limit service outage from blocking all traffic to your API.
93
+
94
+ The tradeoff is that during an outage, rate limits are not enforced and clients
95
+ can exceed their configured thresholds.
96
+
97
+ ### Fail-closed
98
+
99
+ Set `throwOnFailure` to `true` to return an error when the rate limit service is
100
+ unreachable. This guarantees that no request bypasses rate limiting, but it
101
+ means a service disruption blocks all traffic on routes using that policy.
102
+
103
+ ```json
104
+ {
105
+ "options": {
106
+ "rateLimitBy": "user",
107
+ "requestsAllowed": 100,
108
+ "timeWindowMinutes": 1,
109
+ "throwOnFailure": true
110
+ }
111
+ }
112
+ ```
113
+
114
+ :::warning
115
+
116
+ Only use `throwOnFailure: true` when allowing unlimited traffic is more
117
+ dangerous than rejecting all traffic. For most APIs, the fail-open default is
118
+ the safer choice.
119
+
120
+ :::
121
+
122
+ ### Detecting fail-open conditions
123
+
124
+ Because fail-open requests succeed with a `200` (or other normal status code),
125
+ they do not produce a 429 log entry. To detect when the rate limit service is
126
+ unreachable, monitor for a sudden drop in 429 responses during periods when you
127
+ expect rate limiting to be active. A complete absence of 429s alongside steady
128
+ or increasing traffic volume is a strong signal that the service is in fail-open
129
+ mode.
130
+
131
+ ## Strict vs. async mode in production
132
+
133
+ The `mode` option controls whether the rate limit check blocks the request or
134
+ runs in parallel with it.
135
+
136
+ ### Strict mode (default)
137
+
138
+ In `strict` mode, every request waits for the rate limit service to confirm
139
+ whether the request is within limits before proceeding to the backend. This
140
+ provides exact enforcement -- no request exceeds the configured threshold.
141
+
142
+ The tradeoff is added latency on every request due to the round-trip to the rate
143
+ limit service.
144
+
145
+ ### Async mode
146
+
147
+ In `async` mode, the request proceeds to the backend immediately while the rate
148
+ limit check runs in parallel. If the check determines the limit is exceeded, the
149
+ result applies to the _next_ request, not the current one.
150
+
151
+ This means some requests may get through after the limit is reached. In
152
+ practice, the overshoot depends on your request rate and the latency of the rate
153
+ limit check. For an API receiving 100 requests per second with a 10ms check
154
+ time, approximately one extra request may slip through per window.
155
+
156
+ :::tip
157
+
158
+ Use `async` mode when low latency matters more than exact enforcement -- for
159
+ example, on high-throughput public endpoints where a few extra requests over the
160
+ limit are acceptable. Use `strict` mode when precise enforcement is required,
161
+ such as billing-sensitive endpoints or APIs with hard backend capacity limits.
162
+
163
+ :::
164
+
165
+ ## Common troubleshooting scenarios
166
+
167
+ ### Unexpected 429 responses
168
+
169
+ **Shared IP addresses.** When `rateLimitBy` is set to `"ip"`, multiple clients
170
+ behind the same corporate proxy, cloud NAT, or shared Wi-Fi share a single rate
171
+ limit bucket. One heavy user exhausts the limit for everyone on that IP. Switch
172
+ to `rateLimitBy: "user"` for authenticated APIs to avoid this.
173
+
174
+ **Missing authentication policy.** The `"user"` mode requires an authentication
175
+ policy (such as API Key Authentication or JWT) earlier in the policy pipeline to
176
+ populate `request.user`. If no authentication policy runs first, the rate limit
177
+ policy returns an error instead of applying per-user limits. Verify that
178
+ authentication appears before rate limiting in the route's inbound policy list.
179
+
180
+ **Multiple rate limit policies on the same route.** If a route has both a
181
+ per-minute and a per-hour rate limit policy, a request can be rejected by either
182
+ one. Check all rate limit policies attached to the route, and verify the
183
+ ordering (longest time window first, then shorter durations).
184
+
185
+ **Lower limits than expected.** If you use a custom `rateLimitBy: "function"`,
186
+ verify that the function returns the expected `requestsAllowed` and
187
+ `timeWindowMinutes` values. Log the returned values during development to
188
+ confirm the function resolves correctly for each consumer.
189
+
190
+ ### Rate limits not applying
191
+
192
+ **Policy not attached to the route.** Defining a rate limit policy in
193
+ `policies.json` does not activate it. The policy name must appear in the
194
+ `policies.inbound` array of each route in `routes.oas.json` where you want it
195
+ enforced. Verify the route configuration.
196
+
197
+ **Typo in the policy name.** The policy name in `routes.oas.json` must exactly
198
+ match the `name` field in `policies.json`. A mismatched name silently skips the
199
+ policy. Check for case sensitivity and extra whitespace.
200
+
201
+ **Custom function returning `undefined`.** When `rateLimitBy` is set to
202
+ `"function"` and the identifier function returns `undefined`, rate limiting is
203
+ skipped for that request entirely. This is by design -- it allows you to
204
+ selectively exempt certain requests -- but it can cause confusion if the
205
+ function has an unhandled code path that returns `undefined` unintentionally.
206
+
207
+ ### Different behavior across environments
208
+
209
+ Rate limit counters are scoped per environment. Production, preview, and
210
+ working-copy environments each maintain their own separate counters. A request
211
+ that is rate-limited in production does not affect the counter in a preview
212
+ environment, and vice versa.
213
+
214
+ This means:
215
+
216
+ - Testing rate limits in a preview branch does not interfere with production
217
+ traffic.
218
+ - Rate limit thresholds you observe in a low-traffic preview environment may
219
+ behave differently under production load.
220
+ - After deploying a new environment, counters start fresh.
221
+
222
+ :::note
223
+
224
+ If you observe rate limits triggering in one environment but not another,
225
+ confirm that both environments use the same policy configuration and that the
226
+ traffic volume is comparable.
227
+
228
+ :::
229
+
230
+ ## Related resources
231
+
232
+ - [Rate Limit Exceeded error](../errors/rate-limit-exceeded.mdx) --
233
+ Understanding the 429 response format and client-side remediation
234
+ - [How rate limiting works](./how-it-works.md) -- Algorithm details,
235
+ `rateLimitBy` modes, and combining policies
236
+ - [Logging](../articles/logging.mdx) -- Configuring log shipping to external
237
+ providers
238
+ - [Metrics Plugins](../articles/metrics-plugins.mdx) -- Sending request metrics
239
+ to Datadog, Dynatrace, New Relic, or OpenTelemetry
240
+ - [Proactive monitoring](../articles/monitoring-your-gateway.mdx) -- Health
241
+ checks and end-to-end gateway monitoring
242
+ - [Troubleshooting](../articles/troubleshooting.md) -- General gateway
243
+ troubleshooting guide
@@ -1,6 +1,6 @@
1
1
  ---
2
- title: Per user rate-limiting using a database and the ZoneCache
3
- sidebar_label: "Per-User Rate Limits"
2
+ title: Per-user rate limiting using a database and the ZoneCache
3
+ sidebar_label: "Per-user rate limits"
4
4
  description:
5
5
  Learn how to implement advanced dynamic rate limiting with database lookups
6
6
  and ZoneCache for improved performance.
@@ -9,24 +9,22 @@ tags:
9
9
  - caching
10
10
  ---
11
11
 
12
- In this example we show a more advanced implementation of
13
- [dynamic rate limiting](../articles/per-user-rate-limits-using-db.mdx). It uses
14
- a database lookup to get the customer details and combines that with the
15
- ZoneCache to improve performance, reduce latency and lower the load on the
16
- database.
12
+ This example shows a more advanced implementation of
13
+ [dynamic rate limiting](./dynamic-rate-limiting.mdx). It uses a database lookup
14
+ to get the customer details and combines that with the ZoneCache to improve
15
+ performance, reduce latency and lower the load on the database.
17
16
 
18
- In this example we use [Supabase](https://supabase.com) as the database but you
19
- could use your own API, [Xata](https://xata.io),
20
- [Firebase](https://firebase.com) etc. The implementation will be similar for
21
- all.
17
+ This example uses [Supabase](https://supabase.com) as the database, but you
18
+ could use your own API, [Xata](https://xata.io), or
19
+ [Firebase](https://firebase.com). The implementation is similar for all.
22
20
 
23
21
  If you haven't already, check out the
24
22
  [rate-limiting policy](../policies/rate-limit-inbound.mdx) and the
25
- [dynamic rate limiting quickstart](../articles/per-user-rate-limits-using-db.mdx).
26
- Then you should be oriented to how dynamic rate limiting works.
23
+ [dynamic rate limiting guide](./dynamic-rate-limiting.mdx). Then you should be
24
+ oriented to how dynamic rate limiting works.
27
25
 
28
- Below is a full implementation of a custom rate limiting function. In our
29
- example this is a module called `per-user-rate-limiting.ts`.
26
+ Below is a full implementation of a custom rate limiting function. In this
27
+ example it is a module called `per-user-rate-limiting.ts`.
30
28
 
31
29
  ```ts
32
30
  import {
@@ -48,9 +46,18 @@ export async function rateLimitKey(
48
46
  context: ZuploContext,
49
47
  policyName: string,
50
48
  ): Promise<CustomRateLimitDetails> {
51
- // We'll get the customer ID from the user data.
52
- // This might be from a JWT or API Key metadata
53
- const customerId = request.user.data.customerId;
49
+ // Get the customer ID from the user data.
50
+ // This might be from a JWT or API Key metadata.
51
+ // Ensure an authentication policy runs before this.
52
+ const customerId = request.user?.data?.customerId;
53
+ if (!customerId) {
54
+ context.log.error("No customerId found on request.user.data");
55
+ return {
56
+ key: request.user?.sub ?? "unknown",
57
+ requestsAllowed: FALLBACK_REQUESTS_ALLOWED,
58
+ timeWindowMinutes: 1,
59
+ };
60
+ }
54
61
 
55
62
  // We don't want to hit the database on every request
56
63
  // So we'll use the fast zone cache to cache this data
@@ -95,17 +102,21 @@ export async function rateLimitKey(
95
102
  The above function can be applied to a rate limiter with the following
96
103
  configuration in policies
97
104
 
98
- ```json
105
+ ```json title="config/policies.json"
99
106
  {
100
- "export": "RateLimitInboundPolicy",
101
- "module": "$import(@zuplo/runtime)",
102
- "options": {
103
- "rateLimitBy": "function",
104
- "requestsAllowed": 2,
105
- "timeWindowMinutes": 1,
106
- "identifier": {
107
- "export": "rateLimitKey",
108
- "module": "$import(./modules/per-user-rate-limiting)"
107
+ "name": "my-per-user-rate-limit-policy",
108
+ "policyType": "rate-limit-inbound",
109
+ "handler": {
110
+ "export": "RateLimitInboundPolicy",
111
+ "module": "$import(@zuplo/runtime)",
112
+ "options": {
113
+ "rateLimitBy": "function",
114
+ "requestsAllowed": 100,
115
+ "timeWindowMinutes": 1,
116
+ "identifier": {
117
+ "export": "rateLimitKey",
118
+ "module": "$import(./modules/per-user-rate-limiting)"
119
+ }
109
120
  }
110
121
  }
111
122
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zuplo",
3
- "version": "6.70.69",
3
+ "version": "6.70.71",
4
4
  "type": "module",
5
5
  "description": "The programmable API Gateway",
6
6
  "author": "Zuplo, Inc.",
@@ -19,9 +19,9 @@
19
19
  "zuplo": "zuplo.js"
20
20
  },
21
21
  "dependencies": {
22
- "@zuplo/cli": "6.70.69",
23
- "@zuplo/core": "6.70.69",
24
- "@zuplo/runtime": "6.70.69",
22
+ "@zuplo/cli": "6.70.71",
23
+ "@zuplo/core": "6.70.71",
24
+ "@zuplo/runtime": "6.70.71",
25
25
  "@zuplo/test": "1.4.0"
26
26
  }
27
27
  }
@@ -1,246 +0,0 @@
1
- ---
2
- title: Rate Limiting
3
- ---
4
-
5
- Rate limiting controls how many requests a client can make to your API within a
6
- given time window. It protects your backend from traffic spikes, enforces fair
7
- usage across consumers, and enables tiered access for different customer plans.
8
-
9
- Zuplo's rate limiter uses a **sliding window algorithm** enforced globally
10
- across all edge locations. When a client exceeds the limit, they receive a
11
- `429 Too Many Requests` response with a `retry-after` header indicating when
12
- they can retry.
13
-
14
- ## Rate limiting policies
15
-
16
- Zuplo provides two rate limiting policies, each suited to different levels of
17
- complexity.
18
-
19
- ### Rate Limiting policy
20
-
21
- The [Rate Limiting policy](../policies/rate-limit-inbound.mdx) enforces a single
22
- request counter per time window. Configure a maximum number of requests, a time
23
- window, and how to identify callers.
24
-
25
- ```json
26
- {
27
- "name": "my-rate-limit-policy",
28
- "policyType": "rate-limit-inbound",
29
- "handler": {
30
- "export": "RateLimitInboundPolicy",
31
- "module": "$import(@zuplo/runtime)",
32
- "options": {
33
- "rateLimitBy": "user",
34
- "requestsAllowed": 100,
35
- "timeWindowMinutes": 1
36
- }
37
- }
38
- }
39
- ```
40
-
41
- Use this policy when you need a straightforward "X requests per Y minutes"
42
- limit.
43
-
44
- ### Complex Rate Limiting policy
45
-
46
- The [Complex Rate Limiting policy](../policies/complex-rate-limit-inbound.mdx)
47
- supports **multiple named counters** in a single policy. Each counter tracks a
48
- different resource or unit of work.
49
-
50
- ```json
51
- {
52
- "name": "my-complex-rate-limit-policy",
53
- "policyType": "complex-rate-limit-inbound",
54
- "handler": {
55
- "export": "ComplexRateLimitInboundPolicy",
56
- "module": "$import(@zuplo/runtime)",
57
- "options": {
58
- "rateLimitBy": "user",
59
- "timeWindowMinutes": 1,
60
- "limits": {
61
- "requests": 100,
62
- "compute": 500
63
- }
64
- }
65
- }
66
- }
67
- ```
68
-
69
- You can override counter increments programmatically per request using
70
- `ComplexRateLimitInboundPolicy.setIncrements()`. This is useful for usage-based
71
- pricing where different endpoints consume different amounts of a resource (for
72
- example, counting compute units or tokens instead of raw requests).
73
-
74
- ## Choosing a policy
75
-
76
- | Scenario | Policy |
77
- | ------------------------------------------------------ | --------------------------------------------- |
78
- | Fixed requests-per-minute limit for all callers | Rate Limiting |
79
- | Different limits per customer tier (free vs. paid) | Rate Limiting with a custom function |
80
- | Counting multiple resources (requests + compute units) | Complex Rate Limiting |
81
- | Usage-based billing with variable cost per request | Complex Rate Limiting with dynamic increments |
82
-
83
- ## How `rateLimitBy` works
84
-
85
- The `rateLimitBy` option determines how the rate limiter groups requests into
86
- buckets. Both policies support the same four modes.
87
-
88
- ### `ip`
89
-
90
- Groups requests by the client's IP address. No authentication is required. This
91
- is the simplest option and works well for public APIs or as a first layer of
92
- protection.
93
-
94
- ### `user`
95
-
96
- Groups requests by the authenticated user's identity (`request.user.sub`). When
97
- using [API key authentication](../articles/api-key-authentication.mdx), the
98
- `sub` value is the consumer name you assigned when creating the API key. When
99
- using JWT authentication, it comes from the token's `sub` claim.
100
-
101
- This is the recommended mode for authenticated APIs because it ties limits to
102
- the actual consumer rather than a shared IP address.
103
-
104
- ### `function`
105
-
106
- Groups requests using a custom TypeScript function that you provide. The
107
- function returns a `CustomRateLimitDetails` object containing a grouping key
108
- and, optionally, overridden values for `requestsAllowed` and
109
- `timeWindowMinutes`.
110
-
111
- This mode enables dynamic rate limiting where limits vary based on customer
112
- tier, route parameters, or any other request property.
113
-
114
- ### `all`
115
-
116
- Applies a single shared counter across all requests to the route, regardless of
117
- who makes them. Use this for global rate limits on endpoints that call
118
- resource-constrained backends.
119
-
120
- ## Dynamic rate limiting with custom functions
121
-
122
- When `rateLimitBy` is set to `"function"`, you provide a TypeScript module that
123
- determines the rate limit at request time. The function signature is:
124
-
125
- ```ts
126
- import {
127
- CustomRateLimitDetails,
128
- ZuploContext,
129
- ZuploRequest,
130
- } from "@zuplo/runtime";
131
-
132
- export function rateLimit(
133
- request: ZuploRequest,
134
- context: ZuploContext,
135
- policyName: string,
136
- ): CustomRateLimitDetails | undefined {
137
- const user = request.user;
138
-
139
- if (user.data.customerType === "premium") {
140
- return {
141
- key: user.sub,
142
- requestsAllowed: 1000,
143
- timeWindowMinutes: 1,
144
- };
145
- }
146
-
147
- return {
148
- key: user.sub,
149
- requestsAllowed: 50,
150
- timeWindowMinutes: 1,
151
- };
152
- }
153
- ```
154
-
155
- The `CustomRateLimitDetails` object has the following properties:
156
-
157
- - `key` - The string used to group requests into rate limit buckets
158
- - `requestsAllowed` (optional) - Overrides the policy's `requestsAllowed` value
159
- - `timeWindowMinutes` (optional) - Overrides the policy's `timeWindowMinutes`
160
- value
161
-
162
- Returning `undefined` skips rate limiting for that request entirely.
163
-
164
- The function can also be `async` if you need to look up limits from a database
165
- or external service. See
166
- [Per-user rate limiting using a database](../articles/per-user-rate-limits-using-db.mdx)
167
- for a complete example using the ZoneCache for performance.
168
-
169
- Wire the function into the policy configuration using the `identifier` option:
170
-
171
- ```json
172
- {
173
- "export": "RateLimitInboundPolicy",
174
- "module": "$import(@zuplo/runtime)",
175
- "options": {
176
- "rateLimitBy": "function",
177
- "requestsAllowed": 50,
178
- "timeWindowMinutes": 1,
179
- "identifier": {
180
- "export": "rateLimit",
181
- "module": "$import(./modules/rate-limit)"
182
- }
183
- }
184
- }
185
- ```
186
-
187
- :::note
188
-
189
- The `requestsAllowed` and `timeWindowMinutes` values in the policy configuration
190
- serve as defaults. The custom function can override them per request.
191
-
192
- :::
193
-
194
- ## Combining rate limiting with authentication
195
-
196
- Rate limiting works best when combined with authentication so that limits apply
197
- per consumer rather than per IP. A typical policy pipeline is:
198
-
199
- 1. **Authentication** (e.g., API Key Authentication) -- validates credentials
200
- and populates `request.user`
201
- 2. **Rate Limiting** with `rateLimitBy: "user"` -- enforces per-consumer limits
202
- using `request.user.sub`
203
-
204
- With API key authentication, the consumer's metadata (stored when creating the
205
- key) is available at `request.user.data`. A custom rate limit function can read
206
- fields like `customerType` or `plan` from the metadata to apply tiered limits.
207
-
208
- ## Rate limiting and monetization
209
-
210
- If you use Zuplo's
211
- [Monetization](../articles/monetization/monetization-policy.md) feature, the
212
- monetization policy handles quota enforcement based on subscription plans. You
213
- can still add a rate limiting policy after the monetization policy to provide
214
- per-second or per-minute spike protection on top of monthly billing quotas.
215
- These serve different purposes:
216
-
217
- - **Monetization quotas** enforce monthly or billing-period usage limits tied to
218
- a subscription plan
219
- - **Rate limiting** protects against short-duration traffic spikes that could
220
- overwhelm your backend
221
-
222
- ## Combining multiple rate limit policies
223
-
224
- You can apply multiple rate limiting policies to the same route. For example,
225
- you might enforce both a per-minute and a per-hour limit. When using multiple
226
- policies, apply the longest time window first, followed by shorter durations.
227
-
228
- ## Additional options
229
-
230
- Both rate limiting policies support the following additional options:
231
-
232
- - `headerMode` - Set to `"retry-after"` (default) to include the `retry-after`
233
- header in 429 responses, or `"none"` to omit it
234
- - `mode` - Set to `"strict"` (default) for synchronous enforcement, or `"async"`
235
- for non-blocking checks that may allow some requests over the limit
236
- - `throwOnFailure` - Set to `true` to return an error if the rate limit service
237
- is unreachable, or `false` (default) to allow the request through
238
-
239
- ## Related resources
240
-
241
- - [Rate Limiting policy reference](../policies/rate-limit-inbound.mdx)
242
- - [Complex Rate Limiting policy reference](../policies/complex-rate-limit-inbound.mdx)
243
- - [Dynamic Rate Limiting tutorial](../articles/step-5-dynamic-rate-limiting.mdx)
244
- - [Per-user rate limiting with a database](../articles/per-user-rate-limits-using-db.mdx)
245
- - [Quota policy](../policies/quota-inbound.mdx)
246
- - [Monetization policy](../articles/monetization/monetization-policy.md)
@@ -1,41 +0,0 @@
1
- ---
2
- title: GET/HEAD Body Error (GET_HEAD_BODY_ERROR)
3
- ---
4
-
5
- A GET or HEAD request included a body, which is not allowed. The
6
- [Fetch specification](https://fetch.spec.whatwg.org/) defines that GET and HEAD
7
- requests must not have a request body.
8
-
9
- ## Why this happens
10
-
11
- The HTTP specification states that GET and HEAD requests are intended for
12
- retrieving resources and should not include a request body. While some HTTP
13
- clients allow sending a body with GET requests, the Zuplo runtime enforces the
14
- specification and rejects these requests.
15
-
16
- ## How to fix
17
-
18
- - **Use a different HTTP method** - If the request needs to send data in the
19
- body, use `POST`, `PUT`, or `PATCH` instead of `GET` or `HEAD`.
20
- - **Move data to query parameters** - If the request must remain a `GET`,
21
- convert the body data to URL query parameters.
22
- - **Remove the body** - If the body was included unintentionally, remove it from
23
- the request.
24
-
25
- ## Common causes
26
-
27
- - **HTTP client defaults** - Some HTTP client libraries or frameworks
28
- automatically attach a body to requests, even for GET methods. Check the
29
- client configuration.
30
- - **Copied request configuration** - A request configuration copied from a POST
31
- endpoint may still include a body when adapted for a GET endpoint.
32
- - **Framework behavior** - Certain frontend frameworks or API testing tools may
33
- silently include an empty body or content-type header on GET requests.
34
-
35
- :::note
36
-
37
- This is a client-side issue. Update the request on the calling side to remove
38
- the body or change the HTTP method. No changes are needed on the Zuplo gateway
39
- configuration.
40
-
41
- :::