abhigyanpatwari/GitNexus · error · BadRequestError

Parameter "${fieldName}" must be a single string, got an arr

Error message

Parameter "${fieldName}" must be a single string, got an array

What it means

Express's qs parsers turn a repeated query/body key (?x=a&x=b) into an array, which silently defeats .length guards written for a value cast to string — the exact CodeQL js/type-confusion-through-parameter-tampering shape (alert at api.ts:1118). assertString is the hardening guard: when the value is specifically an array it throws BadRequestError (HTTP 400) naming the field, so callers fail fast instead of measuring array length as if it were string length.

Source

Thrown at gitnexus/src/server/validation.ts:60

  }
}

/**
 * Type guard for HTTP request parameters that must be a single string.
 *
 * Express's req.query and req.body parsers return `string | string[] | ParsedQs`
 * for any field, but route handlers commonly cast to `string` and operate on
 * `.length`. When the caller passes the same key twice (?x=a&x=b) the value
 * arrives as an array, and a `.length` check intended for the string ends up
 * counting array elements — bypassing length-based guards (CodeQL
 * js/type-confusion-through-parameter-tampering, alert at api.ts:1118).
 *
 * @throws BadRequestError when value is not a string (array, object, undefined, etc.)
 */
export function assertString(value: unknown, fieldName: string): string {
  if (typeof value !== 'string') {
    if (Array.isArray(value)) {
      throw new BadRequestError(`Parameter "${fieldName}" must be a single string, got an array`);
    }
    throw new BadRequestError(`Parameter "${fieldName}" must be a string`);
  }
  return value;
}

/**
 * Resolve a user-supplied relative path against an allowed root and verify it
 * stays inside that root. Mirrors the existing guard at api.ts:1067-1077.
 *
 * Returns the absolute resolved path. Rejects empty paths, null bytes, and
 * paths that resolve outside the root (e.g., `../../../etc/passwd`).
 *
 * @throws BadRequestError when the path is empty or contains a null byte
 * @throws ForbiddenError when the resolved path escapes the root
 */
export function assertSafePath(rawPath: string, root: string): string {
  if (rawPath.length === 0) {

View on GitHub (pinned to aac7515d2a)

Solutions

  1. Send the parameter exactly once per request
  2. Fix the client's serializer so array-valued options use a distinct key or are joined into one value
  3. If multi-value is genuinely intended, add an explicit array-accepting validator server-side instead of bypassing assertString

Example fix

// before — axios expands arrays into repeated keys → 400
axios.get('/api/grep', { params: { q: ['a', 'b'] } });

// after
axios.get('/api/grep', { params: { q: 'a' } });
// or join client-side into a single value
axios.get('/api/grep', { params: { q: ['a', 'b'].join(',') } });
Defensive patterns

Strategy: type-guard

Validate before calling

const params = new URLSearchParams(query);
for (const key of [...params.keys()]) {
  if (params.getAll(key).length > 1) {
    throw new Error(`Duplicate query key "${key}" — send a single value`);
  }
}

Type guard

function isSingleString(v: unknown): v is string {
  return typeof v === 'string';
}

Try / catch

try {
  await apiCall(req);
} catch (e) {
  if (e instanceof BadRequestError && e.message.includes('single string')) {
    // client bug: the same key was sent twice — fix the sender, do not retry
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling any /api route that passes a query or body field through assertString with the same key present twice: GET /api/...?q=a&q=b, curl with repeated --data-urlencode flags, or a JSON body {"q": ["a","b"]}.

Common situations: HTTP clients whose params serializer expands JS arrays into repeated keys (axios default paramsSerializer, superagent); hand-built query strings that append a default and then user input; intermediaries or proxies re-appending a parameter.

Related errors


AI-assisted analysis of abhigyanpatwari/GitNexus@aac7515d2a (2026-08-20). Data as JSON: /api/errors/957bd538f0e96738. Report an issue: GitHub.