Fetch api can't be used for uploading Confluence files

Hi there!

I’m working on a new app, but I’m struggling very hard with the fetch api.
I’m trying to upload a png file to a Confluence page using the PUT REST api endpoint.

So far, I’ve been hitting multiple roadblocks.

1. Problem with wrapped API call
When I wrote my first piece of code using node-fetch and ran it locally, I managed to get everything working with the form-data library for handling the multipart handling.

When I tried the same snippet in a Confluence Forge event trigger, I got errors stating that the file handle couldn’t be cloned (when trying to read from a file) or function() {} could not be cloned (when reading from an ArrayBuffer).

By creating the multipart body myself, I was able to get ahold of these cloning-related errors - but this introduces a lot of hassle in the code base.

2. Header Content-Type multipart is being rewritten as application/json

More problematic is the fact that I kept getting error 415 (unsupported media) while trying to upload the file. Using a plethora of console.log statements, I couldn’t find the cause. The same code running locally with node-fetch worked like a charm.

By using Pipedream, I was able to set up an endpoint and inspect the data being send over there in detail. It turned out that while I set the content-type to multipart, it ends up being application/json - causing the 415 unsupported media of course.

Since this is totally out of my reach to fix, I’m hoping to find a solution over here :slight_smile:
Thanks a lot!

Tom

let base_url = '';
let pageId = '';
//...
let url = `${base_url}/wiki/rest/api/content/${pageId}/child/attachment?status=current`;
let upload_options = {
    method: 'PUT',
    headers: {
        'X-Atlassian-Token': 'nocheck',
        'Accept': 'application/json',
        'Content-Type': `multipart/form-data; boundary=${boundary}`,
        'Authorization': 'Basic CREDENTIALS'
    },
    body: body
}
fetch(url, upload_options).then((upload_result) => {
// No difference when using a product specific version of fetch
//api.asApp().requestConfluence(url, upload_options).then((upload_result) => {
    console.log("Upload complete");
});

PS: I’m using the fetch with API token for now until the permission problem regarding the Forge user account created is resolved - just a temporary solution

1 Like

Hey @TomMoors,

Sorry you had to experience this issue :frowning:

We are aware of this and working on it at the moment. I cannot give a timeline as to when this will be resolved but here is where you can track it: [FRGE-114] Cannot use multipart/form-data APIs with Forge - Ecosystem Jira.

Thanks for reporting this and happy Forging!

Hi @TomMoors,
it is possible to specify the content-type since a few days. If you now create the form-data-payload manually, you can upload attachments in Confuence.
With this source code we are able to upload attachments.

SomeAction.js

