Skip to content
Merged
5 changes: 4 additions & 1 deletion apps/docs/docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,10 @@
},
{
"group": "User Errors",
"pages": ["errors/user/bad_request/permissions_query_syntax_error"]
"pages": [
"errors/user/bad_request/permissions_query_syntax_error",
"errors/user/bad_request/request_body_too_large"
]
}
]
}
Expand Down
87 changes: 87 additions & 0 deletions apps/docs/errors/user/bad_request/request_body_too_large.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
---
title: "request_body_too_large"
description: "Request body exceeds the maximum allowed size limit"
---

<Danger>`err:user:bad_request:request_body_too_large`</Danger>

```json Example
{
"meta": {
"requestId": "req_4dgzrNP3Je5mU1tD"
},
"error": {
"detail": "The request body exceeds the maximum allowed size of 100 bytes.",
"status": 413,
"title": "Request Entity Too Large",
"type": "https://unkey.com/docs/errors/user/bad_request/request_body_too_large",
"errors": []
}
}
```

## What Happened?

Your request was too big! We limit how much data you can send in a single API request to keep everything running smoothly.

This usually happens when you're trying to send a lot of data at once - like huge metadata objects or really long strings in your request.

## How to Fix It

### 1. Trim Down Your Request

The most common cause is putting too much data in the `meta` field or other parts of your request.

<CodeGroup>

```bash Too Big
curl -X POST https://api.unkey.com/v2/keys.create \
-H "Content-Type: application/json" \
-H "Authorization: Bearer unkey_XXXX" \
-d '{
"apiId": "api_123",
"name": "My Key",
"meta": {
"userProfile": "... really long user profile data ...",
"settings": { /* huge nested object with tons of properties */ }
}
}'
```

```bash Just Right
curl -X POST https://api.unkey.com/v2/keys.create \
-H "Content-Type: application/json" \
-H "Authorization: Bearer unkey_XXXX" \
-d '{
"apiId": "api_123",
"name": "My Key",
"meta": {
"userId": "user_123",
"tier": "premium"
}
}'
```

</CodeGroup>

### 2. Store Big Data Elsewhere

Instead of cramming everything into your API request:

- Store large data in your own database
- Only send IDs or references to Unkey
- Fetch the full data when you need it

## Need a Higher Limit?

<Note>
**Got a special use case?** If you have a legitimate need to send larger requests, we'd love to hear about it!

[Contact our support team](mailto:support@unkey.com) and include:
- What you're building
- Why you need to send large requests
- An example of the data you're trying to send

