AL icon indicating copy to clipboard operation
AL copied to clipboard

Support for MultipartFormDataContent

Open paulcart opened this issue 7 years ago • 10 comments

Hi,

I'm writing an extension that needs to send a CSV file to a 3rd party webservice. In C/AL I would've used HttpClient and the MultipartFormDataContent class, however in AL I only have HttpClient, not MultipartFormDataContent. Are there any plans to include this? If not, does anyone know of a way to get around this limitation?

Thanks in advance, Paul

paulcart avatar Apr 19 '18 10:04 paulcart

Maybe this Code can help you, it is written in C/AL (but maybe you can translate it to AL) and constructs the multipart form data manually:

`// Multipart Mgt DateTime := DateTime.Now(); boundary := '----------------------------' + FORMAT(DateTime.Ticks);

HttpRequestMgt.Initialize(GetApiBaseUri + relativeUri); HttpRequestMgt.DisableUI; HttpRequestMgt.SetContentType('application/octet-stream'); HttpRequestMgt.SetReturnType('application/json'); HttpRequestMgt.SetContentType(STRSUBSTNO('multipart/form-data; charset=UTF-8; boundary=%1', boundary)); HttpRequestMgt.SetMethod('POST'); CreateHeader(HttpRequestMgt);

// Multipart Data memStream := memStream.MemoryStream(); StreamWriter := StreamWriter.StreamWriter(memStream, Encoding.UTF8, 2048, TRUE); // Leave Stream Open

// Start Part of Multipart StreamWriter.Write(CRLF +'--' + boundary + CRLF);

// File Content Disposition StreamWriter.Write( STRSUBSTNO('Content-Disposition: form-data; name="%1"; filename="%2"' + CRLF + 'Content-Type: application/octet-stream' + CRLF + CRLF, 'file', Filename)); StreamWriter.Flush;

// File Content Data TempBlob.Blob.CREATEINSTREAM(IStream); baseStream := IStream; baseStream.CopyTo(memStream, 2048);

// last boundary StreamWriter.Write(CRLF +'--' + boundary + '--' + CRLF);

// Flush Stream Writer (Save to Stream) StreamWriter.Flush;

// Write Memory Stream to TempBlob & to Body TempBlob2.Blob.CREATEOUTSTREAM(OStream); COPYSTREAM(OStream, memStream); HttpRequestMgt.AddBodyBlob(TempBlob2);

CreateResponseStream(ResponseStream); IF HttpRequestMgt.GetResponse(ResponseStream, HttpStatusCode, ResponseHeaders) THEN BEGIN StreamReader := StreamReader.StreamReader(ResponseStream, Encoding.UTF8); EVALUATE(DocumentId, StreamReader.ReadToEnd()); END ELSE BEGIN HttpRequestMgt.ProcessFaultResponse('');
END;`

marknitek avatar Apr 24 '18 05:04 marknitek

Hi @paulcart . We do have adding more http types on our backlog but right now we only support HttpContent, HttpClient, HttpRequestMessage, HttpResponseMessage and HttpHeaders.

Please consider making a proposal for the https://github.com/Microsoft/cal-open-library or try making it work with the currently available types.

doivosevic avatar Apr 24 '18 12:04 doivosevic

Thanks for the code sample @marknitek that's really useful, I'll see if I can convert what it's doing to AL.

paulcart avatar Apr 25 '18 14:04 paulcart

Hi Paul,

Did you manage to convert the code to AL? I'm in a similar situation as you; however, my issue is when streaming a Multipart data to a HttpContent only one line is appended to the class

Many Thanks, Pedro

PedroLReis avatar May 24 '18 12:05 PedroLReis

Hi @PedroLReis - no unfortunately I never got it to work

paulcart avatar May 25 '18 11:05 paulcart

Any update for this issue ?

I'm in the same situation from a year, I managed it by creating and calling an Azure Function but I want to do it in full AL to make my app more maintainable.

Thanks