import { v4 as uuidv4 } from 'uuid';
import { requestConfluence } from "@forge/bridge";
import {
    generateFormData
} from "./index";
.....
let dataForm = {
	id : "some-id",
	name : "some-name",
	...
}
let attachmentName = dataForm.id + ".dataform.json"
let requestUrl = `/wiki/rest/api/content/${pageId}/child/attachment`;
let boundary = "DataForms-" + uuidv4();
const json = JSON.stringify(dataForm);
let formData = generateFormData(attachmentName, json, dataForm.name, boundary);
requestConfluence(requestUrl,
		{
				method: "PUT",
				body: formData,
				headers: {
						'Accept': 'application/json',
						'X-Atlassian-Token': 'no-check',
						'content-type': 'multipart/form-data; boundary=' + boundary
				},
				credentials: 'include'
		}).then(response => { .....
...

and the index.js

...
export const generateFormData = (attachmentName, dataFormJson, comment, boundary) => {
    let newLine = "\r\n";
    // encapsulation boundary two hyphen characters + the boundary param https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html
    let boundaryMarker = "--";
    let formData = boundaryMarker + boundary + newLine +
        "Content-Disposition: form-data; name=\"file\"; filename=\"" + attachmentName + "\"" + newLine +
        "Content-Type: application/dataforms" + newLine + newLine +
        dataFormJson + newLine +
        boundaryMarker + boundary + newLine +
        "Content-Disposition: form-data; name=\"fileName\"" + newLine + newLine +
        attachmentName + newLine +
        boundaryMarker + boundary + newLine +
        "Content-Disposition: form-data; name=\"title\"" + newLine + newLine +
        attachmentName + newLine +
        boundaryMarker + boundary + newLine +
        "Content-Disposition: form-data; name=\"comment\"" + newLine + newLine +
        comment + newLine +
        boundaryMarker + boundary + newLine +
        "Content-Disposition: form-data; name=\"minorEdit\"" + newLine + newLine +
        "true" + newLine +
        boundaryMarker + boundary + boundaryMarker + newLine
    return formData
}
....

I hope the source code helps.

4 Likes

Thanks Andreas!

Hi @spall

Please, is it possible to attach a pdf file?

I actually get my pdf (in ArrayBuffer) from an external service, and then I try converting the ArrayBuffer to File before sending it to the Confluence attachment api. It attaches, but the file is empty (0 kb).

Please, here is my code (pdfBuffer variable is an ArrayBuffer, which is the response from the external pdf service):

var file = new File(
                          [
                            new Blob([
                              new Uint8Array(pdfBuffer, {
                                type: 'application/pdf',
                              }),
                            ]),
                          ],
                          'fwefwe.pdf',
                          {
                            type: 'application/pdf',
                          }
                        );

                        console.log('file: ', file);

                        let dataForm = {
                          id: item.key,
                          name: item.title,
                          comment:
                            'Snapshot ' +
                            item.key +
                            ' by SoftComply Static Snapshots',
                          file: file,
                        };

                        await saveConfluenceAttachment(
                          `/wiki/rest/api/content/${item.contentId}/child/attachment`,
                          dataForm
                        );

The saveConfluenceAttachment method:

export const saveConfluenceAttachment = async (url, dataForm) => {
  let attachmentName = 'XYZ - ' + uuidv4() + '.pdf';
  let boundary = 'DataForms-' + uuidv4();
  const file = dataForm.file;
  let formData = generateFormData(
    attachmentName,
    file,
    dataForm.comment,
    boundary
  );
...

generateFormData method:

export const generateFormData = (
  attachmentName,
  dataFormFile,
  comment,
  boundary
) => {
  let newLine = '\r\n';
  // encapsulation boundary two hyphen characters + the boundary param https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html
  let boundaryMarker = '--';
  let formData =
    boundaryMarker +
    boundary +
    newLine +
    'Content-Disposition: form-data; name="file"; filename="' +
    attachmentName +
    '"' +
    newLine +
    'Content-Type: application/pdf' +
    newLine +
    newLine +
    dataFormFile +
    newLine +
    boundaryMarker +
    boundary +
    newLine +
    'Content-Disposition: form-data; name="fileName"' +
    newLine +
    newLine +
    attachmentName +
    newLine +
    boundaryMarker +
    boundary +
    newLine +
    'Content-Disposition: form-data; name="title"' +
    newLine +
    newLine +
    attachmentName +
    newLine +
    boundaryMarker +
    boundary +
    newLine +
    'Content-Disposition: form-data; name="comment"' +
    newLine +
    newLine +
    comment +
    newLine +
    boundaryMarker +
    boundary +
    newLine +
    'Content-Disposition: form-data; name="minorEdit"' +
    newLine +
    newLine +
    'true' +
    newLine +
    boundaryMarker +
    boundary +
    boundaryMarker +
    newLine;
  return formData;
};

Hi @OnucheIdoko1 , I didn’t find a note, but I think it didn’t work for me with Blob, only when I simply used a string did it work. I guess that the api (requestConfluence(xx)) has a problem with Blob or with Uint8Array. In total, we have removed the attachment algorithm from our app, because we could only create, edit and delete attachments but could not access the data (see [FRGE-410] Enable Confluence download attachment - Rest API - Ecosystem Jira ).

Hello, @spall Thank you very much for your response.