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;

1 Comment

Victor Sijtsma · May 18, 2020 at 20:51

You Rock!!

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

Tnx!!

Leave a Reply

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