judecot avatar May 21 '19 07:05 judecot

Hi Julie,

Below is what I used to generate a multipart-form request message with a file attached, let me know if it works for you.

  1. The function I used to build Multipart Msg

    procedure UploadFile(var AttachmentInStream : InStream;FileID : Text;SendType : Text;NavUserID : Code[50];DocumentType : Text;FileName : Text;var StatusCode : Text[30]) : Text; var ContentTypeTxt : TextConst Comment='{Locked}',ENU='multipart/form-data; boundary=%1',ENG='multipart/form-data; boundary=%1'; TempBLOB : Record TempBlob temporary; TempBlobMultiForm : Record TempBlob; ResponseInStream : InStream; RequestInStream : InStream; IntegrationCode : Code[20]; RequestSuccessful : Boolean; ToFileName : Text; ResponseBlob : Record TempBlob temporary; begin

    ///// // This function generates multipart-form request message with file attachment and // Send it to Online. Returns uploaded file ID in Online System /////

    GetSetup(NavUserID); SetBaseSetup; EndPoint:= SettingsManagement.GetFileURL;

    IntegrationCode:= SettingsManagement.GetWSCodeUploadFile; TempBLOB.Blob.CREATEINSTREAM(ResponseInStream);

    CLEARLASTERROR;

    MultiFormManagement.Initialize;

    Connector.SetContextType(STRSUBSTNO(ContentTypeTxt, MultiFormManagement.GenerateBoundary)); MultiFormManagement.AddBodyElement('fileId', FileID); MultiFormManagement.AddBodyElement('sendType', SendType); if SettingsManagement.GetCompanyID <> '' then MultiFormManagement.AddBodyElement('companyId', SettingsManagement.GetCompanyID); MultiFormManagement.AddBodyElement('encoding', SettingsManagement.GetEncoding); MultiFormManagement.AddBodyElement('documentType', DocumentType);

    MultiFormManagement.AddFile( 'payload', '', FileName, AttachmentInStream); MultiFormManagement.FinishStream; MultiFormManagement.GetBlob(TempBlobMultiForm);

    TempBlobMultiForm.Blob.CREATEINSTREAM(RequestInStream);
    LogMgt.InitLogEntry(IntegrationCode, 1, 2, BaseURL, EndPoint, '', RequestInStream);

    PostRequest(EndPoint,TempBlobMultiForm,3,ResponseInStream, RequestSuccessful,StatusCode,ResponseBlob);

    if RequestSuccessful then exit(Connector.GetResponseHeaderValue('Location')) else exit(''); end;

  2. Codeunit with required functions

