A customer had a requirement to invoke a REST web service that is designed to accept a multipart/form-data request body. I could not get it to work with the APEX_WEB_SERVICE API package, so I reverted to using the good old UTL_HTTP package. That package provides the required flexibility when composing a multipart/form-data request. Here’s the code fragment I ended up with and its resulting debug output.

set serveroutput on;
declare
  l_attachment blob;
  l_newline varchar2(50) := chr(13) || chr(10);
  lco_boundary constant varchar2(30) := 'gc0p4Jq0M2Yt08jU534c0p';

  l_http_request utl_http.req;
  l_request_body clob;
  l_request_body_length number;

  l_http_response utl_http.resp;
  l_response_header_name varchar2(256);
  l_response_header_value varchar2(1024);
  l_response_body varchar2(32767);

  l_offset number := 1;
  l_amount number := 2000;
  l_buffer varchar2(2000);
begin
  select my_file
  into l_attachment
  from my_files
  where seq_nr = 1;

  l_request_body := l_newline
|| '--' || lco_boundary || l_newline
|| 'Content-Disposition: form-data; name="attachment"; filename="file.pdf"' || l_newline
|| 'Content-Type: application/pdf' || l_newline
|| l_newline
|| apex_web_service.blob2clobbase64(l_attachment) || l_newline
|| '--' || lco_boundary || l_newline
|| 'Content-Disposition: form-data; name="filename"' || l_newline
|| l_newline
|| 'file.pdf' || l_newline
|| '--' || lco_boundary || l_newline
|| 'Content-Disposition: form-data; name="MAX_FILE_SIZE"' || l_newline
|| l_newline
|| '4000000' || l_newline
|| '--' || lco_boundary || '--';

  dbms_output.put_line('Request body>');
  dbms_output.put_line(dbms_lob.substr(l_request_body, 4000, 1));

  l_request_body_length := dbms_lob.getlength(l_request_body);

  utl_http.set_wallet(
    path => 'file:/path/to/wallet',
    password => 'my_secret_password'
  );

  l_http_request := utl_http.begin_request(
                      url => 'https://domain.com/rest/addfile/1288',
                      method => 'POST',
                      http_version => 'HTTP/1.1'
                    );

  utl_http.set_header(l_http_request, 'Content-Type', 'multipart/form-data; boundary="' || lco_boundary || '"');
  utl_http.set_header(l_http_request, 'Content-Length', l_request_body_length);

  while l_offset < l_request_body_length loop
    dbms_lob.read(l_request_body, l_amount, l_offset, l_buffer);
    utl_http.write_text(l_http_request, l_buffer);
    l_offset := l_offset + l_amount;
  end loop;

  l_http_response := utl_http.get_response(l_http_request);
  dbms_output.put_line('Response> Status Code: ' || l_http_response.status_code);
  dbms_output.put_line('Response> Reason Phrase: ' || l_http_response.reason_phrase);
  dbms_output.put_line('Response> HTTP Version: ' || l_http_response.http_version);

  for i in 1 .. utl_http.get_header_count(l_http_response) loop
    utl_http.get_header(l_http_response, i, l_response_header_name, l_response_header_value);
    dbms_output.put_line('Response> ' || l_response_header_name || ': ' || l_response_header_value);
  end loop;

  utl_http.read_text(l_http_response, l_response_body, 32767);
  dbms_output.put_line('Response body>');
  dbms_output.put_line(l_response_body);

  if l_http_request.private_hndl is not null then
    utl_http.end_request(l_http_request);
  end if;

  if l_http_response.private_hndl is not null then
    utl_http.end_response(l_http_response);
  end if;
exception
  when others then
    if l_http_request.private_hndl is not null then
      utl_http.end_request(l_http_request);
    end if;

    if l_http_response.private_hndl is not null then
      utl_http.end_response(l_http_response);
    end if;

    raise;
end;
anonymous block completed
Request body>

--gc0p4Jq0M2Yt08jU534c0p
Content-Disposition: form-data; name="attachment"; filename="file.pdf"
Content-Type: application/pdf

JVBERi0xLjUNCiW1tbW1DQoxIDAgb2JqDQo8PC9UeXBlL0NhdGFsb2cvUGFnZXMg
MiAwIFIvTGFuZyhubC1OTCkgL1N0cnVjdFRyZWVSb290IDEwIDAgUi9NYXJrSW5m
bzw8L01hcmtlZCB0cnVlPj4+Pg0KZW5kb2JqDQoyIDAgb2JqDQo8PC9UeXBlL1Bh
Z2VzL0NvdW50IDEvS2lkc1sgMyAwIFJdID4+DQplbmRvYmoNCjMgMCBvYmoNCjw8
L1R5cGUvUGFnZS9QYXJlbnQgMiAwIFIvUmVzb3VyY2VzPDwvRm9udDw8L0YxIDUg
MCBSPj4vRXh0R1N0YXRlPDwvR1M3IDcgMCBSL0dTOCA4IDAgUj4+L1Byb2NTZXRb
L1BERi9UZXh0L0ltYWdlQi9JbWFnZUMvSW1hZ2VJXSA+Pi9NZWRpYUJveFsgMCAw
IDU5NS4yIDg0MS45Ml0gL0NvbnRlbnRzIDQgMCBSL0dyb3VwPDwvVHlwZS9Hcm91
cC9TL1RyYW5zcGFyZW5jeS9DUy9EZXZpY2VSR0I+Pi9UYWJzL1MvU3RydWN0UGFy
ZW50cyAwPj4NC...

Response> Status Code: 200
Response> Reason Phrase: OK
Response> HTTP Version: HTTP/1.1
Response> Date: Mon, 22 Feb 2016 06:52:52 GMT
Response> Server: Apache
Response> X-Powered-By: PHP/5.6.5 ZendServer/8.0.2
Response> ZRay-ID: 39@168458@1455872499@0
Response> Expires: Thu, 19 Nov 1981 08:52:00 GMT
Response> Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Response> Pragma: no-cache
Response> Connection: close
Response> Transfer-Encoding: chunked
Response> Content-Type: application/xml
Response body>

<?xml version="1.0" ?>
<RestResponse service="enquiryattachfile">
  <attachedFileSize>125328</attachedFileSize>
</RestResponse>