diff --git a/apps/docs/errors/unkey/data/ratelimit_namespace_gone.mdx b/apps/docs/errors/unkey/data/ratelimit_namespace_gone.mdx
new file mode 100644
index 0000000000..628b0b904a
--- /dev/null
+++ b/apps/docs/errors/unkey/data/ratelimit_namespace_gone.mdx
@@ -0,0 +1,36 @@
+---
+title: Ratelimit Namespace Gone
+---
+
+## Description
+
+This error occurs when you attempt to use a ratelimit namespace that has been deleted. Once a namespace is deleted, it cannot be restored through the API or dashboard.
+
+## Error Code
+
+`unkey/data/ratelimit_namespace_gone`
+
+## HTTP Status Code
+
+`410 Gone`
+
+## Cause
+
+The ratelimit namespace you're trying to access was previously deleted and is no longer available through the API or dashboard.
+
+## Resolution
+
+
+Contact [support@unkey.dev](mailto:support@unkey.dev) with your workspace ID and namespace name if you need this namespace restored.
+
+
+## Prevention
+
+To avoid accidentally deleting namespaces:
+- Restrict namespace deletion via workspace permissions
+- Carefully review namespace-deletion requests before confirming
+
+## Related
+
+- [Ratelimit Namespace Not Found](/errors/unkey/data/ratelimit_namespace_not_found)
+- [Rate Limiting Documentation](/ratelimiting/introduction)
\ No newline at end of file
diff --git a/apps/docs/errors/user/bad_request/request_body_too_large.mdx b/apps/docs/errors/user/bad_request/request_body_too_large.mdx
index 10d19a0b4c..decf08c6b2 100644
--- a/apps/docs/errors/user/bad_request/request_body_too_large.mdx
+++ b/apps/docs/errors/user/bad_request/request_body_too_large.mdx
@@ -77,7 +77,7 @@ Instead of cramming everything into your API request:
**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:
+[Contact our support team](mailto:support@unkey.dev) and include:
- What you're building
- Why you need to send large requests
- An example of the data you're trying to send
diff --git a/go/apps/api/openapi/gen.go b/go/apps/api/openapi/gen.go
index 554a8352af..2390dff60e 100644
--- a/go/apps/api/openapi/gen.go
+++ b/go/apps/api/openapi/gen.go
@@ -137,6 +137,20 @@ type ForbiddenErrorResponse struct {
Meta Meta `json:"meta"`
}
+// GoneErrorResponse Error response when the requested resource has been soft-deleted and is no longer available. This occurs when:
+// - The resource has been marked as deleted but still exists in the database
+// - The resource is intentionally unavailable but could potentially be restored
+// - The resource cannot be restored through the API or dashboard
+//
+// To resolve this error, contact support if you need the resource restored.
+type GoneErrorResponse struct {
+ // Error Base error structure following Problem Details for HTTP APIs (RFC 7807). This provides a standardized way to carry machine-readable details of errors in HTTP response content.
+ Error BaseError `json:"error"`
+
+ // Meta Metadata object included in every API response. This provides context about the request and is essential for debugging, audit trails, and support inquiries. The `requestId` is particularly important when troubleshooting issues with the Unkey support team.
+ Meta Meta `json:"meta"`
+}
+
// Identity defines model for Identity.
type Identity struct {
// ExternalId External identity ID
diff --git a/go/apps/api/openapi/openapi-generated.yaml b/go/apps/api/openapi/openapi-generated.yaml
index 15744238e5..577f0764a1 100644
--- a/go/apps/api/openapi/openapi-generated.yaml
+++ b/go/apps/api/openapi/openapi-generated.yaml
@@ -1825,6 +1825,23 @@ components:
"$ref": "#/components/schemas/Meta"
data:
"$ref": "#/components/schemas/V2RatelimitLimitResponseData"
+ GoneErrorResponse:
+ type: object
+ required:
+ - meta
+ - error
+ properties:
+ meta:
+ $ref: "#/components/schemas/Meta"
+ error:
+ $ref: "#/components/schemas/BaseError"
+ description: |-
+ Error response when the requested resource has been soft-deleted and is no longer available. This occurs when:
+ - The resource has been marked as deleted but still exists in the database
+ - The resource is intentionally unavailable but could potentially be restored
+ - The resource cannot be restored through the API or dashboard
+
+ To resolve this error, contact support if you need the resource restored.
V2RatelimitListOverridesRequestBody:
additionalProperties: false
properties:
@@ -5670,7 +5687,7 @@ paths:
Use this for rate limiting beyond API keys - limit users by ID, IPs by address, or any custom identifier. Supports namespace organization, variable costs, and custom overrides.
- **Important**: Always returns HTTP 200. Check the `success` field to determine if the request should proceed.
+ **Response Codes**: Successful ratelimit checks return HTTP 200. 4xx responses indicate auth, namespace existence/deletion, or validation errors (e.g., 410 Gone for deleted namespaces). 5xx responses indicate server errors.
**Required Permissions**
@@ -5750,7 +5767,7 @@ paths:
schema:
$ref: '#/components/schemas/V2RatelimitLimitResponseBody'
description: |
- Rate limit check completed. Always returns HTTP 200 - check the `success` field to determine if the request is allowed.
+ Rate limit check completed successfully. Check the `success` field to determine if the request is allowed.
"400":
content:
application/json:
@@ -5775,6 +5792,12 @@ paths:
schema:
$ref: '#/components/schemas/NotFoundErrorResponse'
description: Not Found
+ "410":
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/GoneErrorResponse'
+ description: Gone - Namespace has been deleted
"500":
content:
application/json:
diff --git a/go/apps/api/openapi/spec/error/GoneErrorResponse.yaml b/go/apps/api/openapi/spec/error/GoneErrorResponse.yaml
new file mode 100644
index 0000000000..a65a80d211
--- /dev/null
+++ b/go/apps/api/openapi/spec/error/GoneErrorResponse.yaml
@@ -0,0 +1,16 @@
+type: object
+required:
+ - meta
+ - error
+properties:
+ meta:
+ $ref: "../common/Meta.yaml"
+ error:
+ $ref: "./BaseError.yaml"
+description: |-
+ Error response when the requested resource has been soft-deleted and is no longer available. This occurs when:
+ - The resource has been marked as deleted but still exists in the database
+ - The resource is intentionally unavailable but could potentially be restored
+ - The resource cannot be restored through the API or dashboard
+
+ To resolve this error, contact support if you need the resource restored.
diff --git a/go/apps/api/openapi/spec/paths/v2/ratelimit/limit/index.yaml b/go/apps/api/openapi/spec/paths/v2/ratelimit/limit/index.yaml
index 504e4a0870..cffa616826 100644
--- a/go/apps/api/openapi/spec/paths/v2/ratelimit/limit/index.yaml
+++ b/go/apps/api/openapi/spec/paths/v2/ratelimit/limit/index.yaml
@@ -7,7 +7,7 @@ post:
Use this for rate limiting beyond API keys - limit users by ID, IPs by address, or any custom identifier. Supports namespace organization, variable costs, and custom overrides.
- **Important**: Always returns HTTP 200. Check the `success` field to determine if the request should proceed.
+ **Response Codes**: Rate limit checks return HTTP 200 regardless of whether the limit is exceeded - check the `success` field in the response to determine if the request should be allowed. 4xx responses indicate auth, namespace existence/deletion, or validation errors (e.g., 410 Gone for deleted namespaces). 5xx responses indicate server errors.
**Required Permissions**
@@ -90,7 +90,7 @@ post:
success: true
overrideId: ovr_2cGKbMxRyIzhCxo1Idjz8q
description: |
- Rate limit check completed. Always returns HTTP 200 - check the `success` field to determine if the request is allowed.
+ Rate limit check completed successfully. Check the `success` field to determine if the request is allowed.
"400":
description: Bad request
content:
@@ -115,6 +115,12 @@ post:
application/json:
schema:
"$ref": "../../../../error/NotFoundErrorResponse.yaml"
+ "410":
+ description: Gone - Namespace has been deleted
+ content:
+ application/json:
+ schema:
+ "$ref": "../../../../error/GoneErrorResponse.yaml"
"500":
description: Internal server error
content:
diff --git a/go/apps/api/routes/register.go b/go/apps/api/routes/register.go
index 7bd08e116b..5e3ac1a17a 100644
--- a/go/apps/api/routes/register.go
+++ b/go/apps/api/routes/register.go
@@ -122,6 +122,7 @@ func Register(srv *zen.Server, svc *Services) {
Ratelimit: svc.Ratelimit,
RatelimitNamespaceCache: svc.Caches.RatelimitNamespace,
TestMode: srv.Flags().TestMode,
+ Auditlogs: svc.Auditlogs,
},
)
diff --git a/go/apps/api/routes/v2_ratelimit_delete_override/handler.go b/go/apps/api/routes/v2_ratelimit_delete_override/handler.go
index e507543d35..85595e6810 100644
--- a/go/apps/api/routes/v2_ratelimit_delete_override/handler.go
+++ b/go/apps/api/routes/v2_ratelimit_delete_override/handler.go
@@ -3,12 +3,15 @@ package handler
import (
"context"
"database/sql"
+ "encoding/json"
"fmt"
"net/http"
+ "strings"
"time"
"github.com/unkeyed/unkey/go/apps/api/openapi"
"github.com/unkeyed/unkey/go/internal/services/auditlogs"
+ "github.com/unkeyed/unkey/go/internal/services/caches"
"github.com/unkeyed/unkey/go/internal/services/keys"
"github.com/unkeyed/unkey/go/pkg/auditlog"
"github.com/unkeyed/unkey/go/pkg/cache"
@@ -23,7 +26,6 @@ import (
type Request = openapi.V2RatelimitDeleteOverrideRequestBody
type Response = openapi.V2RatelimitDeleteOverrideResponseBody
-// Handler implements zen.Route interface for the v2 ratelimit delete override endpoint
type Handler struct {
Logger logging.Logger
DB db.Database
@@ -32,17 +34,14 @@ type Handler struct {
RatelimitNamespaceCache cache.Cache[cache.ScopedKey, db.FindRatelimitNamespace]
}
-// Method returns the HTTP method this route responds to
func (h *Handler) Method() string {
return "POST"
}
-// Path returns the URL path pattern this route matches
func (h *Handler) Path() string {
return "/v2/ratelimit.deleteOverride"
}
-// Handle processes the HTTP request
func (h *Handler) Handle(ctx context.Context, s *zen.Session) error {
auth, emit, err := h.Keys.GetRootKey(ctx, s)
defer emit()
@@ -55,68 +54,113 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error {
return err
}
- err = db.Tx(ctx, h.DB.RW(), func(ctx context.Context, tx db.DBTX) error {
- namespace, err := db.Query.FindRatelimitNamespace(ctx, tx, db.FindRatelimitNamespaceParams{
- WorkspaceID: auth.AuthorizedWorkspaceID,
- Namespace: req.Namespace,
- })
- if err != nil {
- if db.IsNotFound(err) {
- return fault.New("namespace not found",
- fault.Code(codes.Data.RatelimitNamespace.NotFound.URN()),
- fault.Internal("namespace not found"),
- fault.Public("This namespace does not exist."),
- )
+ namespace, hit, err := h.RatelimitNamespaceCache.SWR(
+ ctx,
+ cache.ScopedKey{WorkspaceID: auth.AuthorizedWorkspaceID, Key: req.Namespace},
+ func(ctx context.Context) (db.FindRatelimitNamespace, error) {
+ result := db.FindRatelimitNamespace{} // nolint:exhaustruct
+ response, err := db.WithRetry(func() (db.FindRatelimitNamespaceRow, error) {
+ return db.Query.FindRatelimitNamespace(ctx, h.DB.RO(), db.FindRatelimitNamespaceParams{
+ WorkspaceID: auth.AuthorizedWorkspaceID,
+ Namespace: req.Namespace,
+ })
+ })
+ if err != nil {
+ return result, err
}
- return err
- }
- if namespace.DeletedAtM.Valid {
- return fault.New("namespace was deleted",
+ result = db.FindRatelimitNamespace{
+ ID: response.ID,
+ WorkspaceID: response.WorkspaceID,
+ Name: response.Name,
+ CreatedAtM: response.CreatedAtM,
+ UpdatedAtM: response.UpdatedAtM,
+ DeletedAtM: response.DeletedAtM,
+ DirectOverrides: make(map[string]db.FindRatelimitNamespaceLimitOverride),
+ WildcardOverrides: make([]db.FindRatelimitNamespaceLimitOverride, 0),
+ }
+
+ overrides := make([]db.FindRatelimitNamespaceLimitOverride, 0)
+ if overrideBytes, ok := response.Overrides.([]byte); ok && overrideBytes != nil {
+ err = json.Unmarshal(overrideBytes, &overrides)
+ if err != nil {
+ return result, err
+ }
+ }
+
+ for _, override := range overrides {
+ result.DirectOverrides[override.Identifier] = override
+ if strings.Contains(override.Identifier, "*") {
+ result.WildcardOverrides = append(result.WildcardOverrides, override)
+ }
+ }
+
+ return result, nil
+ },
+ caches.DefaultFindFirstOp,
+ )
+
+ if err != nil {
+ if db.IsNotFound(err) {
+ return fault.New("namespace not found",
fault.Code(codes.Data.RatelimitNamespace.NotFound.URN()),
+ fault.Internal("namespace not found"),
fault.Public("This namespace does not exist."),
)
}
- err = auth.VerifyRootKey(ctx, keys.WithPermissions(rbac.Or(
- rbac.T(rbac.Tuple{
- ResourceType: rbac.Ratelimit,
- ResourceID: namespace.ID,
- Action: rbac.DeleteOverride,
- }),
- rbac.T(rbac.Tuple{
- ResourceType: rbac.Ratelimit,
- ResourceID: "*",
- Action: rbac.DeleteOverride,
- }),
- )))
- if err != nil {
- return err
- }
- // Check if the override exists before deleting
- override, overrideErr := db.Query.FindRatelimitOverrideByIdentifier(ctx, tx, db.FindRatelimitOverrideByIdentifierParams{
- WorkspaceID: auth.AuthorizedWorkspaceID,
- NamespaceID: namespace.ID,
- Identifier: req.Identifier,
- })
+ return fault.Wrap(err,
+ fault.Code(codes.App.Internal.UnexpectedError.URN()),
+ fault.Public("An unexpected error occurred while fetching the namespace."),
+ )
+ }
- if db.IsNotFound(overrideErr) {
- return fault.New("override not found",
- fault.Code(codes.Data.RatelimitOverride.NotFound.URN()),
- fault.Internal("override not found"),
- fault.Public("This override does not exist."),
- )
- }
- if overrideErr != nil {
- return overrideErr
- }
+ if namespace.DeletedAtM.Valid {
+ return fault.New("namespace deleted",
+ fault.Code(codes.Data.RatelimitNamespace.NotFound.URN()),
+ fault.Internal("namespace deleted"),
+ fault.Public("This namespace does not exist."),
+ )
+ }
- // Perform soft delete by updating the DeletedAt field
+ if hit == cache.Null {
+ return fault.New("namespace not found",
+ fault.Code(codes.Data.RatelimitNamespace.NotFound.URN()),
+ fault.Internal("namespace not found"),
+ fault.Public("This namespace does not exist."),
+ )
+ }
+
+ err = auth.VerifyRootKey(ctx, keys.WithPermissions(rbac.Or(
+ rbac.T(rbac.Tuple{
+ ResourceType: rbac.Ratelimit,
+ ResourceID: namespace.ID,
+ Action: rbac.DeleteOverride,
+ }),
+ rbac.T(rbac.Tuple{
+ ResourceType: rbac.Ratelimit,
+ ResourceID: "*",
+ Action: rbac.DeleteOverride,
+ }),
+ )))
+ if err != nil {
+ return err
+ }
+
+ override, ok := namespace.DirectOverrides[req.Identifier]
+ if !ok {
+ return fault.New("override not found",
+ fault.Code(codes.Data.RatelimitOverride.NotFound.URN()),
+ fault.Internal("override not found"),
+ fault.Public("This override does not exist."),
+ )
+ }
+
+ err = db.Tx(ctx, h.DB.RW(), func(ctx context.Context, tx db.DBTX) error {
err = db.Query.SoftDeleteRatelimitOverride(ctx, tx, db.SoftDeleteRatelimitOverrideParams{
ID: override.ID,
Now: sql.NullInt64{Valid: true, Int64: time.Now().UnixMilli()},
})
-
if err != nil {
return fault.Wrap(err,
fault.Code(codes.App.Internal.ServiceUnavailable.URN()),
@@ -158,23 +202,24 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error {
return err
}
- h.RatelimitNamespaceCache.Remove(ctx,
- cache.ScopedKey{
- WorkspaceID: auth.AuthorizedWorkspaceID,
- Key: namespace.ID,
- },
- cache.ScopedKey{
- WorkspaceID: auth.AuthorizedWorkspaceID,
- Key: namespace.Name,
- },
- )
-
return nil
})
if err != nil {
return err
}
+ h.RatelimitNamespaceCache.Remove(
+ ctx,
+ cache.ScopedKey{
+ WorkspaceID: auth.AuthorizedWorkspaceID,
+ Key: namespace.ID,
+ },
+ cache.ScopedKey{
+ WorkspaceID: auth.AuthorizedWorkspaceID,
+ Key: namespace.Name,
+ },
+ )
+
return s.JSON(http.StatusOK, Response{
Meta: openapi.Meta{
RequestId: s.RequestID(),
diff --git a/go/apps/api/routes/v2_ratelimit_limit/200_test.go b/go/apps/api/routes/v2_ratelimit_limit/200_test.go
index e97ca836fa..0a03784859 100644
--- a/go/apps/api/routes/v2_ratelimit_limit/200_test.go
+++ b/go/apps/api/routes/v2_ratelimit_limit/200_test.go
@@ -10,6 +10,7 @@ import (
"github.com/stretchr/testify/require"
handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_ratelimit_limit"
+ "github.com/unkeyed/unkey/go/pkg/auditlog"
"github.com/unkeyed/unkey/go/pkg/clickhouse"
"github.com/unkeyed/unkey/go/pkg/clickhouse/schema"
"github.com/unkeyed/unkey/go/pkg/db"
@@ -28,10 +29,58 @@ func TestLimitSuccessfully(t *testing.T) {
ClickHouse: h.ClickHouse,
Ratelimit: h.Ratelimit,
RatelimitNamespaceCache: h.Caches.RatelimitNamespace,
+ Auditlogs: h.Auditlogs,
}
h.Register(route)
+ // Test auto-creation of namespace with audit log
+ t.Run("auto-create namespace with audit log", func(t *testing.T) {
+ // Use a namespace that doesn't exist
+ namespaceName := uid.New("nonexistent")
+ rootKey := h.CreateRootKey(h.Resources().UserWorkspace.ID, "ratelimit.*.create_namespace", "ratelimit.*.limit")
+
+ headers := http.Header{
+ "Content-Type": {"application/json"},
+ "Authorization": {fmt.Sprintf("Bearer %s", rootKey)},
+ }
+ req := handler.Request{
+ Namespace: namespaceName,
+ Identifier: "user_123",
+ Limit: 100,
+ Duration: 60000, // 1 minute in ms
+ }
+
+ // First request should succeed and create the namespace
+ res := testutil.CallRoute[handler.Request, handler.Response](h, route, headers, req)
+ require.Equal(t, 200, res.Status, "expected 200, received: %v", res.Body)
+ require.NotNil(t, res.Body)
+ require.True(t, res.Body.Data.Success, "Rate limit should not be exceeded on first request")
+ require.Equal(t, int64(100), res.Body.Data.Limit)
+ require.Equal(t, int64(99), res.Body.Data.Remaining)
+
+ // Verify namespace was created
+ namespace, err := db.Query.FindRatelimitNamespaceByName(ctx, h.DB.RO(), db.FindRatelimitNamespaceByNameParams{
+ WorkspaceID: h.Resources().UserWorkspace.ID,
+ Name: namespaceName,
+ })
+ require.NoError(t, err)
+ require.Equal(t, namespaceName, namespace.Name)
+ require.False(t, namespace.DeletedAtM.Valid, "Namespace should not be deleted")
+
+ // Verify audit log was created for the namespace
+ auditTargets, err := db.Query.FindAuditLogTargetByID(ctx, h.DB.RO(), namespace.ID)
+ require.NoError(t, err)
+ require.Len(t, auditTargets, 1, "Should have exactly one audit log entry for the namespace")
+
+ auditTarget := auditTargets[0]
+ require.Equal(t, string(auditlog.RatelimitNamespaceResourceType), auditTarget.AuditLogTarget.Type)
+ require.Equal(t, namespace.ID, auditTarget.AuditLogTarget.ID)
+ require.Equal(t, namespaceName, auditTarget.AuditLogTarget.Name.String)
+ require.Equal(t, string(auditlog.RatelimitNamespaceCreateEvent), auditTarget.AuditLog.Event)
+ require.Contains(t, auditTarget.AuditLog.Display, namespaceName, "Audit log should mention the namespace name")
+ })
+
// Test basic rate limiting
t.Run("basic rate limiting", func(t *testing.T) {
namespaceID, namespaceName := createNamespace(t, h)
@@ -102,7 +151,6 @@ func TestLimitSuccessfully(t *testing.T) {
require.Equal(t, req.Identifier, row.Identifier)
require.Equal(t, res.Body.Data.Success, row.Passed)
require.Equal(t, res.Body.Meta.RequestId, row.RequestID)
-
})
// Test with custom cost
diff --git a/go/apps/api/routes/v2_ratelimit_limit/400_test.go b/go/apps/api/routes/v2_ratelimit_limit/400_test.go
index 195961917f..fc786b2b7a 100644
--- a/go/apps/api/routes/v2_ratelimit_limit/400_test.go
+++ b/go/apps/api/routes/v2_ratelimit_limit/400_test.go
@@ -25,6 +25,7 @@ func TestBadRequests(t *testing.T) {
Logger: h.Logger,
Ratelimit: h.Ratelimit,
RatelimitNamespaceCache: h.Caches.RatelimitNamespace,
+ Auditlogs: h.Auditlogs,
}
h.Register(route)
@@ -104,10 +105,12 @@ func TestMissingAuthorizationHeader(t *testing.T) {
h := testutil.NewHarness(t)
route := &handler.Handler{
- DB: h.DB,
- Keys: h.Keys,
- Logger: h.Logger,
- Ratelimit: h.Ratelimit,
+ DB: h.DB,
+ Keys: h.Keys,
+ Logger: h.Logger,
+ Ratelimit: h.Ratelimit,
+ RatelimitNamespaceCache: h.Caches.RatelimitNamespace,
+ Auditlogs: h.Auditlogs,
}
h.Register(route)
diff --git a/go/apps/api/routes/v2_ratelimit_limit/401_test.go b/go/apps/api/routes/v2_ratelimit_limit/401_test.go
index e50ea8ac25..24013805d1 100644
--- a/go/apps/api/routes/v2_ratelimit_limit/401_test.go
+++ b/go/apps/api/routes/v2_ratelimit_limit/401_test.go
@@ -18,6 +18,7 @@ func TestUnauthorizedAccess(t *testing.T) {
Logger: h.Logger,
Ratelimit: h.Ratelimit,
RatelimitNamespaceCache: h.Caches.RatelimitNamespace,
+ Auditlogs: h.Auditlogs,
}
h.Register(route)
diff --git a/go/apps/api/routes/v2_ratelimit_limit/403_test.go b/go/apps/api/routes/v2_ratelimit_limit/403_test.go
index 5386b683ae..d8500d2a5d 100644
--- a/go/apps/api/routes/v2_ratelimit_limit/403_test.go
+++ b/go/apps/api/routes/v2_ratelimit_limit/403_test.go
@@ -36,6 +36,7 @@ func TestWorkspacePermissions(t *testing.T) {
Logger: h.Logger,
Ratelimit: h.Ratelimit,
RatelimitNamespaceCache: h.Caches.RatelimitNamespace,
+ Auditlogs: h.Auditlogs,
}
h.Register(route)
@@ -57,9 +58,60 @@ func TestWorkspacePermissions(t *testing.T) {
Duration: 60000,
}
- res := testutil.CallRoute[handler.Request, openapi.NotFoundErrorResponse](h, route, headers, req)
+ res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse](h, route, headers, req)
- // This should return a 404 Not Found (for security reasons we don't reveal if the namespace exists)
- require.Equal(t, http.StatusNotFound, res.Status, "expected 404, got: %d, body: %s", res.Status, res.RawBody)
+ // This should return a 403 Forbidden - user lacks create_namespace permission
+ require.Equal(t, http.StatusForbidden, res.Status, "expected 403, got: %d, body: %s", res.Status, res.RawBody)
require.NotNil(t, res.Body)
+ require.Contains(t, res.Body.Error.Detail, "create_namespace", "Error should mention missing create_namespace permission")
+}
+
+func TestInsufficientPermissions(t *testing.T) {
+ h := testutil.NewHarness(t)
+
+ route := &handler.Handler{
+ DB: h.DB,
+ Keys: h.Keys,
+ Logger: h.Logger,
+ Ratelimit: h.Ratelimit,
+ RatelimitNamespaceCache: h.Caches.RatelimitNamespace,
+ Auditlogs: h.Auditlogs,
+ }
+
+ h.Register(route)
+
+ t.Run("has limit permission but no create_namespace permission", func(t *testing.T) {
+ // Use a namespace that doesn't exist
+ nonExistentNamespace := uid.New("nonexistent")
+
+ // Create a key that can limit any namespace but cannot create namespaces
+ rootKey := h.CreateRootKey(h.Resources().UserWorkspace.ID, "ratelimit.*.limit")
+
+ headers := http.Header{
+ "Content-Type": {"application/json"},
+ "Authorization": {fmt.Sprintf("Bearer %s", rootKey)},
+ }
+
+ req := handler.Request{
+ Namespace: nonExistentNamespace,
+ Identifier: "user_123",
+ Limit: 100,
+ Duration: 60000,
+ }
+
+ res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse](h, route, headers, req)
+
+ // Should return 403 because user has some permissions but not create_namespace
+ require.Equal(t, http.StatusForbidden, res.Status, "expected 403, got: %d, body: %s", res.Status, res.RawBody)
+ require.NotNil(t, res.Body)
+ require.Contains(t, res.Body.Error.Detail, "create_namespace", "Error should mention missing create_namespace permission")
+
+ // Verify the namespace was NOT created
+ ctx := context.Background()
+ _, err := db.Query.FindRatelimitNamespace(ctx, h.DB.RO(), db.FindRatelimitNamespaceParams{
+ WorkspaceID: h.Resources().UserWorkspace.ID,
+ Namespace: nonExistentNamespace,
+ })
+ require.True(t, db.IsNotFound(err), "Namespace should not have been created when user lacks create_namespace permission")
+ })
}
diff --git a/go/apps/api/routes/v2_ratelimit_limit/404_test.go b/go/apps/api/routes/v2_ratelimit_limit/410_test.go
similarity index 57%
rename from go/apps/api/routes/v2_ratelimit_limit/404_test.go
rename to go/apps/api/routes/v2_ratelimit_limit/410_test.go
index c8314eb136..be1545399b 100644
--- a/go/apps/api/routes/v2_ratelimit_limit/404_test.go
+++ b/go/apps/api/routes/v2_ratelimit_limit/410_test.go
@@ -16,7 +16,7 @@ import (
"github.com/unkeyed/unkey/go/pkg/uid"
)
-func TestNamespaceNotFound(t *testing.T) {
+func TestSoftDeletedNamespace(t *testing.T) {
h := testutil.NewHarness(t)
route := &handler.Handler{
@@ -25,38 +25,14 @@ func TestNamespaceNotFound(t *testing.T) {
Logger: h.Logger,
Ratelimit: h.Ratelimit,
RatelimitNamespaceCache: h.Caches.RatelimitNamespace,
+ Auditlogs: h.Auditlogs,
}
h.Register(route)
- rootKey := h.CreateRootKey(h.Resources().UserWorkspace.ID)
-
- headers := http.Header{
- "Content-Type": {"application/json"},
- "Authorization": {fmt.Sprintf("Bearer %s", rootKey)},
- }
-
- // Test with non-existent namespace
- t.Run("namespace not found", func(t *testing.T) {
- t.Skip()
- nonExistentNamespace := "nonexistent_namespace"
- req := handler.Request{
- Namespace: nonExistentNamespace,
- Identifier: "user_123",
- Limit: 100,
- Duration: 60000,
- }
-
- res := testutil.CallRoute[handler.Request, openapi.NotFoundErrorResponse](h, route, headers, req)
- require.Equal(t, http.StatusNotFound, res.Status)
- require.NotNil(t, res.Body)
- require.Equal(t, "https://unkey.com/docs/errors/not_found", res.Body.Error.Type)
- require.Equal(t, http.StatusNotFound, res.Body.Error.Status)
- })
-
- // Test with deleted namespace
- t.Run("deleted namespace", func(t *testing.T) {
- // Create a namespace and then delete it
+ // Test with soft-deleted namespace - should return 410
+ t.Run("soft-deleted namespace returns 410", func(t *testing.T) {
+ // Create a namespace and then soft-delete it
deletedNamespace := uid.New("test")
ctx := context.Background()
@@ -77,6 +53,14 @@ func TestNamespaceNotFound(t *testing.T) {
})
require.NoError(t, err)
+ // Create root key with permissions to use the namespace
+ rootKeyWithPerms := h.CreateRootKey(h.Resources().UserWorkspace.ID, fmt.Sprintf("ratelimit.%s.limit", namespaceID))
+
+ headersWithPerms := http.Header{
+ "Content-Type": {"application/json"},
+ "Authorization": {fmt.Sprintf("Bearer %s", rootKeyWithPerms)},
+ }
+
req := handler.Request{
Namespace: deletedNamespace,
Identifier: "user_123",
@@ -84,9 +68,11 @@ func TestNamespaceNotFound(t *testing.T) {
Duration: 60000,
}
- res := testutil.CallRoute[handler.Request, openapi.NotFoundErrorResponse](h, route, headers, req)
- require.Equal(t, http.StatusNotFound, res.Status)
+ // Should return 410 because namespace is soft-deleted
+ res := testutil.CallRoute[handler.Request, openapi.GoneErrorResponse](h, route, headersWithPerms, req)
+ require.Equal(t, http.StatusGone, res.Status, "Should return 410 for soft-deleted namespace")
require.NotNil(t, res.Body)
- require.Equal(t, "https://unkey.com/docs/errors/unkey/data/ratelimit_namespace_not_found", res.Body.Error.Type)
+ require.Equal(t, "https://unkey.com/docs/errors/unkey/data/ratelimit_namespace_gone", res.Body.Error.Type)
+ require.Contains(t, res.Body.Error.Detail, "deleted", "Error message should indicate namespace was deleted")
})
}
diff --git a/go/apps/api/routes/v2_ratelimit_limit/accuracy_test.go b/go/apps/api/routes/v2_ratelimit_limit/accuracy_test.go
index d00c456eb5..e0fd9cadee 100644
--- a/go/apps/api/routes/v2_ratelimit_limit/accuracy_test.go
+++ b/go/apps/api/routes/v2_ratelimit_limit/accuracy_test.go
@@ -63,6 +63,7 @@ func TestRateLimitAccuracy(t *testing.T) {
ClickHouse: h.ClickHouse,
Ratelimit: h.Ratelimit,
RatelimitNamespaceCache: h.Caches.RatelimitNamespace,
+ Auditlogs: h.Auditlogs,
}
h.Register(route)
ctx := context.Background()
diff --git a/go/apps/api/routes/v2_ratelimit_limit/handler.go b/go/apps/api/routes/v2_ratelimit_limit/handler.go
index 4802a1a264..79e77cfa9f 100644
--- a/go/apps/api/routes/v2_ratelimit_limit/handler.go
+++ b/go/apps/api/routes/v2_ratelimit_limit/handler.go
@@ -2,6 +2,7 @@ package v2RatelimitLimit
import (
"context"
+ "database/sql"
"encoding/json"
"net/http"
"strconv"
@@ -9,9 +10,11 @@ import (
"time"
"github.com/unkeyed/unkey/go/apps/api/openapi"
+ "github.com/unkeyed/unkey/go/internal/services/auditlogs"
"github.com/unkeyed/unkey/go/internal/services/caches"
"github.com/unkeyed/unkey/go/internal/services/keys"
"github.com/unkeyed/unkey/go/internal/services/ratelimit"
+ "github.com/unkeyed/unkey/go/pkg/auditlog"
"github.com/unkeyed/unkey/go/pkg/cache"
"github.com/unkeyed/unkey/go/pkg/clickhouse"
"github.com/unkeyed/unkey/go/pkg/clickhouse/schema"
@@ -20,7 +23,9 @@ import (
"github.com/unkeyed/unkey/go/pkg/fault"
"github.com/unkeyed/unkey/go/pkg/match"
"github.com/unkeyed/unkey/go/pkg/otel/logging"
+ "github.com/unkeyed/unkey/go/pkg/ptr"
"github.com/unkeyed/unkey/go/pkg/rbac"
+ "github.com/unkeyed/unkey/go/pkg/uid"
"github.com/unkeyed/unkey/go/pkg/zen"
)
@@ -29,13 +34,13 @@ type Response = openapi.V2RatelimitLimitResponseBody
// Handler implements zen.Route interface for the v2 ratelimit limit endpoint
type Handler struct {
- // Services as public fields
Logger logging.Logger
Keys keys.KeyService
DB db.Database
ClickHouse clickhouse.Bufferer
Ratelimit ratelimit.Service
RatelimitNamespaceCache cache.Cache[cache.ScopedKey, db.FindRatelimitNamespace]
+ Auditlogs auditlogs.AuditLogService
TestMode bool
}
@@ -55,7 +60,6 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error {
s.DisableClickHouseLogging()
}
- // Authenticate the request with a root key
auth, emit, err := h.Keys.GetRootKey(ctx, s)
defer emit()
if err != nil {
@@ -67,87 +71,160 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error {
return err
}
- cost := int64(1)
- if req.Cost != nil {
- cost = *req.Cost
- }
+ cacheKey := cache.ScopedKey{WorkspaceID: auth.AuthorizedWorkspaceID, Key: req.Namespace}
- // Use the namespace field directly - it can be either name or ID
- namespaceKey := req.Namespace
+ var loader = func(ctx context.Context) (db.FindRatelimitNamespace, error) {
+ result := db.FindRatelimitNamespace{} // nolint:exhaustruct
+ response, err := db.WithRetry(func() (db.FindRatelimitNamespaceRow, error) {
+ return db.Query.FindRatelimitNamespace(ctx, h.DB.RO(), db.FindRatelimitNamespaceParams{
+ WorkspaceID: auth.AuthorizedWorkspaceID,
+ Namespace: req.Namespace,
+ })
+ })
+ if err != nil {
+ return result, err
+ }
- namespace, hit, err := h.RatelimitNamespaceCache.SWR(ctx,
- cache.ScopedKey{WorkspaceID: auth.AuthorizedWorkspaceID, Key: namespaceKey},
- func(ctx context.Context) (db.FindRatelimitNamespace, error) {
- result := db.FindRatelimitNamespace{} // nolint:exhaustruct
+ result = db.FindRatelimitNamespace{
+ ID: response.ID,
+ WorkspaceID: response.WorkspaceID,
+ Name: response.Name,
+ CreatedAtM: response.CreatedAtM,
+ UpdatedAtM: response.UpdatedAtM,
+ DeletedAtM: response.DeletedAtM,
+ DirectOverrides: make(map[string]db.FindRatelimitNamespaceLimitOverride),
+ WildcardOverrides: make([]db.FindRatelimitNamespaceLimitOverride, 0),
+ }
- response, err := db.WithRetry(func() (db.FindRatelimitNamespaceRow, error) {
- return db.Query.FindRatelimitNamespace(ctx, h.DB.RO(), db.FindRatelimitNamespaceParams{
- WorkspaceID: auth.AuthorizedWorkspaceID,
- Namespace: namespaceKey,
- })
- })
+ overrides := make([]db.FindRatelimitNamespaceLimitOverride, 0)
+ if overrideBytes, ok := response.Overrides.([]byte); ok && overrideBytes != nil {
+ err = json.Unmarshal(overrideBytes, &overrides)
if err != nil {
return result, err
}
+ }
- result = db.FindRatelimitNamespace{
- ID: response.ID,
- WorkspaceID: response.WorkspaceID,
- Name: response.Name,
- CreatedAtM: response.CreatedAtM,
- UpdatedAtM: response.UpdatedAtM,
- DeletedAtM: response.DeletedAtM,
- DirectOverrides: make(map[string]db.FindRatelimitNamespaceLimitOverride),
- WildcardOverrides: make([]db.FindRatelimitNamespaceLimitOverride, 0),
+ for _, override := range overrides {
+ result.DirectOverrides[override.Identifier] = override
+ if strings.Contains(override.Identifier, "*") {
+ result.WildcardOverrides = append(result.WildcardOverrides, override)
}
+ }
- overrides := make([]db.FindRatelimitNamespaceLimitOverride, 0)
- if overrideBytes, ok := response.Overrides.([]byte); ok && overrideBytes != nil {
- err = json.Unmarshal(overrideBytes, &overrides)
- if err != nil {
- return result, err
- }
+ return result, nil
+ }
+
+ namespace, hit, err := h.RatelimitNamespaceCache.SWR(
+ ctx,
+ cacheKey,
+ loader,
+ caches.DefaultFindFirstOp,
+ )
+ if err != nil && !db.IsNotFound(err) {
+ return fault.Wrap(err,
+ fault.Code(codes.App.Internal.UnexpectedError.URN()),
+ fault.Public("An unexpected error occurred while fetching the namespace."),
+ )
+ }
+
+ if hit == cache.Null || db.IsNotFound(err) {
+ err = auth.VerifyRootKey(ctx, keys.WithPermissions(
+ rbac.T(
+ rbac.Tuple{
+ ResourceType: rbac.Ratelimit,
+ ResourceID: "*",
+ Action: rbac.CreateNamespace,
+ },
+ ),
+ ))
+ if err != nil {
+ return err
+ }
+
+ namespace, err = db.TxWithResult(ctx, h.DB.RW(), func(ctx context.Context, tx db.DBTX) (db.FindRatelimitNamespace, error) {
+ result := db.FindRatelimitNamespace{}
+ now := time.Now().UnixMilli()
+ id := uid.New(uid.RatelimitNamespacePrefix)
+
+ err := db.Query.InsertRatelimitNamespace(ctx, tx, db.InsertRatelimitNamespaceParams{
+ ID: id,
+ WorkspaceID: auth.AuthorizedWorkspaceID,
+ Name: req.Namespace,
+ CreatedAt: now,
+ })
+ if err != nil && !db.IsDuplicateKeyError(err) {
+ return result, fault.Wrap(err,
+ fault.Code(codes.App.Internal.UnexpectedError.URN()),
+ fault.Public("An unexpected error occurred while creating the namespace."),
+ )
}
- for _, override := range overrides {
- result.DirectOverrides[override.Identifier] = override
- if strings.Contains(override.Identifier, "*") {
- result.WildcardOverrides = append(result.WildcardOverrides, override)
+ if db.IsDuplicateKeyError(err) {
+ namespace, err := loader(ctx)
+ if err != nil {
+ return result, fault.Wrap(err,
+ fault.Code(codes.App.Internal.UnexpectedError.URN()),
+ fault.Public("An unexpected error occurred while fetching the namespace."),
+ )
}
+
+ return namespace, err
}
- return result, nil
- }, caches.DefaultFindFirstOp)
+ result = db.FindRatelimitNamespace{
+ ID: id,
+ WorkspaceID: auth.AuthorizedWorkspaceID,
+ Name: req.Namespace,
+ CreatedAtM: now,
+ UpdatedAtM: sql.NullInt64{Valid: false, Int64: 0},
+ DeletedAtM: sql.NullInt64{Valid: false, Int64: 0},
+ DirectOverrides: make(map[string]db.FindRatelimitNamespaceLimitOverride),
+ WildcardOverrides: make([]db.FindRatelimitNamespaceLimitOverride, 0),
+ }
- if err != nil {
- if db.IsNotFound(err) {
- return fault.New("namespace was deleted",
- fault.Code(codes.Data.RatelimitNamespace.NotFound.URN()),
- fault.Public("This namespace does not exist."),
- )
- }
+ // Audit log for namespace creation
+ err = h.Auditlogs.Insert(ctx, tx, []auditlog.AuditLog{
+ {
+ WorkspaceID: auth.AuthorizedWorkspaceID,
+ Event: auditlog.RatelimitNamespaceCreateEvent,
+ Display: "Created ratelimit namespace " + req.Namespace,
+ ActorID: auth.Key.ID,
+ ActorName: auth.Key.Name.String,
+ ActorMeta: map[string]any{},
+ ActorType: auditlog.RootKeyActor,
+ RemoteIP: s.Location(),
+ UserAgent: s.UserAgent(),
+ Resources: []auditlog.AuditLogResource{
+ {
+ ID: id,
+ Type: auditlog.RatelimitNamespaceResourceType,
+ Meta: nil,
+ Name: req.Namespace,
+ DisplayName: req.Namespace,
+ },
+ },
+ },
+ })
+ if err != nil {
+ return result, err
+ }
- return fault.Wrap(err,
- fault.Code(codes.App.Internal.UnexpectedError.URN()),
- fault.Public("An unexpected error occurred while fetching the namespace."),
- )
- }
+ h.RatelimitNamespaceCache.Set(ctx, cacheKey, result)
- if hit == cache.Null {
- return fault.New("namespace cache null",
- fault.Code(codes.Data.RatelimitNamespace.NotFound.URN()),
- fault.Public("This namespace does not exist."),
- )
+ return result, nil
+ })
+ if err != nil {
+ return err
+ }
}
if namespace.DeletedAtM.Valid {
return fault.New("namespace was deleted",
- fault.Code(codes.Data.RatelimitNamespace.NotFound.URN()),
- fault.Public("This namespace does not exist."),
+ fault.Code(codes.Data.RatelimitNamespace.Gone.URN()),
+ fault.Public("This namespace has been deleted. Contact support to restore."),
)
}
- // Verify permissions for rate limiting
err = auth.VerifyRootKey(ctx, keys.WithPermissions(rbac.Or(
rbac.T(rbac.Tuple{
ResourceType: rbac.Ratelimit,
@@ -164,33 +241,22 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error {
return err
}
- // Determine limit and duration from override or request
- var (
- limit = req.Limit
- duration = req.Duration
- overrideId = ""
- )
-
- override, found, err := matchOverride(req.Identifier, namespace)
+ // Apply override if found, otherwise use request values
+ limit, duration, overrideId, err := getLimitAndDuration(req, namespace)
if err != nil {
return fault.Wrap(err,
fault.Code(codes.App.Internal.UnexpectedError.URN()),
- fault.Internal("error matching overrides"), fault.Public("Error matching ratelimit override"),
+ fault.Internal("error matching overrides"),
+ fault.Public("Error matching ratelimit override"),
)
}
- if found {
- limit = override.Limit
- duration = override.Duration
- overrideId = override.ID
- }
-
// Apply rate limit
limitReq := ratelimit.RatelimitRequest{
Identifier: namespace.ID + ":" + req.Identifier,
Duration: time.Duration(duration) * time.Millisecond,
Limit: limit,
- Cost: cost,
+ Cost: ptr.SafeDeref(req.Cost, 1),
Time: time.Time{},
}
@@ -246,6 +312,19 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error {
return s.JSON(http.StatusOK, res)
}
+func getLimitAndDuration(req Request, namespace db.FindRatelimitNamespace) (int64, int64, string, error) {
+ override, found, err := matchOverride(req.Identifier, namespace)
+ if err != nil {
+ return 0, 0, "", err
+ }
+
+ if found {
+ return override.Limit, override.Duration, override.ID, nil
+ }
+
+ return req.Limit, req.Duration, "", nil
+}
+
func matchOverride(identifier string, namespace db.FindRatelimitNamespace) (db.FindRatelimitNamespaceLimitOverride, bool, error) {
if override, ok := namespace.DirectOverrides[identifier]; ok {
return override, true, nil
diff --git a/go/demo_api/main.go b/go/demo_api/main.go
index cfa33d8d4b..3e7c3a4a0f 100644
--- a/go/demo_api/main.go
+++ b/go/demo_api/main.go
@@ -617,7 +617,7 @@ info:
version: 2.0.0
contact:
name: Unkey Support
- email: support@unkey.com
+ email: support@unkey.dev
servers:
- url: /v2
description: API v2 (BREAKING CHANGES)
diff --git a/go/pkg/codes/constants_gen.go b/go/pkg/codes/constants_gen.go
index eedae35e25..86b1fe0c2f 100644
--- a/go/pkg/codes/constants_gen.go
+++ b/go/pkg/codes/constants_gen.go
@@ -84,6 +84,8 @@ const (
// NotFound indicates the requested rate limit namespace was not found.
UnkeyDataErrorsRatelimitNamespaceNotFound URN = "err:unkey:data:ratelimit_namespace_not_found"
+ // Gone indicates the requested rate limit namespace was deleted and is no longer available.
+ UnkeyDataErrorsRatelimitNamespaceGone URN = "err:unkey:data:ratelimit_namespace_gone"
// RatelimitOverride
diff --git a/go/pkg/codes/unkey_data.go b/go/pkg/codes/unkey_data.go
index 4988f6a15a..5d21739314 100644
--- a/go/pkg/codes/unkey_data.go
+++ b/go/pkg/codes/unkey_data.go
@@ -48,6 +48,8 @@ type dataKeyAuth struct {
type dataRatelimitNamespace struct {
// NotFound indicates the requested rate limit namespace was not found.
NotFound Code
+ // Gone indicates the requested rate limit namespace was deleted and is no longer available.
+ Gone Code
}
// dataRatelimitOverride defines errors related to rate limit override operations.
@@ -119,6 +121,7 @@ var Data = UnkeyDataErrors{
RatelimitNamespace: dataRatelimitNamespace{
NotFound: Code{SystemUnkey, CategoryUnkeyData, "ratelimit_namespace_not_found"},
+ Gone: Code{SystemUnkey, CategoryUnkeyData, "ratelimit_namespace_gone"},
},
RatelimitOverride: dataRatelimitOverride{
diff --git a/go/pkg/db/acme_user_update_registered.sql_generated.go b/go/pkg/db/acme_user_update_registered.sql_generated.go
deleted file mode 100644
index bcf036a2e8..0000000000
--- a/go/pkg/db/acme_user_update_registered.sql_generated.go
+++ /dev/null
@@ -1,27 +0,0 @@
-// Code generated by sqlc. DO NOT EDIT.
-// versions:
-// sqlc v1.27.0
-// source: acme_user_update_registered.sql
-
-package db
-
-import (
- "context"
-)
-
-const updateAcmeUserRegistered = `-- name: UpdateAcmeUserRegistered :exec
-UPDATE acme_users SET registered = ? WHERE id = ?
-`
-
-type UpdateAcmeUserRegisteredParams struct {
- Registered bool `db:"registered"`
- ID uint64 `db:"id"`
-}
-
-// UpdateAcmeUserRegistered
-//
-// UPDATE acme_users SET registered = ? WHERE id = ?
-func (q *Queries) UpdateAcmeUserRegistered(ctx context.Context, db DBTX, arg UpdateAcmeUserRegisteredParams) error {
- _, err := db.ExecContext(ctx, updateAcmeUserRegistered, arg.Registered, arg.ID)
- return err
-}
diff --git a/go/pkg/db/queries/ratelimit_namespace_find.sql b/go/pkg/db/queries/ratelimit_namespace_find.sql
index 6e9220f749..0e92ce1bc6 100644
--- a/go/pkg/db/queries/ratelimit_namespace_find.sql
+++ b/go/pkg/db/queries/ratelimit_namespace_find.sql
@@ -13,5 +13,5 @@ SELECT *,
json_array()
) as overrides
FROM `ratelimit_namespaces` ns
-WHERE ns.workspace_id = sqlc.arg(workspace_id)
+WHERE ns.workspace_id = sqlc.arg(workspace_id)
AND (ns.id = sqlc.arg(namespace) OR ns.name = sqlc.arg(namespace));
diff --git a/go/pkg/db/ratelimit_namespace_find.sql_generated.go b/go/pkg/db/ratelimit_namespace_find.sql_generated.go
index cac3a0e56d..2b73610f98 100644
--- a/go/pkg/db/ratelimit_namespace_find.sql_generated.go
+++ b/go/pkg/db/ratelimit_namespace_find.sql_generated.go
@@ -25,7 +25,7 @@ SELECT id, workspace_id, name, created_at_m, updated_at_m, deleted_at_m,
json_array()
) as overrides
FROM ` + "`" + `ratelimit_namespaces` + "`" + ` ns
-WHERE ns.workspace_id = ?
+WHERE ns.workspace_id = ?
AND (ns.id = ? OR ns.name = ?)
`
diff --git a/go/pkg/zen/middleware_errors.go b/go/pkg/zen/middleware_errors.go
index ec619cad2c..13b9ede930 100644
--- a/go/pkg/zen/middleware_errors.go
+++ b/go/pkg/zen/middleware_errors.go
@@ -89,6 +89,20 @@ func WithErrorHandling(logger logging.Logger) Middleware {
},
})
+ // Request Entity Too Large errors
+ case codes.UnkeyDataErrorsRatelimitNamespaceGone:
+ return s.JSON(http.StatusGone, openapi.GoneErrorResponse{
+ Meta: openapi.Meta{
+ RequestId: s.RequestID(),
+ },
+ Error: openapi.BaseError{
+ Title: "Resource Gone",
+ Type: code.DocsURL(),
+ Detail: fault.UserFacingMessage(err),
+ Status: http.StatusGone,
+ },
+ })
+
// Unauthorized errors
case
codes.UnkeyAuthErrorsAuthenticationKeyNotFound:
diff --git a/go/pkg/zen/middleware_metrics.go b/go/pkg/zen/middleware_metrics.go
index b8aeb72530..a177fad598 100644
--- a/go/pkg/zen/middleware_metrics.go
+++ b/go/pkg/zen/middleware_metrics.go
@@ -57,10 +57,8 @@ func redact(in []byte) []byte {
// route,
// )
func WithMetrics(eventBuffer EventBuffer) Middleware {
-
return func(next HandleFunc) HandleFunc {
return func(ctx context.Context, s *Session) error {
-
start := time.Now()
nextErr := next(ctx, s)
serviceLatency := time.Since(start)