codeunit xxxx " MultiPartForm Msg Build" {

trigger OnRun();
begin
end;

var
    FileManagement : Codeunit "File Management";
    TempBlob : Record TempBlob;
    Boundary : Text;
    NewLine : Text[2];
    BodyOutStream : OutStream;
    Text001 : TextConst ENU='Content-Disposition: form-data; name="%1"',ENG='Content-Disposition: form-data; name="%1"';
    Text002 : TextConst ENU='Content-Disposition: form-data; name="%1"; filename="%2"',ENG='Content-Disposition: form-data; name="%1"; filename="%2"';
    Text003 : TextConst ENU='Content-Type: %1',ENG='Content-Type: %1';

procedure GenerateBoundary() : Text;
begin
    Boundary := DELCHR(FORMAT(CURRENTDATETIME),'=',DELCHR(FORMAT(CURRENTDATETIME,1000),'=','1234567890'));
    exit(Boundary);
end;

procedure Initialize();
begin
    TempBlob.INIT();
    TempBlob.Blob.CREATEOUTSTREAM(BodyOutStream);
    NewLine[1] := 13;
    NewLine[2] := 10;
    CLEAR(Boundary);
end;

procedure AddBodyElement(Element : Text;ElementValue : Text);
var
    BodyText : BigText;
begin
    BodyText.ADDTEXT('--' + Boundary);
    BodyText.ADDTEXT(NewLine);
    BodyText.ADDTEXT(STRSUBSTNO(Text001,Element));
    BodyText.ADDTEXT(NewLine);
    BodyText.ADDTEXT(NewLine);
    BodyText.ADDTEXT(ElementValue);
    BodyText.ADDTEXT(NewLine);
    BodyText.WRITE(BodyOutStream);
end;

procedure AddFile(Element : Text;ElementValue : Text;FileName : Text;FileInStream : InStream);
var
    BodyText : BigText;
    StreamByte : Byte;
begin
    BodyText.ADDTEXT('--' + Boundary);
    BodyText.ADDTEXT(NewLine);
    BodyText.ADDTEXT(STRSUBSTNO(Text002,Element,FileName));
    BodyText.ADDTEXT(NewLine);
    BodyText.WRITE(BodyOutStream);
    AddContentType(FileName);
    CLEAR(BodyText);
    BodyText.ADDTEXT(NewLine);
    BodyText.WRITE(BodyOutStream);
    while not FileInStream.EOS() do begin
      FileInStream.READ(StreamByte);
      BodyOutStream.WRITE(StreamByte);
    end;
end;

procedure GetBlob(var pTempBlob : Record TempBlob);
var
    InStreamTest : InStream;
    OutStreamTest : OutStream;
begin
    TempBlob.Blob.CREATEINSTREAM(InStreamTest);
    pTempBlob.Blob.CREATEOUTSTREAM(OutStreamTest);
    COPYSTREAM(OutStreamTest, InStreamTest);
end;

local procedure AddContentType(FileName : Text);
begin
    case FileManagement.GetExtension(FileName) of
      'zip' : begin
                AddContentTypeLine('application/x-zip-compressed');
              end;
      'xml' : begin
                AddContentTypeLine('text/xml');
              end;
    end;
end;

local procedure AddContentTypeLine(ContentType : Text);
var
    BodyText : BigText;
begin
    BodyText.ADDTEXT(STRSUBSTNO(Text003,ContentType));
    BodyText.ADDTEXT(NewLine);
    BodyText.WRITE(BodyOutStream);
end;

procedure FinishStream();
var
    BodyText : BigText;
begin
    BodyText.ADDTEXT(NewLine);
    BodyText.ADDTEXT('--' + Boundary+'--');
    BodyText.WRITE(BodyOutStream);
end;

}

PedroLReis avatar May 21 '19 08:05 PedroLReis

@PedroLReis
Yes your code works for me ! Thanks a lot! Still hope we will have native support for MultiParFormDataContent in AL. Even if it works, for now we have for each project which needs multipart to paste this object, or deliver a mother app containing this object on which we depends on

judecot avatar May 24 '19 10:05 judecot

Hi Julie,

