SumeUploadError at step put: the storage upload failed

uploadFile makes three calls. Read SumeUploadError.step to see which failed, and why extra headers on the put step can break the presigned signature.

4 min readSume
All posts

SumeUploadError has a step field, and it is the whole diagnosis: create, put or complete. A failure at put means the storage request failed, not the Sume API, and a likely cause is a header the presigned URL did not sign: the SDK source says adding other headers breaks the signature, so a proxy or a custom fetch that adds one is the first suspect. Pass the SDK's own transport, or none, for that hop, and the upload goes through.

What are the three steps?

uploadFile turns bytes into a durable HTTPS URL you can pass as Format input. The bytes never go through the API. The helper reserves a presigned PUT, sends the bytes to storage, then completes the asset. Only completion mints the durable public URL.

The three uploadFile calls and their failure meaning. Source: TypeScript SDK, docs.sume.com/sdk, and the SDK source in the Sume repository, read 2026-10-03.
stepCallTypical failure
createPOST /v1/assets/upload-urlMissing contentType (status undefined), bad key, 4xx from the API
putPUT to the presigned storage URLStorage refused the PUT (for example altered signed headers), or the transfer was aborted
completePOST /v1/assets/{id}/completeSize mismatch, or completion that returns no public URL

Why can the put step fail behind a proxy?

The presigned request is signed over a specific set of headers. The helper sends exactly the headers the presign asked for, plus the content type, and nothing else, because adding any other header breaks the signature. If you inject a fetch through createSumeClient({ fetch }) that attaches an Authorization header, a tracing header or a gateway credential to every call, that wrapper also touches the storage hop, since uploadFile reuses the client's transport by default.

The fix is the fetch option on uploadFile itself. It overrides the transport for the storage PUT only, so the two API calls keep your instrumented client and the bytes go out with a clean request.

import { SumeUploadError, uploadFile } from "@sume-com/sdk";

export async function putImage(client, bytes: Uint8Array) {
  try {
    return await uploadFile({
      client,
      file: bytes,
      contentType: "image/png",
      filename: "look.png",
      // Plain fetch for the storage PUT; the presign signed its headers.
      fetch: globalThis.fetch.bind(globalThis),
    });
  } catch (error) {
    if (error instanceof SumeUploadError) {
      console.error(error.step, error.status);
    }
    throw error;
  }
}

What about the create step and contentType?

contentType is required unless file is a Blob that already carries a non-empty type. A raw Uint8Array or ArrayBuffer has no type, so omitting it throws a SumeUploadError at step create with status undefined before any request is made. Also keep the returned url for Format input rather than calling the separate download endpoint, which returns a short-lived presigned URL that must not be handed to a Format.

When does complete fail?

The helper checks that completion returned a public URL. If the asset comes back without one, it throws at step complete instead of returning a result whose url is empty, which would fail later inside a Format run where the cause is hard to see. Re-run the upload from the start; the failed asset is not usable.

How do I use the URL afterwards?

uploadFile resolves with url, asset_id, content_type and size_bytes. The url is the durable HTTPS address and is safe to pass as Format input. Media URLs on Sume are public to anyone who holds them, so treat an uploaded customer file the way you would any link you have shared.

If you get a 400 attachment_not_found later, you passed an asset_id the workspace does not know: upload it again or pass the URL instead. Attachments have their own limits, an image over 30 MB or a set over 500 MB answering 413 attachment_too_large, so resize before you upload rather than after.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume