Tencent/WeKnora · error · errInvalidExternalUserID

%w: %v

Error message

%w: %v

What it means

In internal/middleware/auth.go:587, resolveAPIPrincipal (APIPrincipalModeDirect) wraps errInvalidExternalUserID with the underlying validation failure from validateExternalUserID. The external user id supplied in the direct header is empty, too long, or contains control characters, so no Principal is resolved and the request is rejected.

Source

Thrown at internal/middleware/auth.go:587

	}
	if tenant == nil || tenantID == 0 {
		return fallback, nil
	}
	cfg := tenant.APIPrincipalConfig
	if cfg == nil || cfg.Mode == "" || cfg.Mode == types.APIPrincipalModeTenant {
		return fallback, nil
	}
	switch cfg.Mode {
	case types.APIPrincipalModeDirect:
		externalUserID := strings.TrimSpace(header.Get(defaultExternalUserIDHeader))
		if externalUserID == "" {
			if cfg.RequireDirectHeader {
				return types.Principal{}, errMissingDirectHeader
			}
			return fallback, nil
		}
		if err := validateExternalUserID(externalUserID); err != nil {
			return types.Principal{}, fmt.Errorf("%w: %v", errInvalidExternalUserID, err)
		}
		return types.Principal{
			Type: types.PrincipalAPIExternalUser,
			ID:   strconv.FormatUint(tenantID, 10) + ":" + externalUserID,
		}, nil
	case types.APIPrincipalModeSignedToken:
		externalUserID, err := verifyExternalUserJWT(header.Get(defaultExternalUserTokenHeader), tenantID, cfg.HMACSecret)
		if err != nil || externalUserID == "" {
			logger.Warnf(ctx, "invalid external user token for tenant=%d: %v", tenantID, err)
			return types.Principal{}, fmt.Errorf("%w: %w", errInvalidExternalUserToken, err)
		}
		if err := validateExternalUserID(externalUserID); err != nil {
			return types.Principal{}, fmt.Errorf("%w: %v", errInvalidExternalUserID, err)
		}
		return types.Principal{
			Type: types.PrincipalAPIExternalUser,
			ID:   strconv.FormatUint(tenantID, 10) + ":" + externalUserID,
		}, nil

View on GitHub (pinned to 988cbb0330)

Solutions

  1. Send a non-empty external user id in the direct header with no control characters and within maxExternalUserIDLen.
  2. Trim whitespace/newlines from the value on the client side before setting the header.
  3. Check errors.Is(err, errInvalidExternalUserID) and surface the inner message to fix the exact violation (empty / too long / invalid characters).

Example fix

// before (client)
req.Header.Set("X-External-User-ID", userID + "\n") // control char -> rejected
// after
req.Header.Set("X-External-User-ID", strings.TrimSpace(userID))
Defensive patterns

Strategy: try-catch

Validate before calling

id := strings.TrimSpace(extUserID)
if id == "" || len(id) > maxExternalUserIDLen || strings.ContainsFunc(id, func(r rune) bool { return r < 0x20 || r == 0x7f }) {
    // fix the header value before sending the request
}

Type guard

func validExternalUserID(id string) bool {
    id = strings.TrimSpace(id)
    if id == "" || len(id) > maxExternalUserIDLen { return false }
    for _, r := range id { if r < 0x20 || r == 0x7f { return false } }
    return true
}

Try / catch

if err != nil {
    if errors.Is(err, errInvalidExternalUserID) {
        // correct the external user id header and retry once
    }
}

Prevention

When it happens

Trigger: Tenant configured with APIPrincipalModeDirect; request carries the external user id header (defaultExternalUserIDHeader) with a value that is empty after trim, longer than maxExternalUserIDLen, or contains bytes <0x20 or 0x7f.

Common situations: Client sends a padded/newline-terminated user id from a misconfigured header; integration passes an oversized UUID-with-prefix string; id built by concatenating fields picks up a tab or control character.

Related errors


AI-assisted analysis of Tencent/WeKnora@988cbb0330 (2026-09-02). Data as JSON: /api/errors/cd944ec499fa3dd3. Report an issue: GitHub.