Below is what I used to generate a multipart-form request message with a file attached, let me know if it works for you.

  1. The function I used to build Multipart Msg procedure UploadFile(var AttachmentInStream : InStream;FileID : Text;SendType : Text;NavUserID : Code[50];DocumentType : Text;FileName : Text;var StatusCode : Text[30]) : Text; var ContentTypeTxt : TextConst Comment='{Locked}',ENU='multipart/form-data; boundary=%1',ENG='multipart/form-data; boundary=%1'; TempBLOB : Record TempBlob temporary; TempBlobMultiForm : Record TempBlob; ResponseInStream : InStream; RequestInStream : InStream; IntegrationCode : Code[20]; RequestSuccessful : Boolean; ToFileName : Text; ResponseBlob : Record TempBlob temporary; begin ///// // This function generates multipart-form request message with file attachment and // Send it to Online. Returns uploaded file ID in Online System ///// GetSetup(NavUserID); SetBaseSetup; EndPoint:= SettingsManagement.GetFileURL; IntegrationCode:= SettingsManagement.GetWSCodeUploadFile; TempBLOB.Blob.CREATEINSTREAM(ResponseInStream); CLEARLASTERROR; MultiFormManagement.Initialize; Connector.SetContextType(STRSUBSTNO(ContentTypeTxt, MultiFormManagement.GenerateBoundary)); MultiFormManagement.AddBodyElement('fileId', FileID); MultiFormManagement.AddBodyElement('sendType', SendType); if SettingsManagement.GetCompanyID <> '' then MultiFormManagement.AddBodyElement('companyId', SettingsManagement.GetCompanyID); MultiFormManagement.AddBodyElement('encoding', SettingsManagement.GetEncoding); MultiFormManagement.AddBodyElement('documentType', DocumentType); MultiFormManagement.AddFile( 'payload', '', FileName, AttachmentInStream); MultiFormManagement.FinishStream; MultiFormManagement.GetBlob(TempBlobMultiForm); TempBlobMultiForm.Blob.CREATEINSTREAM(RequestInStream); LogMgt.InitLogEntry(IntegrationCode, 1, 2, BaseURL, EndPoint, '', RequestInStream); PostRequest(EndPoint,TempBlobMultiForm,3,ResponseInStream, RequestSuccessful,StatusCode,ResponseBlob); if RequestSuccessful then exit(Connector.GetResponseHeaderValue('Location')) else exit(''); end;
  2. Codeunit with required functions

codeunit xxxx " MultiPartForm Msg Build" {

trigger OnRun();
begin
end;

var
    FileManagement : Codeunit "File Management";
    TempBlob : Record TempBlob;
    Boundary : Text;
    NewLine : Text[2];
    BodyOutStream : OutStream;
    Text001 : TextConst ENU='Content-Disposition: form-data; name="%1"',ENG='Content-Disposition: form-data; name="%1"';
    Text002 : TextConst ENU='Content-Disposition: form-data; name="%1"; filename="%2"',ENG='Content-Disposition: form-data; name="%1"; filename="%2"';
    Text003 : TextConst ENU='Content-Type: %1',ENG='Content-Type: %1';

procedure GenerateBoundary() : Text;
begin
    Boundary := DELCHR(FORMAT(CURRENTDATETIME),'=',DELCHR(FORMAT(CURRENTDATETIME,1000),'=','1234567890'));
    exit(Boundary);
end;

procedure Initialize();
begin
    TempBlob.INIT();
    TempBlob.Blob.CREATEOUTSTREAM(BodyOutStream);
    NewLine[1] := 13;
    NewLine[2] := 10;
    CLEAR(Boundary);
end;

procedure AddBodyElement(Element : Text;ElementValue : Text);
var
    BodyText : BigText;
begin
    BodyText.ADDTEXT('--' + Boundary);
    BodyText.ADDTEXT(NewLine);
    BodyText.ADDTEXT(STRSUBSTNO(Text001,Element));
    BodyText.ADDTEXT(NewLine);
    BodyText.ADDTEXT(NewLine);
    BodyText.ADDTEXT(ElementValue);
    BodyText.ADDTEXT(NewLine);
    BodyText.WRITE(BodyOutStream);
end;

procedure AddFile(Element : Text;ElementValue : Text;FileName : Text;FileInStream : InStream);
var
    BodyText : BigText;
    StreamByte : Byte;
begin
    BodyText.ADDTEXT('--' + Boundary);
    BodyText.ADDTEXT(NewLine);
    BodyText.ADDTEXT(STRSUBSTNO(Text002,Element,FileName));
    BodyText.ADDTEXT(NewLine);
    BodyText.WRITE(BodyOutStream);
    AddContentType(FileName);
    CLEAR(BodyText);
    BodyText.ADDTEXT(NewLine);
    BodyText.WRITE(BodyOutStream);
    while not FileInStream.EOS() do begin
      FileInStream.READ(StreamByte);
      BodyOutStream.WRITE(StreamByte);
    end;
end;

procedure GetBlob(var pTempBlob : Record TempBlob);
var
    InStreamTest : InStream;
    OutStreamTest : OutStream;
begin
    TempBlob.Blob.CREATEINSTREAM(InStreamTest);
    pTempBlob.Blob.CREATEOUTSTREAM(OutStreamTest);
    COPYSTREAM(OutStreamTest, InStreamTest);
end;

local procedure AddContentType(FileName : Text);
begin
    case FileManagement.GetExtension(FileName) of
      'zip' : begin
                AddContentTypeLine('application/x-zip-compressed');
              end;
      'xml' : begin
                AddContentTypeLine('text/xml');
              end;
    end;
end;

local procedure AddContentTypeLine(ContentType : Text);
var
    BodyText : BigText;
begin
    BodyText.ADDTEXT(STRSUBSTNO(Text003,ContentType));
    BodyText.ADDTEXT(NewLine);
    BodyText.WRITE(BodyOutStream);
end;

procedure FinishStream();
var
    BodyText : BigText;
begin
    BodyText.ADDTEXT(NewLine);
    BodyText.ADDTEXT('--' + Boundary+'--');
    BodyText.WRITE(BodyOutStream);
end;

}

