Introduction

We’ve been developing extensions for Microsoft Dynamics Business Central for some time and we stumbled upon a quite interesting problem – sending an HTTP Post request with an attached file.

The Body of the Request

An example request containing a file would have the following body.

POST https://www.someurl.com/ HTTP/1.1
Content-Type: multipart/form-data; boundary=boundary
...

--boundary
Content-Disposition: form-data; name="key1"

value1
--boundary
Content-Disposition: form-data; name="key2"

value2
--boundary
Content-Disposition: form-data; name="file1"; filename="Test.mp3"
Content-Type: application/octet-stream

<File contents go here>
--boundary

When you send such a request with AL, you need to construct the request body yourself.

Binary Data

Everything goes smooth until you get to the point of converting a stream of bytes to text. The problem we encountered was that just using InStream.ReadText produced corrupt file data.

FileInStream.ReadText(FileDataAsText);

The Solution

After spending a lot of time figuring this out, we managed to leverage the use of multiple streams to properly serialize the binary file data to the request body. The trick here is that writing the request content from a stream will retain the encoding. That is why everything is written to a stream and then the stream is used to create the request content.

procedure UploadFile(UploadStream: InStream)
var
    TempBlob: Record TempBlob temporary;
    PayloadOutStream: OutStream;
    PayloadInStream: InStream;
    CR: Char;
    LF: Char;
    NewLine: Text;
    Content: HttpContent;
    ContentHeaders: HttpHeaders;
    Client: HttpClient;
    Request: HttpRequestMessage;
    Response: HttpResponseMessage;
begin
    CR := 13;
    LF := 10;
    NewLine += '' + CR + LF;

    Content.GetHeaders(ContentHeaders);
    ContentHeaders.Clear();
    ContentHeaders.Add('Content-Type', 'multipart/form-data;boundary=boundary');

    // IMPORTANT: The following stream operations are necessary because
    // if we convert the uploaded file stream to text, we will lose encoding
    // and the data will be corrupt.

    // Create a new temporary stream for writing and write all of the
    // request text in it.
    TempBlob.Blob.CreateOutStream(PayloadOutStream);

    PayloadOutStream.WriteText('--boundary' + NewLine);
    // key1 value1
    PayloadOutStream.WriteText('Content-Disposition: form-data; name="key1"' + NewLine);
    PayloadOutStream.WriteText(NewLine);
    PayloadOutStream.WriteText('value1' + NewLine);
    PayloadOutStream.WriteText('--boundary' + NewLine);
    // key2 value2
    PayloadOutStream.WriteText('Content-Disposition: form-data; name="key2"' + NewLine);
    PayloadOutStream.WriteText(NewLine);
    PayloadOutStream.WriteText('value2' + NewLine);
    PayloadOutStream.WriteText('--boundary' + NewLine);
    // file1
    PayloadOutStream.WriteText('Content-Disposition: form-data; name="file1"; fileName="Test.mp3"' + NewLine);
    PayloadOutStream.WriteText('Content-Type: application/octet-stream' + NewLine);
    PayloadOutStream.WriteText(NewLine);
    // Copy all bytes from the uploaded file to the stream.
    CopyStream(PayloadOutStream, UploadStream);
    PayloadOutStream.WriteText(NewLine);
    PayloadOutStream.WriteText('--boundary');

    // Copy all bytes from the write stream to a read stream.
    TempBlob.Blob.CreateInStream(PayloadInStream);

    // Write all bytes from the request body stream.
    Content.WriteFrom(PayloadInStream);

    Request.Content := Content;
    Request.SetRequestUri('https://www.someurl.com/');
    Request.Method := 'POST';

    Client.Send(Request, Response);

    if not Response.IsSuccessStatusCode() then
        Error('Failed to upload the file.');
end;

6 Comments

Victor Sijtsma · May 18, 2020 at 20:51

You Rock!!

This is indeed the right way, it even works with AL language.

Tnx!!

Rasmus · August 26, 2020 at 11:27

