hcengineering/platform · error

upload already terminated or completed

Error message

upload already terminated or completed

What it means

MultipartUpload.Write refuses new data once the upload has been terminated or completed. The struct guards with terminated/completed flags so that writes after lifecycle end fail fast instead of corrupting state or sending parts to storage after finalization.

Source

Thrown at foundations/stream/internal/pkg/mediaconvert/multipart.go:82

	logger := log.FromContext(ctx).With(zap.String("multipart", "upload"), zap.String("uploadID", uploadID))

	return &MultipartUpload{
		logger:      logger,
		buffer:      bytes.NewBuffer(nil),
		info:        info,
		storage:     multipartStorage,
		objectName:  objectName,
		uploadID:    uploadID,
		parts:       make([]storage.MultipartPart, 0),
		nextPartNum: 1,
	}, nil
}

// Write writes chunk of data to the storage
func (w *MultipartUpload) Write(ctx context.Context, data []byte) error {
	if w.terminated || w.completed {
		return errors.New("upload already terminated or completed")
	}

	if err := ctx.Err(); err != nil {
		return err
	}

	_, err := w.buffer.Write(data)
	if err != nil {
		return errors.Wrap(err, "failed to write to buffer")
	}
	w.bytesWritten += int64(len(data))

	// flush parts of at least minPartSize
	for w.buffer.Len() >= minPartSize {
		partNum := w.nextPartNum
		partData := w.buffer.Next(minPartSize)

		if err := ctx.Err(); err != nil {

View on GitHub (pinned to 63e28dc964)

Solutions

  1. Check and respect the upload lifecycle: stop writing once Complete/Terminate was issued
  2. Serialize access: ensure only one goroutine writes to a given MultipartUpload, or guard calls with a mutex
  3. Fix retry logic to create a new MultipartUpload (or resume properly) instead of reusing a finished one
  4. Log/inspect the code path that called Write after completion — it usually indicates a duplicated callback

Example fix

// before
await upload.Write(ctx, chunk) // after upload.Complete(ctx)
// after
if err := ctx.Err(); err == nil && !upload.IsFinished() {
    err = upload.Write(ctx, chunk)
}
Defensive patterns

Strategy: validation

Validate before calling

type finished interface{ IsFinished() bool }
func canWrite(u *MultipartUpload) bool {
    return u != nil && !u.terminated && !u.completed
}

Type guard

func writable(u *MultipartUpload) bool {
    return u != nil && !u.terminated && !u.completed
}

Try / catch

if err := upload.Write(ctx, chunk); err != nil {
    if err.Error() == "upload already terminated or completed" {
        return ErrUploadClosed // stop retrying on this handle
    }
    return err
}

Prevention

When it happens

Trigger: Calling Write after Complete() finished the upload; calling Write after Terminate()/Abort(); a retry loop continuing to push chunks after a concurrent goroutine completed or terminated the upload (data race on the flags).

Common situations: Retry logic that doesn't check upload state between attempts; concurrent writers sharing one MultipartUpload without synchronization; resuming a finished tus/stream upload and writing trailing bytes.

Related errors


AI-assisted analysis of hcengineering/platform@63e28dc964 (2026-08-29). Data as JSON: /api/errors/0bb52472e50fa86a. Report an issue: GitHub.