Hi @PedroLReis i'm trying to do this but I don't know what variable is "connector" and wha does "PostRequest" does. Can you help me? Thanks a lot!

pd: @JulienDecottignies maybe you can help me too, seeing you made the code work :D

onzema avatar Mar 24 '22 09:03 onzema

Hi Julie, Below is what I used to generate a multipart-form request message with a file attached, let me know if it works for you.

  1. The function I used to build Multipart Msg procedure UploadFile(var AttachmentInStream : InStream;FileID : Text;SendType : Text;NavUserID : Code[50];DocumentType : Text;FileName : Text;var StatusCode : Text[30]) : Text; var ContentTypeTxt : TextConst Comment='{Locked}',ENU='multipart/form-data; boundary=%1',ENG='multipart/form-data; boundary=%1'; TempBLOB : Record TempBlob temporary; TempBlobMultiForm : Record TempBlob; ResponseInStream : InStream; RequestInStream : InStream; IntegrationCode : Code[20]; RequestSuccessful : Boolean; ToFileName : Text; ResponseBlob : Record TempBlob temporary; begin ///// // This function generates multipart-form request message with file attachment and // Send it to Online. Returns uploaded file ID in Online System ///// GetSetup(NavUserID); SetBaseSetup; EndPoint:= SettingsManagement.GetFileURL; IntegrationCode:= SettingsManagement.GetWSCodeUploadFile; TempBLOB.Blob.CREATEINSTREAM(ResponseInStream); CLEARLASTERROR; MultiFormManagement.Initialize; Connector.SetContextType(STRSUBSTNO(ContentTypeTxt, MultiFormManagement.GenerateBoundary)); MultiFormManagement.AddBodyElement('fileId', FileID); MultiFormManagement.AddBodyElement('sendType', SendType); if SettingsManagement.GetCompanyID <> '' then MultiFormManagement.AddBodyElement('companyId', SettingsManagement.GetCompanyID); MultiFormManagement.AddBodyElement('encoding', SettingsManagement.GetEncoding); MultiFormManagement.AddBodyElement('documentType', DocumentType); MultiFormManagement.AddFile( 'payload', '', FileName, AttachmentInStream); MultiFormManagement.FinishStream; MultiFormManagement.GetBlob(TempBlobMultiForm); TempBlobMultiForm.Blob.CREATEINSTREAM(RequestInStream); LogMgt.InitLogEntry(IntegrationCode, 1, 2, BaseURL, EndPoint, '', RequestInStream); PostRequest(EndPoint,TempBlobMultiForm,3,ResponseInStream, RequestSuccessful,StatusCode,ResponseBlob); if RequestSuccessful then exit(Connector.GetResponseHeaderValue('Location')) else exit(''); end;
  2. Codeunit with required functions

