base64dynamics-business-centralbusinesscentral

Document Attachment in Sales Order in Business Central — "Read called with an open stream" Error


I’m trying to create a document attachment in Business Central using this API:

🔗 POST /documentAttachments

However, I always get the following error:

Read called with an open stream or textreader. Please close any open streams or text readers before calling Read. CorrelationId: bee212fd-2651-45e1-b57c-84926816a7ea

I’ve already tried several variations of base64 encoding, but no luck so far. Here's the current version of my function (written in vue3, using Axios):

const attachFileToBCModule = async (payload) => {
  const { module, recordId, file, config } = payload;

  const attachmentEndpoint = replaceUrlPlaceholders(attachmentEndpointFormat, config);

  try {
    const token = await acquireToken('businesscentral');

    // Process file completely before making API call
    const fileContent = await file.arrayBuffer();
    const uint8Array = new Uint8Array(fileContent);
    const binaryString = Array.from(uint8Array, byte => String.fromCharCode(byte)).join('');
    const base64Content = btoa(binaryString);

    const fileName = file.name;
    const fileSize = file.size;

    const uploadUrl2 = `${attachmentEndpoint}/documentAttachments`;

    const uploadResponse2 = await axios.post(
      uploadUrl2,
      {
        fileName: fileName,
        byteSize: fileSize,
        attachmentContent: base64Content,
        parentId: recordId,
        parentType: 'Sales Order',
      },
      {
        headers: {
          Authorization: `Bearer ${token}`,
          'Content-Type': 'application/json',
        },
      }
    );

    return {
      status: 201,
      message: `Attachment uploaded to ${module} successfully.`,
      data: { upload2: uploadResponse2.data },
    };

  } catch (err) {
    console.error('Error uploading attachment:', err?.response?.data || err.message);
    return {
      status: err.response?.status || 400,
      error: err.response?.data?.error?.message || err.response?.data || 'Attachment upload failed',
    };
  }
};

For reference, I also tried the other endpoint:

🔗 POST /attachments

That one works fine—but it uploads the file to Incoming Documents, not the Attachments panel in the Sales Order. From what I understand, the correct endpoint is /documentAttachments.

enter image description here


Solution

  • I found a solution!

    To resolve the issue, I created a custom API for Document Attachment in Business Central.

    Here’s what I did:

    1. Go to the Web Services module in Business Central.

    2. Add a new Web Service:

      • Object Type: Page

      • Object ID: 30080 (this is the ID of the Document Attachment page)

      • Service Name: DocumentAttachments (or any name you prefer for the endpoint)

    After publishing this, you can now consume the API using a POST and PATCH request. Here’s the revised code I used:

    const attachV2DocumentAttachments = async (payload) => {
        const { module, createdOrder, documentFile, config } = payload
        
        const v2DocumentAttachmentsEndpoint = replaceUrlPlaceholders(v2DocumentAttachmentsEndpointFormat, config)
        const attachmentUrl = `${v2DocumentAttachmentsEndpoint}`
        
        try {
          const token = await acquireToken('businesscentral')
          
          // Step 1: Create attachment record with metadata only
          const attachmentRecord = {
            parentId: createdOrder.id,
            fileName: documentFile.name,
            parentType: 'Sales Order'
          }
          
          console.log('Creating attachment record:', attachmentRecord)
          
          const createResponse = await axios.post(
            attachmentUrl,
            attachmentRecord,
            {
              headers: {
                Authorization: `Bearer ${token}`,
                'Content-Type': 'application/json',
              },
            }
          )
    
          console.log('createResponse', createResponse)
          
          const attachmentId = createResponse.data.id
          console.log('Attachment record created with ID:', attachmentId)
          
          // Step 2: Upload the actual file content
          const v2DocumentAttachmentsEndpoint = replaceUrlPlaceholders(v2DocumentAttachmentsEndpointFormat, config)
          const v2DocumentAttachmentsURL = `${v2DocumentAttachmentsEndpoint}(${attachmentId})/attachmentContent`
          
          const uploadResponse = await axios.patch(
            v2DocumentAttachmentsURL,
            documentFile,
            {
              headers: {
                Authorization: `Bearer ${token}`,
                'Content-Type': documentFile.type || 'application/octet-stream',
                'If-Match': '*'
              },
            }
          )
    
          return {
            status: 201,
            message: `Attachment uploaded to ${module} successfully.`,
            data: {
              attachment: createResponse.data,
              upload: uploadResponse.data
            },
          }
            
        } catch (err) {
          console.error('Error uploading attachment:', err?.response?.data || err.message)
          return {
            status: err.response?.status || 400,
            error: err.response?.data?.error?.message || err.response?.data || 'Attachment upload failed',
          }
        }
      }