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.
- package/docs/ai-gateway/getting-started.mdx +14 -9
- package/docs/ai-gateway/integrations/ai-sdk.mdx +17 -0
- package/docs/ai-gateway/introduction.mdx +12 -10
- package/docs/ai-gateway/providers.mdx +2 -0
- package/docs/analytics/access-and-entitlements.md +71 -0
- package/docs/analytics/overview.md +63 -0
- package/docs/analytics/reference/metrics-glossary.md +105 -0
- package/docs/analytics/reference/url-parameters.md +66 -0
- package/docs/analytics/shared-controls.md +121 -0
- package/docs/analytics/tabs/agents.md +88 -0
- package/docs/analytics/tabs/consumers.md +73 -0
- package/docs/analytics/tabs/graphql.md +77 -0
- package/docs/analytics/tabs/mcp.md +80 -0
- package/docs/analytics/tabs/origins.md +82 -0
- package/docs/analytics/tabs/requests.md +96 -0
- package/docs/articles/api-key-buckets.mdx +4 -2
- package/docs/articles/archiving-requests-to-storage.mdx +4 -4
- package/docs/articles/branch-based-deployments.mdx +10 -8
- package/docs/articles/ci-cd-github/basic-deployment.mdx +10 -1
- package/docs/articles/ci-cd-github/cleanup-on-branch-delete.mdx +52 -31
- package/docs/articles/ci-cd-github/deploy-and-test.mdx +14 -1
- package/docs/articles/ci-cd-github/local-testing.mdx +3 -1
- package/docs/articles/ci-cd-github/pr-preview-environments.mdx +53 -10
- package/docs/articles/custom-ci-cd-azure.mdx +1 -1
- package/docs/articles/custom-ci-cd-bitbucket.mdx +1 -1
- package/docs/articles/custom-ci-cd-circleci.mdx +1 -1
- package/docs/articles/custom-ci-cd-github.mdx +12 -3
- package/docs/articles/custom-ci-cd-gitlab.mdx +1 -1
- package/docs/articles/graphql.mdx +276 -0
- package/docs/articles/monetization/api-access.mdx +184 -0
- package/docs/articles/monetization/meters.mdx +4 -4
- package/docs/articles/monetization/monetization-policy.md +4 -1
- package/docs/articles/monetization/private-plans.md +3 -4
- package/docs/articles/monetization/stripe-integration.md +9 -0
- package/docs/articles/monetization/subscription-lifecycle.md +12 -11
- package/docs/articles/monorepo-deployment.mdx +37 -5
- package/docs/articles/opentelemetry.mdx +5 -2
- package/docs/articles/securing-the-gateway-with-client-mtls.mdx +68 -43
- package/docs/articles/step-1-setup-basic-gateway.mdx +1 -3
- package/docs/articles/step-2-add-rate-limiting.mdx +1 -1
- package/docs/articles/testing.mdx +1 -1
- package/docs/articles/troubleshooting.md +7 -3
- package/docs/articles/waf-ddos-akamai.md +35 -16
- package/docs/articles/waf-ddos-aws-waf-shield.mdx +35 -16
- package/docs/articles/waf-ddos-fastly.mdx +10 -7
- package/docs/cli/deploy.mdx +44 -9
- package/docs/cli/deploy.partial.mdx +44 -9
- package/docs/concepts/api-keys.md +2 -2
- package/docs/dev-portal/zudoku/components/callout.mdx +11 -18
- package/docs/dev-portal/zudoku/components/sidecar-box.mdx +131 -0
- package/docs/dev-portal/zudoku/configuration/api-catalog.md +62 -42
- package/docs/dev-portal/zudoku/configuration/api-reference.md +5 -4
- package/docs/dev-portal/zudoku/configuration/navigation.mdx +70 -7
- package/docs/dev-portal/zudoku/configuration/search.md +36 -0
- package/docs/dev-portal/zudoku/configuration/site.md +38 -0
- package/docs/dev-portal/zudoku/customization/colors-theme.mdx +51 -40
- package/docs/errors/rate-limit-exceeded.mdx +30 -3
- package/docs/guides/canary-routing-for-employees.mdx +103 -39
- package/docs/guides/modify-openapi-paths.mdx +3 -3
- package/docs/handlers/legacy-dev-portal-handler.mdx +1 -1
- package/docs/handlers/mcp-server.mdx +13 -11
- package/docs/handlers/url-forward.mdx +5 -1
- package/docs/handlers/url-rewrite.mdx +7 -2
- package/docs/handlers/websocket-handler.mdx +5 -1
- package/docs/mcp-gateway/observability/logging.mdx +19 -12
- package/docs/mcp-server/resources.mdx +27 -15
- package/docs/mcp-server/testing.mdx +0 -2
- package/docs/policies/_index.md +2 -0
- package/docs/policies/archive-request-azure-storage-inbound/doc.md +1 -1
- package/docs/policies/archive-response-azure-storage-outbound/doc.md +1 -1
- package/docs/policies/data-loss-prevention-inbound/doc.md +116 -0
- package/docs/policies/data-loss-prevention-inbound/intro.md +15 -0
- package/docs/policies/data-loss-prevention-inbound/schema.json +220 -0
- package/docs/policies/data-loss-prevention-outbound/doc.md +116 -0
- package/docs/policies/data-loss-prevention-outbound/intro.md +18 -0
- package/docs/policies/data-loss-prevention-outbound/schema.json +220 -0
- package/docs/policies/ip-restriction-inbound/policy.ts +1 -1
- package/docs/programmable-api/background-dispatcher.mdx +6 -8
- package/docs/programmable-api/http-problems.mdx +0 -18
- package/docs/programmable-api/jwt-service-plugin.mdx +131 -109
- package/docs/programmable-api/runtime-behaviors.mdx +4 -2
- package/docs/programmable-api/streaming-zone-cache.mdx +4 -6
- package/docs/programmable-api/web-crypto-apis.mdx +10 -6
- package/docs/programmable-api/zone-cache.mdx +1 -1
- package/docs/rate-limiting/combining-policies.mdx +293 -0
- package/docs/rate-limiting/dynamic-rate-limiting.mdx +240 -0
- package/docs/rate-limiting/getting-started.mdx +339 -0
- package/docs/rate-limiting/how-it-works.md +225 -0
- package/docs/rate-limiting/monitoring-and-troubleshooting.mdx +243 -0
- package/docs/{articles → rate-limiting}/per-user-rate-limits-using-db.mdx +39 -28
- package/package.json +4 -4
- package/docs/concepts/rate-limiting.md +0 -246
- 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
|
|
3
|
-
sidebar_label: "Per-
|
|
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
|
-
|
|
13
|
-
[dynamic rate limiting](
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
19
|
-
could use your own API, [Xata](https://xata.io),
|
|
20
|
-
[Firebase](https://firebase.com)
|
|
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
|
|
26
|
-
|
|
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
|
|
29
|
-
example
|
|
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
|
-
//
|
|
52
|
-
// This might be from a JWT or API Key metadata
|
|
53
|
-
|
|
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
|
-
"
|
|
101
|
-
"
|
|
102
|
-
"
|
|
103
|
-
"
|
|
104
|
-
"
|
|
105
|
-
"
|
|
106
|
-
|
|
107
|
-
"
|
|
108
|
-
"
|
|
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.
|
|
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.
|
|
23
|
-
"@zuplo/core": "6.70.
|
|
24
|
-
"@zuplo/runtime": "6.70.
|
|
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
|
-
:::
|