codeunit xxxx " MultiPartForm Msg Build" {

trigger OnRun();
begin
end;

var
    FileManagement : Codeunit "File Management";
    TempBlob : Record TempBlob;
    Boundary : Text;
    NewLine : Text[2];
    BodyOutStream : OutStream;
    Text001 : TextConst ENU='Content-Disposition: form-data; name="%1"',ENG='Content-Disposition: form-data; name="%1"';
    Text002 : TextConst ENU='Content-Disposition: form-data; name="%1"; filename="%2"',ENG='Content-Disposition: form-data; name="%1"; filename="%2"';
    Text003 : TextConst ENU='Content-Type: %1',ENG='Content-Type: %1';

procedure GenerateBoundary() : Text;
begin
    Boundary := DELCHR(FORMAT(CURRENTDATETIME),'=',DELCHR(FORMAT(CURRENTDATETIME,1000),'=','1234567890'));
    exit(Boundary);
end;

procedure Initialize();
begin
    TempBlob.INIT();
    TempBlob.Blob.CREATEOUTSTREAM(BodyOutStream);
    NewLine[1] := 13;
    NewLine[2] := 10;
    CLEAR(Boundary);
end;

procedure AddBodyElement(Element : Text;ElementValue : Text);
var
    BodyText : BigText;
begin
    BodyText.ADDTEXT('--' + Boundary);
    BodyText.ADDTEXT(NewLine);
    BodyText.ADDTEXT(STRSUBSTNO(Text001,Element));
    BodyText.ADDTEXT(NewLine);
    BodyText.ADDTEXT(NewLine);
    BodyText.ADDTEXT(ElementValue);
    BodyText.ADDTEXT(NewLine);
    BodyText.WRITE(BodyOutStream);
end;

procedure AddFile(Element : Text;ElementValue : Text;FileName : Text;FileInStream : InStream);
var
    BodyText : BigText;
    StreamByte : Byte;
begin
    BodyText.ADDTEXT('--' + Boundary);
    BodyText.ADDTEXT(NewLine);
    BodyText.ADDTEXT(STRSUBSTNO(Text002,Element,FileName));
    BodyText.ADDTEXT(NewLine);
    BodyText.WRITE(BodyOutStream);
    AddContentType(FileName);
    CLEAR(BodyText);
    BodyText.ADDTEXT(NewLine);
    BodyText.WRITE(BodyOutStream);
    while not FileInStream.EOS() do begin
      FileInStream.READ(StreamByte);
      BodyOutStream.WRITE(StreamByte);
    end;
end;

procedure GetBlob(var pTempBlob : Record TempBlob);
var
    InStreamTest : InStream;
    OutStreamTest : OutStream;
begin
    TempBlob.Blob.CREATEINSTREAM(InStreamTest);
    pTempBlob.Blob.CREATEOUTSTREAM(OutStreamTest);
    COPYSTREAM(OutStreamTest, InStreamTest);
end;

local procedure AddContentType(FileName : Text);
begin
    case FileManagement.GetExtension(FileName) of
      'zip' : begin
                AddContentTypeLine('application/x-zip-compressed');
              end;
      'xml' : begin
                AddContentTypeLine('text/xml');
              end;
    end;
end;

local procedure AddContentTypeLine(ContentType : Text);
var
    BodyText : BigText;
begin
    BodyText.ADDTEXT(STRSUBSTNO(Text003,ContentType));
    BodyText.ADDTEXT(NewLine);
    BodyText.WRITE(BodyOutStream);
end;

procedure FinishStream();
var
    BodyText : BigText;
begin
    BodyText.ADDTEXT(NewLine);
    BodyText.ADDTEXT('--' + Boundary+'--');
    BodyText.WRITE(BodyOutStream);
end;

}

Hi @PedroLReis i'm trying to do this but I don't know what variable is "connector" and wha does "PostRequest" does. Can you help me? Thanks a lot!

pd: @juliendecottignies maybe you can help me too, seeing you made the code work :D

@onzema did you get your answer?

Oman-Pradhan avatar Feb 22 '24 04:02 Oman-Pradhan