Thanks a million!

But whats the deal with the broken data?
Why cant we directly set the content to the instream from our blob on the record?
Why does it have to got to through a temp blob, what is the deal? haah

    Nikolay Arhangelov · August 26, 2020 at 11:49

    The problem was in the combining of the text content of the body with the file bytes. This lead to a loss of encoding. Everything had to be put into a stream and then passed to the request.

Developer · September 23, 2020 at 14:43

I am trying to use this same code and send JSON and PDF in multipart/form-data to Django

Django is receiving the request but its having blank JSON

below is code
BoundaryText2 := DELCHR(FORMAT(CURRENTDATETIME), ‘=’, DELCHR(FORMAT(CURRENTDATETIME, 1000), ‘=’, ‘1234567890’));
BoundaryText := ‘—-‘ + BoundaryText2;//DELCHR(FORMAT(CURRENTDATETIME), ‘=’, DELCHR(FORMAT(CURRENTDATETIME, 1000), ‘=’, ‘1234567890’));
Multipart := ‘multipart/form-data; boundary=’ + BoundaryText;
////Send
SMBTranMgt.GetAPICredMuti(APICred, Multipart);
//Attach PDF

TempBlob.Blob.CreateOutStream(PayloadOutStream);

PayloadOutStream.WriteText(BoundaryText + NewLine);
// key1 value1
PayloadOutStream.WriteText(‘Content-Disposition: form-data; name=”invoice”‘ + NewLine);
PayloadOutStream.WriteText(NewLine);
PayloadOutStream.WriteText(‘value1’ + NewLine);
PayloadOutStream.WriteText(BoundaryText + NewLine);
// key2 value2
PayloadOutStream.WriteText(‘Content-Disposition: form-data; name=”document_header”‘ + NewLine);
PayloadOutStream.WriteText(NewLine);
PayloadOutStream.WriteText(‘value2’ + NewLine);
PayloadOutStream.WriteText(BoundaryText + NewLine);
// file1
PayloadOutStream.WriteText(‘Content-Disposition: form-data; name=”invoice”; fileName=”‘ + STRSUBSTNO(‘SalesInvoice_%1.Pdf’, SalesInvoiceHeader2.”No.”) + NewLine);
PayloadOutStream.WriteText(‘Content-Type: application/pdf’ + NewLine);
PayloadOutStream.WriteText(NewLine);
// Copy all bytes from the uploaded file to the stream.
CopyStream(PayloadOutStream, DocStream2);
PayloadOutStream.WriteText(NewLine);
PayloadOutStream.WriteText(BoundaryText);

// file2
PayloadOutStream.WriteText(StrSubstNo(‘Content-Disposition: form-data; filename=”%1″‘, ‘document_header’) + NewLine);
PayloadOutStream.WriteText(‘Content-Type: application/json’ + NewLine);
PayloadOutStream.WriteText(NewLine);
// Copy all bytes from the uploaded file to the stream.
CopyStream(PayloadOutStream, UploadStream);
PayloadOutStream.WriteText(NewLine);
PayloadOutStream.WriteText(BoundaryText);
// Copy all bytes from the write stream to a read stream.
TempBlob.Blob.CreateInStream(PayloadInStream);

RequestContent.WriteFrom(PayloadInStream);

contentHeaders := APICred.DefaultRequestHeaders;
RequestContent.GetHeaders(contentHeaders);
contentHeaders.Clear();
contentHeaders.Add(‘Content-Type’, Multipart);

///Call POST Action with JSON Object
APICred.Post(URL, RequestContent, UploadResponse);
IF Not UploadResponse.IsSuccessStatusCode then
Error(‘Error in sending Http request’);

Help is appreciated,

    Nikolay Arhangelov · September 23, 2020 at 16:29

    Did you inspect the request that gets generated and sent? It might be something with the boundaries.

Developer · September 23, 2020 at 18:26

Thanks for your input Boundaries was having issue, I corrected it.

Leave a Reply

Your email address will not be published. Required fields are marked *