How to Upload Files with an ORDS POST Handler

Receive files over REST in an ORDS PL/SQL handler, store them as BLOBs with their MIME type and name, and avoid reading :body twice.

Uploading a file through a REST API is a POST whose body is the file itself. In an ORDS PL/SQL handler, the raw body arrives as a BLOB in the :body bind, and its MIME type in :content_type. A header can carry the file name. This guide stores uploaded files in a table and shows the one rule that is easy to miss: :body can be read only once.

Before You Start

You need ORDS installed and running against your database, and a schema to work in. The examples use NIMBUS, the sample schema of a fictional airline, which you can install with the scripts in the setup/nimbus folder of the Oracle Database 26ai code repository on GitHub. The schema comes from Oracle Database 26ai SQL and PL/SQL Book.

ORDS in the examples answers at https://localhost:8443/ords/, and NIMBUS is REST-enabled with the URL alias nimbus. Replace the host and port with your own ORDS address. The curl commands use -k because the test server has a self-signed certificate; leave it out when your certificate is trusted. JSON responses are formatted for reading; ORDS returns them on one line.

The handler joins the lounge module from How to Write a POST Handler That Inserts Rows in ORDS.

Syntax

declare
  v_file blob := :body;          -- read the body once, into a variable
begin
  insert into files (name, mime_type, content) values (:file_name, :content_type, v_file);
end;

A Table for Files

Example:

create table offer_files (
  file_id      number generated always as identity primary key,
  file_name    varchar2(200) not null,
  mime_type    varchar2(100) not null,
  content      blob not null,
  uploaded_at  timestamp default systimestamp
);

The Upload Handler

The X-File-Name request header is mapped to the :file_name bind with ORDS.DEFINE_PARAMETER. The handler answers 201 with the new file ID and its size.

Example:

begin
  ords.define_template(p_module_name => 'lounge', p_pattern => 'files');

  ords.define_handler(p_module_name => 'lounge',
                      p_pattern     => 'files',
                      p_method      => 'POST',
                      p_source_type => ords.source_type_plsql,
                      p_source      => q'[
declare
  v_file blob := :body;        -- the request body; :body can be read only once
  v_id   offer_files.file_id%type;
begin
  insert into offer_files (file_name, mime_type, content)
  values (:file_name, :content_type, v_file)
  returning file_id into v_id;
  :status_code := 201;
  owa_util.mime_header('application/json', true);
  htp.p(json_object('file_id' value v_id, 'bytes' value dbms_lob.getlength(v_file)));
end;]');

  ords.define_parameter(p_module_name => 'lounge', p_pattern => 'files', p_method => 'POST',
                        p_name => 'X-File-Name', p_bind_variable_name => 'file_name',
                        p_source_type => 'HEADER', p_param_type => 'STRING',
                        p_access_method => 'IN');
  commit;
end;
/

Upload Two Files

curl sends a file as the raw body with --data-binary; -d would alter it.

Example:

curl -k -X POST https://localhost:8443/ords/nimbus/lounge/files \
  -H "Content-Type: image/png" \
  -H "X-File-Name: dxb-lounge-offer.png" \
  --data-binary @dxb-lounge-offer.png

Output (HTTP 201 Created):

{
    "file_id": 1,
    "bytes": 17879
}

A CSV file goes the same way with Content-Type: text/csv, and the table then holds both:

Example:

select file_id, file_name, mime_type, dbms_lob.getlength(content) as bytes from offer_files;

Output:

   FILE_ID FILE_NAME               MIME_TYPE       BYTES
__________ _______________________ ____________ ________
         1 dxb-lounge-offer.png    image/png       17879
         2 fuel_prices.csv         text/csv          279

Read :body Only Once

:body is a stream: the first reference consumes it. A first version of this handler inserted :body and then returned DBMS_LOB.GETLENGTH(:body); the insert worked, but the second reference was empty and the response said "bytes": null. Copying :body into a variable first, as above, avoids that.

Things to Know

  • For multipart form uploads from HTML forms, ORDS provides the files through ORDS.BODY_FILE_COUNT and ORDS.GET_BODY_FILE instead.
  • Limit the size of uploads at the web server or ORDS level, and check MIME types before storing files.
  • Protect upload endpoints with a privilege; an open upload URL invites abuse.

Related Guides

Conclusion

ORDS hands a POST body to PL/SQL as the :body BLOB, with :content_type for its MIME type, and request headers can carry details such as the file name. Read :body once into a variable, store it, and answer 201 with what was saved.

Vinish Kapoor
Vinish Kapoor

An Oracle ACE, author of four books on Oracle APEX, SQL and PL/SQL, and Oracle Forms, and a software developer building Oracle database applications since 2001.

guest

0 Comments
Oldest
Newest Most Voted