paragonie/random_compat · error · TypeError

RandomCompat_substr(): Second argument should be an integer

Error message

RandomCompat_substr(): Second argument should be an integer

What it means

RandomCompat_substr()'s second parameter $start must be an integer byte offset. When it is not (float, string numeric like "5", null, bool), the function throws TypeError. This strictness exists because float/string offsets would silently truncate or coerce in native substr and break the deterministic byte-slicing random_compat relies on.

Solutions

  1. Cast explicitly with (int) $start once you have confirmed the value is numeric, e.g. $start = (int) $rawStart.
  2. Use is_int($start) validation at your boundary and reject or coerce non-ints.
  3. If the value derives from float math, round intentionally: $start = (int) floor($x).
  4. Provide defaults for optional values: $start = $opts['start'] ?? 0.

Example fix

// before
$start = $data['offset']; // string "5" from JSON
RandomCompat_substr($str, $start);
// after
$start = (int) $data['offset'];
RandomCompat_substr($str, $start);
Defensive patterns

Strategy: validation

Validate before calling

$start = $opts['start'] ?? 0;
if (!is_int($start)) {
    $start = (int) $start; // only safe if numeric; validate first if untrusted
}
RandomCompat_substr($str, $start);

Type guard

function isIntOffset($value): bool {
    return is_int($value) && $value >= 0;
}
if (isIntOffset($start)) {
    $chunk = RandomCompat_substr($str, $start);
}

Try / catch

try {
    $chunk = RandomCompat_substr($str, $start);
} catch (TypeError $e) {
    // $start was not an int; coerce or fail
    $chunk = RandomCompat_substr($str, (int) $start);
}

Prevention

When it happens

Trigger: Calling RandomCompat_substr($str, $start) where $start came from float math (e.g. strlen()/2), from a string config value, from JSON with a numeric string, or is null because an optional parameter was never set. Also triggered when using random_int()'s output stored through a middleware that casted to string.

Common situations: Dividing lengths to find a midpoint produces a float (32/2 is int in PHP, but ceil()/floor() results are floats); config files (YAML/JSON/env) providing "0" as a string; null from optional array keys accessed without defaults; database columns typed as decimal.

Understand the failure class

Background: "must be a positive integer", "cannot be empty", "invalid argument": how invalid-argument errors work across open-source libraries — this error's family across 33 libraries.

Related errors


AI-assisted analysis of paragonie/random_compat@b5d188cc9d (2026-09-13). Data as JSON: /api/errors/3218e2bd3020ff88. Report an issue: GitHub.

Appendix: source

Thrown at lib/byte_safe_strings.php:112

         *
         * @param string $binary_string
         * @param int $start
         * @param int|null $length (optional)
         *
         * @throws TypeError
         *
         * @return string
         */
        function RandomCompat_substr($binary_string, $start, $length = null)
        {
            if (!is_string($binary_string)) {
                throw new TypeError(
                    'RandomCompat_substr(): First argument should be a string'
                );
            }

            if (!is_int($start)) {
                throw new TypeError(
                    'RandomCompat_substr(): Second argument should be an integer'
                );
            }

            if ($length === null) {
                /**
                 * mb_substr($str, 0, NULL, '8bit') returns an empty string on
                 * PHP 5.3, so we have to find the length ourselves.
                 */
                /** @var int $length */
                $length = RandomCompat_strlen($binary_string) - $start;
            } elseif (!is_int($length)) {
                throw new TypeError(
                    'RandomCompat_substr(): Third argument should be an integer, or omitted'
                );
            }

            // Consistency with PHP's behavior

View on GitHub (pinned to b5d188cc9d)