2

I'm trying to use the Microsoft Graph API's createUploadSession endpoint with the JavaScript SDK and attempting to test the upload with a small file that's just over 3MB. Whenever send a PUT request, I receive the following error response:

{
  "error": {
     "code": "InvalidContentRangeHeader",
     "message": "Invalid Content-Range header."
  }
}

I'm trying to send over the whole file at once, so here's what the request looks like:

  const uploadSession = await client
    .api(`/me/messages/${messageId}/attachments/createUploadSession`)
    .version('beta')
    .post({
      AttachmentItem: {
        attachmentType: 'file',
        name: attachment.name,
        size: attachment.size,
      },
    });

  const res = await fetch(uploadSession.uploadUrl, {
    method: 'PUT',
    headers: {
      'Content-Type': 'application/octet-stream',
      'Content-Range': `bytes 0-${attachment.size - 1}/${attachment.size}`,
      'Content-Length': attachment.size,
    },
    body: attachment.contentBytes,
  });

  const json = await res.json();
  return json;

Is there anything here that I'm missing? If I understand correctly giving the complete range should pass along the entire file.

Shiva Keshav Varma
  • 3,398
  • 2
  • 9
  • 13
xsnvt
  • 61
  • 1
  • 7

1 Answers1

1

The issue might be outdated, since you were using the beta version, and UploadSessions are now part of the core functionality: https://learn.microsoft.com/en-us/graph/outlook-large-attachments.

Anyway, I still decided to share my working implementation of this in JS (TypeScript, to be more correct), since most of questions on this topic are for C#.

I didn't try to upload the whole file at once after creating the uploadSession (though the docs state it's possible), but decided to PUT the whole stuff in 4MB chunks as they recommend.

const BYTE_RANGE = 4e6

...

// Send large attachments using an uploadSession
private async _uploadLargeAttachments(
    attachments: IMSLargeFileAttachment[],
    messageId: string,
    clientInstance: Client
): Promise<string[]> {
    return Promise.all(
        attachments.map(async (attachment) => {
            const { attachmentType, name, size } = attachment
            const uploadSession: UploadSession = await clientInstance
                .api(`/me/messages/${messageId}/attachments/createUploadSession`)
                .post({
                    AttachmentItem: { attachmentType, name, size },
                })

                if (!uploadSession.uploadUrl) {
                    // You may also want to throw an error here
                    return null
                }

            const fragmentsTotal = Math.ceil(size / BYTE_RANGE)
            let i = 0
            let bytesRemaining = size

            while (i < fragmentsTotal) {
                const step = (i + 1) * BYTE_RANGE
                const from = i * BYTE_RANGE
                const to = bytesRemaining > BYTE_RANGE ? step - 1 : size - 1
                bytesRemaining = Math.max(size - step, 0)

                const res = await fetch(uploadSession.uploadUrl, {
                    method: 'PUT',
                    headers: {
                        'Content-Type': 'application/octet-stream',
                        'Content-Length': `${to - from + 1}`,
                        'Content-Range': `bytes ${from}-${to}/${size}`,
                    },
                    body: attachment.body.slice(from, to + 1), // the 2nd slice arg is not inclusive
                })

                // The final response should have the 'Location' header with a long URL,
                // from which you can extract the attachment ID
                if (res.status === 201 && res.headers.has('Location')) {
                    return res.headers.get('Location')
                }
                i++
            }
        }).filter((it) => Boolean(it))
    )
}

Hope this helps anyone struggling with this as I was to reach their goal faster.

maxm
  • 11
  • 3