transloadit/uppy · error

s3: the part number must be a number between 1 and 10000.

Error message

s3: the part number must be a number between 1 and 10000.

What it means

When signing a part upload, Companion validates that partNumber is a string that parses as an integer (via parseInt). A missing, non-numeric, or zero-parsing value like 'abc' or '0' fails the check and gets HTTP 400. AWS multipart part numbers must be between 1 and 10000, hence the message.

Source

Thrown at packages/@uppy/companion/src/server/controllers/s3.ts:338

    if (!client) return

    const uploadId = req.params['uploadId']
    const partNumber = req.params['partNumber']
    const key = req.query['key']

    if (typeof uploadId !== 'string' || uploadId.length === 0) {
      res.status(400).json({ error: 's3: uploadId must be provided.' })
      return
    }
    if (typeof key !== 'string') {
      res.status(400).json({
        error:
          's3: the object key must be passed as a query parameter. For example: "?key=abc.jpg"',
      })
      return
    }
    if (typeof partNumber !== 'string' || !parseInt(partNumber, 10)) {
      res.status(400).json({
        error: 's3: the part number must be a number between 1 and 10000.',
      })
      return
    }

    const bucket = getBucket({ bucketOrFn: config.bucket, req })

    getSignedUrl(
      client,
      new UploadPartCommand({
        Bucket: bucket,
        Key: key,
        UploadId: uploadId,
        PartNumber: Number(partNumber),
        Body: '',
      }),
      { expiresIn: config.expires },
    ).then((url) => {

View on GitHub (pinned to 5d4dedd02a)

Solutions

  1. Use 1-based integer part numbers: partNumber = Math.floor(offset / chunkSize) + 1 and interpolate it as a plain integer in the URL.
  2. Validate partNumber client-side: Number.isInteger(n) && n >= 1 && n <= 10000 before requesting a signature.
  3. Check for off-by-one bugs where the loop starts at index 0 and passes it directly as the part number.

Example fix

// before (0-based index -> part 0 rejected)
chunks.forEach((chunk, i) => signPart(uploadId, i, key))

// after (1-based, 1..10000)
chunks.forEach((chunk, i) => signPart(uploadId, i + 1, key))
Defensive patterns

Strategy: type-guard

Validate before calling

const partNumber = Math.floor(byteOffset / chunkSize) + 1
if (!Number.isInteger(partNumber) || partNumber < 1 || partNumber > 10000) {
  throw new RangeError(`invalid part number ${partNumber}`)
}

Type guard

const isValidPartNumber = (n: unknown): n is number =>
  typeof n === 'number' && Number.isInteger(n) && n >= 1 && n <= 10000

Prevention

When it happens

Trigger: POST /s3/multipart/<uploadId>/<partNumber> with a non-numeric partNumber such as 'part1' or 'abc', with '0' (parseInt returns 0, falsy), or an empty segment; also non-integer values like '1.5' parse to 1 and pass, but strings like '' fail.

Common situations: Custom chunking loops that compute partNumber incorrectly (0-based indexing sending 0), string concatenation bugs, or template URLs where partNumber is undefined producing '/s3/multipart/<id>/undefined'.

Related errors


AI-assisted analysis of transloadit/uppy@5d4dedd02a (2026-08-28). Data as JSON: /api/errors/9a3725d650611c03. Report an issue: GitHub.