A multipart message contains a body with many component body parts. Each body part has a content type, a body, and headers (which are optional). Multipart messages are typically used in order to avoid multiple request/responses, or to provide different representations of the same data.

The OpenEdge.Net library supports the transmission of multipart messages through the OpenEdge.Net.MultipartEntity and the OpenEdge.Net.MessagePart classes.

To create a request body consisting of multiple parts, create multiple MessagePart objects, add them to a MultipartEntity object, and pass the MultipartEntity as the request body.

Each MessagePart can have two parameters—the content type (string) and an object. The object can be string/longchar, a string or JSON object, or a binary file (for example, a ByteBucket).
Note: When a binary file is present as part of a multipart request, the entire data payload will be base64 encoded for transfer. If all parts of the message are strings then they will remain as such and will not be encoded during the tranmission and subsequent reading of the request.
using OpenEdge.Core.ByteBucket.
using OpenEdge.Net.HTTP.IHttpClient.
using OpenEdge.Net.HTTP.IHttpRequest.
using OpenEdge.Net.HTTP.IHttpResponse.
using OpenEdge.Net.HTTP.ClientBuilder.
using OpenEdge.Net.HTTP.RequestBuilder.
using OpenEdge.Net.MessagePart.
using OpenEdge.Net.MultipartEntity.
using OpenEdge.Net.URI.
using Progress.Json.ObjectModel.JsonObject.

define variable oURI      as URI                no-undo.
define variable oClient   as IHttpClient        no-undo.
define variable oRequest  as IHttpRequest       no-undo. 
define variable oResponse as IHttpResponse      no-undo.
define variable oJsonBody as JsonObject         no-undo.
define variable oJsonPart as JsonObject         no-undo.
define variable oFile     as OpenEdge.Core.File no-undo.
define variable oPart1    as MessagePart        no-undo.
define variable oPart2    as MessagePart        no-undo.
define variable oPart3    as MessagePart        no-undo.
define variable oEntity   as MultipartEntity    no-undo.
define variable mData     as memptr             no-undo.
define variable oBinary   as ByteBucket         no-undo.

// Create first message part - a String
define variable oText as OpenEdge.Core.String no-undo.
oText = new OpenEdge.Core.String("Some sample text").
oPart1 = new MessagePart('text/plain', oText).


// Create second message part - a JSON object
oJsonPart = new JsonObject().
oJsonPart:Add("CustNum", "200").
oJsonPart:Add("Name", "Excellent Sports Apparel").
oPart2 = new MessagePart('application/json', oJsonPart).


// Create third message part - a binary file
oFile = new OpenEdge.Core.File(search("test-image.jpg")).
oBinary = ByteBucket:Instance().

if oFile:Exists and oFile:CanRead then do:
    // Copy to a memptr primitive and add to a ByteBucket object instance.
    copy-lob from file oFile:AbsolutePath() to mData.
    oBinary:PutBytes(mData). // Put data into the ByteBucket.
end.
oPart3 = new MessagePart('image/jpg', oBinary).

// Create the multipart entity
oEntity = new MultipartEntity().
oEntity:AddPart(oPart1).
oEntity:AddPart(oPart2).
oEntity:AddPart(oPart3). // Note: This will cause the payload to be base64 encoded

// Build a request to a PAS instance with a custom WebHandler
oURI = URI:Parse("http://localhost:8840/web/multipart").
oRequest = RequestBuilder:POST(oURI, oEntity):Request.

// Execute via client instance
oClient = ClientBuilder:Build():Client.
oResponse = oClient:Execute(oRequest).

// Process the response and write out a response file with context.
if oResponse:StatusCode ne 200 then
    message "Response Status: " + string(oResponse:StatusCode).
else do:
    if valid-object(oResponse:Entity) and
       oResponse:Entity:GetClass():TypeName eq "Progress.Json.ObjectModel.JsonObject" then
        oJsonBody = cast(oResponse:Entity, JsonObject).

    if valid-object(oJsonBody) then
        oJsonBody:WriteFile("response.json", true).

    message "See 'response.json' for server output".
end.

finally:
    oBinary:Clear().
    set-size(mData) = 0.
end finally.
Note: The test-image.jpg referenced in the third message part of the sample code can be any *.jpeg image located in the PROPATH for the code when executed.
*------------------------------------------------------------------------
    File        : MultipartHandler
    Purpose     : Respond to a POST with multipart data in a request body
  ----------------------------------------------------------------------*/

