fix: use a single translator for middleware (#10633)

# Which Problems Are Solved

Comparing the v3 and v4 deployments we noticed an increase in memory
usage. A first analysis revealed that it might be related to the
(multiple) initialization of the `i18n.Translator`, partially related

# How the Problems Are Solved

Initialize the tranlator once (apart from the translator interceptor,
which uses context / request specific information) and pass it to all
necessary middleware.

# Additional Changes

Removed unnecessary error return parameter from the translator
initialization.

# Additional Context

- noticed internally
- backport to v4.x

(cherry picked from commit a0c3ccecf7)
This commit is contained in:
Livio Spring
2025-09-09 08:34:59 +02:00
parent 0753ed2d6b
commit 0f6380b474
19 changed files with 118 additions and 106 deletions

View File

@@ -16,12 +16,14 @@ import (
"github.com/zitadel/zitadel/internal/api/scim/metadata"
"github.com/zitadel/zitadel/internal/api/scim/schemas"
"github.com/zitadel/zitadel/internal/api/scim/serrors"
"github.com/zitadel/zitadel/internal/i18n"
"github.com/zitadel/zitadel/internal/zerrors"
)
type BulkHandler struct {
cfg *scim_config.BulkConfig
handlersByPluralResourceName map[schemas.ScimResourceTypePlural]RawResourceHandlerAdapter
translator *i18n.Translator
}
type BulkRequest struct {
@@ -56,6 +58,7 @@ func (r *BulkRequest) GetSchemas() []schemas.ScimSchemaType {
func NewBulkHandler(
cfg scim_config.BulkConfig,
translator *i18n.Translator,
handlers ...RawResourceHandlerAdapter,
) *BulkHandler {
handlersByPluralResourceName := make(map[schemas.ScimResourceTypePlural]RawResourceHandlerAdapter, len(handlers))
@@ -66,6 +69,7 @@ func NewBulkHandler(
return &BulkHandler{
&cfg,
handlersByPluralResourceName,
translator,
}
}
@@ -140,7 +144,7 @@ func (h *BulkHandler) processOperation(ctx context.Context, op *BulkRequestOpera
opResp.Status = strconv.Itoa(statusCode)
if err != nil {
opResp.Error = serrors.MapToScimError(ctx, err)
opResp.Error = serrors.MapToScimError(ctx, h.translator, err)
opResp.Status = opResp.Error.Status
}
}()

View File

@@ -9,7 +9,6 @@ import (
"strconv"
"github.com/zitadel/logging"
"golang.org/x/text/language"
http_util "github.com/zitadel/zitadel/internal/api/http"
zhttp_middleware "github.com/zitadel/zitadel/internal/api/http/middleware"
@@ -67,24 +66,22 @@ const (
ScimTypeUniqueness scimErrorType = "uniqueness"
)
var translator *i18n.Translator
func ErrorHandler(translator *i18n.Translator) func(next zhttp_middleware.HandlerFuncWithError) http.Handler {
return func(next zhttp_middleware.HandlerFuncWithError) http.Handler {
var err error
func ErrorHandler(next zhttp_middleware.HandlerFuncWithError) http.Handler {
var err error
translator, err = i18n.NewZitadelTranslator(language.English)
logging.OnError(err).Panic("unable to get translator")
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if err = next(w, r); err == nil {
return
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if err = next(w, r); err == nil {
return
}
scimErr := MapToScimError(r.Context(), translator, err)
w.WriteHeader(scimErr.StatusCode)
scimErr := MapToScimError(r.Context(), err)
w.WriteHeader(scimErr.StatusCode)
jsonErr := json.NewEncoder(w).Encode(scimErr)
logging.OnError(jsonErr).Warn("Failed to marshal scim error response")
})
jsonErr := json.NewEncoder(w).Encode(scimErr)
logging.OnError(jsonErr).Warn("Failed to marshal scim error response")
})
}
}
func ThrowInvalidValue(parent error) error {
@@ -146,7 +143,7 @@ func (err *wrappedScimError) Error() string {
return fmt.Sprintf("SCIM Error: %s: %s", err.ScimType, err.Parent.Error())
}
func MapToScimError(ctx context.Context, err error) *ScimError {
func MapToScimError(ctx context.Context, translator *i18n.Translator, err error) *ScimError {
scimError := new(ScimError)
if ok := errors.As(err, &scimError); ok {
return scimError
@@ -154,7 +151,7 @@ func MapToScimError(ctx context.Context, err error) *ScimError {
scimWrappedError := new(wrappedScimError)
if ok := errors.As(err, &scimWrappedError); ok {
mappedErr := MapToScimError(ctx, scimWrappedError.Parent)
mappedErr := MapToScimError(ctx, translator, scimWrappedError.Parent)
if scimWrappedError.ScimType != "" {
mappedErr.ScimType = scimWrappedError.ScimType
}

View File

@@ -7,6 +7,7 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/i18n"
"github.com/zitadel/zitadel/internal/zerrors"
@@ -97,9 +98,10 @@ func TestErrorHandler(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
req := httptest.NewRequest("GET", "/", nil)
recorder := httptest.NewRecorder()
ErrorHandler(func(http.ResponseWriter, *http.Request) error {
return tt.err
}).ServeHTTP(recorder, req)
ErrorHandler(i18n.NewZitadelTranslator(language.English))(
func(http.ResponseWriter, *http.Request) error {
return tt.err
}).ServeHTTP(recorder, req)
assert.Equal(t, tt.wantStatus, recorder.Code)
if tt.wantBody != "" {

View File

@@ -18,6 +18,7 @@ import (
"github.com/zitadel/zitadel/internal/api/scim/serrors"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/i18n"
"github.com/zitadel/zitadel/internal/query"
)
@@ -27,10 +28,11 @@ func NewServer(
verifier *authz.ApiTokenVerifier,
userCodeAlg crypto.EncryptionAlgorithm,
config *sconfig.Config,
translator *i18n.Translator,
middlewares ...zhttp_middlware.MiddlewareWithErrorFunc,
) http.Handler {
verifier.RegisterServer("SCIM-V2", schemas.HandlerPrefix, AuthMapping)
return buildHandler(command, query, userCodeAlg, config, middlewares...)
return buildHandler(command, query, userCodeAlg, config, translator, middlewares...)
}
func buildHandler(
@@ -38,16 +40,17 @@ func buildHandler(
query *query.Queries,
userCodeAlg crypto.EncryptionAlgorithm,
cfg *sconfig.Config,
translator *i18n.Translator,
middlewares ...zhttp_middlware.MiddlewareWithErrorFunc,
) http.Handler {
router := mux.NewRouter()
middleware := buildMiddleware(cfg, query, middlewares)
middleware := buildMiddleware(cfg, query, translator, middlewares)
usersHandler := sresources.NewResourceHandlerAdapter(sresources.NewUsersHandler(command, query, userCodeAlg, cfg))
mapResource(router, middleware, usersHandler)
bulkHandler := sresources.NewBulkHandler(cfg.Bulk, usersHandler)
bulkHandler := sresources.NewBulkHandler(cfg.Bulk, translator, usersHandler)
router.Handle("/"+zhttp.OrgIdInPathVariable+"/Bulk", middleware(handleJsonResponse(bulkHandler.BulkFromHttp))).Methods(http.MethodPost)
serviceProviderHandler := newServiceProviderHandler(cfg, usersHandler)
@@ -60,11 +63,16 @@ func buildHandler(
return router
}
func buildMiddleware(cfg *sconfig.Config, query *query.Queries, middlewares []zhttp_middlware.MiddlewareWithErrorFunc) zhttp_middlware.ErrorHandlerFunc {
func buildMiddleware(
cfg *sconfig.Config,
query *query.Queries,
translator *i18n.Translator,
middlewares []zhttp_middlware.MiddlewareWithErrorFunc,
) zhttp_middlware.ErrorHandlerFunc {
// content type middleware needs to run at the very beginning to correctly set content types of errors
middlewares = append([]zhttp_middlware.MiddlewareWithErrorFunc{smiddleware.ContentTypeMiddleware}, middlewares...)
middlewares = append(middlewares, smiddleware.ScimContextMiddleware(query))
scimMiddleware := zhttp_middlware.ChainedWithErrorHandler(serrors.ErrorHandler, middlewares...)
scimMiddleware := zhttp_middlware.ChainedWithErrorHandler(serrors.ErrorHandler(translator), middlewares...)
return func(handler zhttp_middlware.HandlerFuncWithError) http.Handler {
return http.MaxBytesHandler(scimMiddleware(handler), cfg.MaxRequestBodySize)
}