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
- Ensure the client records the ETag returned by each part PUT and sends it in the complete request
- Verify every part_number is an integer between 1 and 10000 with no gaps or duplicates
- Retry the upload from scratch if part metadata was lost client-side
- 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)