using OpenEdge.Core.ByteBucket.
using OpenEdge.Net.HTTP.IHttpResponse.
using OpenEdge.Net.HTTP.StatusCodeEnum.
using OpenEdge.Net.MessagePart.
using OpenEdge.Net.MultipartEntity.
using OpenEdge.Web.IWebRequest.
using OpenEdge.Web.WebResponseWriter.
using Progress.Json.ObjectModel.JsonObject.

block-level on error undo, throw.

class MultipartHandler inherits OpenEdge.Web.WebHandler: 

    method override protected integer HandleNotAllowedMethod( input poRequest as IWebRequest ):
        undo, throw new Progress.Lang.AppError("METHOD NOT IMPLEMENTED").
    end method.

    method override protected integer HandleNotImplemented( input poRequest as IWebRequest ):
        undo, throw new Progress.Lang.AppError("METHOD NOT IMPLEMENTED").
    end method.

    method override protected integer HandlePost( input poRequest as OpenEdge.Web.IWebRequest ):
        var IHttpResponse oResponse = new OpenEdge.Web.WebResponse().
        var WebResponseWriter oWriter = new WebResponseWriter(oResponse).
        var MultipartEntity oEntity.
        var JsonObject oBody = new JsonObject().
        var MessagePart[] oPart.
        var integer iParts, iX.
        var character cFile = session:temp-directory + "output".

        // We'll always assume this to be a valid response.
        assign oResponse:StatusCode = integer(StatusCodeEnum:OK).

        // Get the multipart entity expected as the request.
        if poRequest:ContentType begins "multipart" then
            oEntity = cast(poRequest:GetTypedEntity(), MultipartEntity).

        // Add info to the body to prove we have our multipart request.
        if valid-object(oEntity) then do:
            assign iParts = oEntity:size. // Get the size once.
            extent(oPart) = iParts. // Initialize the extent.

            oBody:Add("size", iParts).
            oBody:Add("boundary", oEntity:Boundary).
            
            do iX = 1 to iParts:
                oPart[iX] = oEntity:GetPart(iX).
                oBody:Add(substitute("part&1", iX), new JsonObject()).
                oBody:GetJsonObject(substitute("part&1", iX)):Add("ContentType", oPart[iX]:ContentType).
                oBody:GetJsonObject(substitute("part&1", iX)):Add("Object", oPart[iX]:Body:GetClass():TypeName).
                case true:
                    when oPart[iX]:Body:GetClass():IsA(get-class(OpenEdge.Core.String)) then
                        // Report the string data recieved by the server.
                        oBody:GetJsonObject(substitute("part&1", iX)):Add("Data", cast(oPart[iX]:Body, OpenEdge.Core.String):Value).

                    when oPart[iX]:Body:GetClass():IsA(get-class(JsonObject)) then
                        // Report the JSON data recieved by the server.
                        oBody:GetJsonObject(substitute("part&1", iX)):Add("Data", cast(oPart[iX]:Body, JsonObject)).

                    when oPart[iX]:Body:GetClass():IsA(get-class(ByteBucket)) then do:
                        // Report the size of the binary data recieved by the server.
                        oBody:GetJsonObject(substitute("part&1", iX)):Add("Size", cast(oPart[iX]:Body, ByteBucket):Size).

                        // Write the file to disk and tell the requester where it was placed.
                        cFile = substitute("&1.&2", cFile, entry(2, oPart[iX]:ContentType, "/")).
                        copy-lob from cast(oPart[iX]:Body, ByteBucket):Value to file cFile.
                        oBody:GetJsonObject(substitute("part&1", iX)):Add("File", cFile).
                    end.

                    otherwise
                        oBody:GetJsonObject(substitute("part&1", iX)):Add("Data", "Unsupported datatype provided").
                end case.
            end.
        end.
        else
            oBody:Add("error", "Not a multipart request").

        assign // Return JSON payload with appropriate content type.
            oResponse:Entity        = oBody
            oResponse:ContentType   = 'application/json':u
            oResponse:ContentLength = length(oBody:GetJsonText())
            .

        // Writes out the status line and all headers before the message body/entity.
        oWriter:Open().

        // Finish writing the response message and close the response.
        oWriter:Close().

        return 0.
    end method.

end class.