We'll work with you to find a solution that works for your use case.
</Note>
```
5 changes: 5 additions & 0 deletions go/apps/api/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ type Config struct {

// ChproxyToken is the authentication token for ClickHouse proxy endpoints
ChproxyToken string

// MaxRequestBodySize sets the maximum allowed request body size in bytes.
// If 0 or negative, no limit is enforced. Default is 0 (no limit).
// This helps prevent DoS attacks from excessively large request bodies.
MaxRequestBodySize int64
}

func (c Config) Validate() error {
Expand Down
2 changes: 2 additions & 0 deletions go/apps/api/routes/chproxy_metrics/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ func (h *Handler) Path() string {

// Handle processes the HTTP request
func (h *Handler) Handle(ctx context.Context, s *zen.Session) error {
s.DisableClickHouseLogging()

// Authenticate using Bearer token
token, err := zen.Bearer(s)
if err != nil {
Expand Down
2 changes: 2 additions & 0 deletions go/apps/api/routes/chproxy_ratelimits/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ func (h *Handler) Path() string {

// Handle processes the HTTP request
func (h *Handler) Handle(ctx context.Context, s *zen.Session) error {
s.DisableClickHouseLogging()

// Authenticate using Bearer token
token, err := zen.Bearer(s)
if err != nil {
Expand Down
2 changes: 2 additions & 0 deletions go/apps/api/routes/chproxy_verifications/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ func (h *Handler) Path() string {

// Handle processes the HTTP request
func (h *Handler) Handle(ctx context.Context, s *zen.Session) error {
s.DisableClickHouseLogging()

// Authenticate using Bearer token
token, err := zen.Bearer(s)
if err != nil {
Expand Down
4 changes: 3 additions & 1 deletion go/apps/api/routes/openapi/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ func (h *Handler) Path() string {

// Handle processes the HTTP request
func (h *Handler) Handle(ctx context.Context, s *zen.Session) error {
s.AddHeader("Content-Type", "text/html")
s.DisableClickHouseLogging()

s.AddHeader("Content-Type", "application/yaml")
return s.Send(200, openapi.Spec)
}
2 changes: 2 additions & 0 deletions go/apps/api/routes/reference/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ func (h *Handler) Path() string {

// Handle processes the HTTP request
func (h *Handler) Handle(ctx context.Context, s *zen.Session) error {
s.DisableClickHouseLogging()

html := fmt.Sprintf(`
<!doctype html>
<html>
Expand Down
7 changes: 4 additions & 3 deletions go/apps/api/routes/v2_liveness/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,14 @@ func (h *Handler) Path() string {

// Handle processes the HTTP request
func (h *Handler) Handle(ctx context.Context, s *zen.Session) error {
res := Response{
s.DisableClickHouseLogging()

return s.JSON(http.StatusOK, Response{
Meta: openapi.Meta{
RequestId: s.RequestID(),
},
Data: openapi.V2LivenessResponseData{
Message: "we're cooking",
},
}
return s.JSON(http.StatusOK, res)
})
}
7 changes: 6 additions & 1 deletion go/apps/api/routes/v2_ratelimit_limit/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ func (h *Handler) Path() string {

// Handle processes the HTTP request
func (h *Handler) Handle(ctx context.Context, s *zen.Session) error {
if s.Request().Header.Get("X-Unkey-Metrics") == "disabled" {
s.DisableClickHouseLogging()
}

// Authenticate the request with a root key
auth, emit, err := h.Keys.GetRootKey(ctx, s)
defer emit()
Expand Down Expand Up @@ -206,7 +210,7 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error {
)
}

if s.Request().Header.Get("X-Unkey-Metrics") != "disabled" {
if s.ShouldLogRequestToClickHouse() {
h.ClickHouse.BufferRatelimit(schema.RatelimitRequestV1{
RequestID: s.RequestID(),
WorkspaceID: auth.AuthorizedWorkspaceID,
Expand All @@ -216,6 +220,7 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error {
Passed: result.Success,
})
}

res := Response{
Meta: openapi.Meta{
RequestId: s.RequestID(),
Expand Down
3 changes: 2 additions & 1 deletion go/apps/api/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,8 @@ func Run(ctx context.Context, cfg Config) error {
Flags: &zen.Flags{
TestMode: cfg.TestMode,
},
TLS: cfg.TLSConfig,
TLS: cfg.TLSConfig,
MaxRequestBodySize: cfg.MaxRequestBodySize,
})
if err != nil {
return fmt.Errorf("unable to create server: %w", err)
Expand Down
7 changes: 7 additions & 0 deletions go/cmd/api/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ var Cmd = &cli.Command{
"Authentication token for ClickHouse proxy endpoints. Required when proxy is enabled.",
cli.EnvVar("UNKEY_CHPROXY_AUTH_TOKEN"),
),

// Request Body Configuration
cli.Int64("max-request-body-size", "Maximum allowed request body size in bytes. Set to 0 or negative to disable limit. Default: 10485760 (10MB)",
cli.Default(int64(10485760)), cli.EnvVar("UNKEY_MAX_REQUEST_BODY_SIZE")),
},

Action: action,
Expand Down Expand Up @@ -146,6 +150,9 @@ func action(ctx context.Context, cmd *cli.Command) error {

// ClickHouse proxy configuration
ChproxyToken: cmd.String("chproxy-auth-token"),

// Request body configuration
MaxRequestBodySize: cmd.Int64("max-request-body-size"),
}

err := config.Validate()
Expand Down
1 change: 0 additions & 1 deletion go/internal/services/usagelimiter/limit.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ func (s *service) Limit(ctx context.Context, req UsageRequest) (UsageResponse, e
}

metrics.UsagelimiterDecisions.WithLabelValues("db", "allowed").Inc()
metrics.UsagelimiterCreditsProcessed.Add(float64(req.Cost))
return UsageResponse{Valid: true, Remaining: max(0, remaining-req.Cost)}, nil
}

Expand Down
4 changes: 0 additions & 4 deletions go/internal/services/usagelimiter/redis.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,6 @@ func (s *counterService) Limit(ctx context.Context, req UsageRequest) (UsageResp
// Attempt decrement if key already exists in Redis
remaining, exists, success, err := s.counter.DecrementIfExists(ctx, redisKey, int64(req.Cost))
if err != nil {
metrics.UsagelimiterFallbackOperations.Inc()
return s.dbFallback.Limit(ctx, req)
}

Expand All @@ -215,7 +214,6 @@ func (s *counterService) handleResult(req UsageRequest, remaining int64, success
})

metrics.UsagelimiterDecisions.WithLabelValues("redis", "allowed").Inc()
metrics.UsagelimiterCreditsProcessed.Add(float64(req.Cost))

return UsageResponse{Valid: true, Remaining: int32(remaining)}, nil
}
Expand Down Expand Up @@ -262,7 +260,6 @@ func (s *counterService) initializeFromDatabase(ctx context.Context, req UsageRe

wasSet, err := s.counter.SetIfNotExists(ctx, redisKey, initValue, s.ttl)
if err != nil {
metrics.UsagelimiterFallbackOperations.Inc()
s.logger.Debug("failed to initialize counter with SetIfNotExists, falling back to DB", "error", err, "keyId", req.KeyId)
return s.dbFallback.Limit(ctx, req)
}
Expand All @@ -281,7 +278,6 @@ func (s *counterService) initializeFromDatabase(ctx context.Context, req UsageRe
// Another node already initialized the key, check if we have enough after decrement
remaining, exists, success, err := s.counter.DecrementIfExists(ctx, redisKey, int64(req.Cost))
if err != nil || !exists {
metrics.UsagelimiterFallbackOperations.Inc()
s.logger.Debug("failed to decrement after initialization attempt", "error", err, "exists", exists, "keyId", req.KeyId)
return s.dbFallback.Limit(ctx, req)
}
Expand Down
29 changes: 29 additions & 0 deletions go/pkg/cli/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,33 @@ func (c *Command) RequireInt(name string) int {
return inf.Value()
}

// Int64 returns the value of an int64 flag by name
// Returns 0 if flag doesn't exist or isn't an Int64Flag
func (c *Command) Int64(name string) int64 {
if flag, ok := c.flagMap[name]; ok {
if i64f, ok := flag.(*Int64Flag); ok {
return i64f.Value()
}
}
return 0
}

// RequireInt64 returns the value of an int64 flag by name
// Panics if flag doesn't exist or isn't an Int64Flag
func (c *Command) RequireInt64(name string) int64 {
flag, ok := c.flagMap[name]
if !ok {
panic(c.newFlagNotFoundError(name))
}

i64f, ok := flag.(*Int64Flag)
if !ok {
panic(c.newWrongFlagTypeError(name, flag, "Int64Flag"))
}

return i64f.Value()
}

// Float returns the value of a float flag by name
// Returns 0.0 if flag doesn't exist or isn't a FloatFlag
func (c *Command) Float(name string) float64 {
Expand Down Expand Up @@ -220,6 +247,8 @@ func (c *Command) getFlagType(flag Flag) string {
return "BoolFlag"
case *IntFlag:
return "IntFlag"
case *Int64Flag:
return "Int64Flag"
case *FloatFlag:
return "FloatFlag"
case *StringSliceFlag:
Expand Down
Loading