{"record":{"id":"9a3725d650611c03","repo":"transloadit/uppy","slug":"s3-the-part-number-must-be-a-number-between-1-and","errorCode":null,"errorMessage":"s3: the part number must be a number between 1 and 10000.","messagePattern":"s3: the part number must be a number between 1 and 10000\\.","errorType":"http","errorClass":null,"httpStatus":400,"severity":"error","filePath":"packages/@uppy/companion/src/server/controllers/s3.ts","lineNumber":338,"sourceCode":"    if (!client) return\n\n    const uploadId = req.params['uploadId']\n    const partNumber = req.params['partNumber']\n    const key = req.query['key']\n\n    if (typeof uploadId !== 'string' || uploadId.length === 0) {\n      res.status(400).json({ error: 's3: uploadId must be provided.' })\n      return\n    }\n    if (typeof key !== 'string') {\n      res.status(400).json({\n        error:\n          's3: the object key must be passed as a query parameter. For example: \"?key=abc.jpg\"',\n      })\n      return\n    }\n    if (typeof partNumber !== 'string' || !parseInt(partNumber, 10)) {\n      res.status(400).json({\n        error: 's3: the part number must be a number between 1 and 10000.',\n      })\n      return\n    }\n\n    const bucket = getBucket({ bucketOrFn: config.bucket, req })\n\n    getSignedUrl(\n      client,\n      new UploadPartCommand({\n        Bucket: bucket,\n        Key: key,\n        UploadId: uploadId,\n        PartNumber: Number(partNumber),\n        Body: '',\n      }),\n      { expiresIn: config.expires },\n    ).then((url) => {","sourceCodeStart":320,"sourceCodeEnd":356,"githubUrl":"https://github.com/transloadit/uppy/blob/5d4dedd02a1ac0ae022c75c54aca76558f88e256/packages/@uppy/companion/src/server/controllers/s3.ts#L320-L356","documentation":"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.","triggerScenarios":"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.","commonSituations":"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'.","solutions":["Use 1-based integer part numbers: partNumber = Math.floor(offset / chunkSize) + 1 and interpolate it as a plain integer in the URL.","Validate partNumber client-side: Number.isInteger(n) && n >= 1 && n <= 10000 before requesting a signature.","Check for off-by-one bugs where the loop starts at index 0 and passes it directly as the part number."],"exampleFix":"// before (0-based index -> part 0 rejected)\nchunks.forEach((chunk, i) => signPart(uploadId, i, key))\n\n// after (1-based, 1..10000)\nchunks.forEach((chunk, i) => signPart(uploadId, i + 1, key))","handlingStrategy":"type-guard","validationCode":"const partNumber = Math.floor(byteOffset / chunkSize) + 1\nif (!Number.isInteger(partNumber) || partNumber < 1 || partNumber > 10000) {\n  throw new RangeError(`invalid part number ${partNumber}`)\n}","typeGuard":"const isValidPartNumber = (n: unknown): n is number =>\n  typeof n === 'number' && Number.isInteger(n) && n >= 1 && n <= 10000","tryCatchPattern":null,"preventionTips":["Use 1-based indexing for part numbers","Compute part numbers from offsets with Math.floor(offset/chunkSize)+1, never from array index alone","Cap chunk count at 10000 parts (increase chunk size for very large files)"],"tags":["s3","companion","multipart-upload","part-number","request-validation"],"backgroundTag":"parameter-out-of-range","analyzedSha":"5d4dedd02a1ac0ae022c75c54aca76558f88e256","analyzedAt":"2026-08-28T12:18:41.267Z","schemaVersion":2},"datasetVersion":"2026-08-28T16:17:29.566Z"}