discourse/discourse · error · Discourse::InvalidParameters

All parts must have an etag and a valid part number

Error message

All parts must have an etag and a valid part number

What it means

Raised in ExternalUploadHelpers#complete_multipart when a client completes a multipart external upload but one of the submitted parts lacks an etag or has an invalid part number. The AbortMultipartUpload/CompleteUpload request payload is incomplete, so the S3-complete call cannot be built. The at-fault input is the parts array sent by the client.

Solutions

  1. Ensure the client records the ETag returned by each part PUT and sends it in the complete request
  2. Verify every part_number is an integer between 1 and 10000 with no gaps or duplicates
  3. Retry the upload from scratch if part metadata was lost client-side
  4. Check the browser network log to confirm each part upload returned 200 with an ETag header
Defensive patterns

Strategy: validation

When it happens

Trigger: Thrown at lib/external_upload_helpers.rb:246 when the library encounters an invalid state.

Common situations: See trigger scenarios.


AI-assisted analysis of discourse/discourse@1b2d7253e8 (2026-08-26). Data as JSON: /api/errors/0c3a64aeee6442db. Report an issue: GitHub.

Appendix: source

Thrown at lib/external_upload_helpers.rb:246

    ).performed!

    external_upload_stub =
      ExternalUploadStub.find_by(unique_identifier: unique_identifier, created_by: current_user)
    return render_404 if external_upload_stub.blank?

    message = check_multipart_upload_exists(external_upload_stub)
    return render_404(message) if message.present?

    store = multipart_store(external_upload_stub.upload_type)
    parts =
      parts
        .map do |part|
          part_number = part[:part_number]
          etag = part[:etag]
          part_number = validate_part_number(part_number)

          if etag.blank?
            raise Discourse::InvalidParameters.new(
                    "All parts must have an etag and a valid part number",
                  )
          end

          # this is done so it's an array of hashes rather than an array of
          # ActionController::Parameters
          { part_number: part_number, etag: etag }
        end
        .sort_by { |part| part[:part_number] }

    begin
      store.complete_multipart(
        upload_id: external_upload_stub.external_upload_identifier,
        key: external_upload_stub.key,
        parts: parts,
      )
    rescue Aws::S3::Errors::ServiceError => err
      return(

View on GitHub (pinned to 1b2d7253e8)