multica-ai/multica · warning

key must match ^[a-zA-Z_][a-zA-Z0-9_.-]{0,63}$

Error message

key must match ^[a-zA-Z_][a-zA-Z0-9_.-]{0,63}$

What it means

validateIssueMetadataKey rejects keys that do not match ^[a-zA-Z_][a-zA-Z0-9_.-]{0,63}$: they must start with a letter or underscore, may continue with letters, digits, underscore, dot, or hyphen, and be 1..64 characters total. This keeps metadata keys safe for use as JSONB object members and prevents injection-ish or unbounded key names. The error message helpfully embeds the exact regex so the caller can self-correct.

Source

Thrown at server/internal/handler/issue_metadata.go:50

	maxIssueMetadataKeys = 50
)

var issueMetadataKeyRE = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_.-]{0,63}$`)

// SetIssueMetadataKeyRequest carries the JSON value to write under the key
// named in the URL. Value is a RawMessage so we can preserve numeric vs.
// string typing through to PostgreSQL — once decoded into `any`, JSON
// numbers all collapse to float64 and we'd lose integer fidelity.
type SetIssueMetadataKeyRequest struct {
	Value json.RawMessage `json:"value"`
}

func validateIssueMetadataKey(key string) error {
	if key == "" {
		return errors.New("key is required")
	}
	if !issueMetadataKeyRE.MatchString(key) {
		return errors.New("key must match ^[a-zA-Z_][a-zA-Z0-9_.-]{0,63}$")
	}
	return nil
}

// validateIssueMetadataValue rejects anything other than a primitive JSON
// scalar. Null, arrays, and objects are not allowed — the V1 surface is
// flat KV. Removing a key uses DELETE, not a null value.
func validateIssueMetadataValue(raw json.RawMessage) error {
	if len(raw) == 0 {
		return errors.New("value is required")
	}
	var v any
	if err := json.Unmarshal(raw, &v); err != nil {
		return fmt.Errorf("value must be valid JSON: %w", err)
	}
	switch v.(type) {
	case string, bool, float64:
		return nil

View on GitHub (pinned to 2c0912b6ec)

Solutions

  1. Normalize keys before sending: lowercase, replace disallowed chars with '_' or '-', trim to 64 chars, prefix with '_' if it starts with a digit or hyphen.
  2. Reserve the raw label as the metadata *value* and use a normalized slug as the key.
  3. Apply the same regex client-side and show it as inline validation.
  4. Reject non-ASCII input at the form layer or transliterate it.

Example fix

// before
const key = label; // e.g. "Due Date!"
await fetch(`${base}/issues/${id}/metadata/${key}`, ...);

// after
const key = label.trim().replace(/[^a-zA-Z0-9_.-]+/g, '_').replace(/^[^a-zA-Z_]/, '_$&').slice(0, 64);
await fetch(`${base}/issues/${id}/metadata/${encodeURIComponent(key)}`, ...);
Defensive patterns

Strategy: validation

Validate before calling

const KEY_RE = /^[a-zA-Z_][a-zA-Z0-9_.-]{0,63}$/;

function normalizeMetadataKey(raw) {
  let k = String(raw ?? '').trim()
    .replace(/[^a-zA-Z0-9_.-]+/g, '_')
    .replace(/^[0-9-]/, '_$&')   // must start with letter or underscore
    .slice(0, 64);
  if (!KEY_RE.test(k)) throw new TypeError(`cannot normalize key: ${raw}`);
  return k;
}

Type guard

const isValidMetadataKey = (k) => typeof k === 'string' && /^[a-zA-Z_][a-zA-Z0-9_.-]{0,63}$/.test(k);

Prevention

When it happens

Trigger: PUT with key '123abc' (starts with digit), 'my key' (space), 'my/key' (slash), 'key!' (special char), 'a'.repeat(65) (too long), or '-flag' (starts with hyphen). Note dots and hyphens inside the key are fine.

Common situations: Using user-facing labels or free-text tags as metadata keys; embedding emails/URLs as keys ('user@x.com' fails on '@'); non-ASCII keys from localized UIs; long descriptive keys from LLM-generated metadata.

Related errors


AI-assisted analysis of multica-ai/multica@2c0912b6ec (2026-08-15). Data as JSON: /api/errors/fb9026d7943b6d6a. Report an issue: GitHub.